Folder-Based Routing

Learn how folders define URL segments and create nested routes automatically in Bini.js.

Overview

Bini.js uses a folder-based routing system where the folder structure inside src/app/ directly maps to URL paths. This makes routing intuitive and eliminates the need for manual route configuration.

Intuitive Structure

Folders map directly to URL segments

Dynamic Segments

Create dynamic routes with [param] syntax

Catch-all Routes

Match multiple segments with [...] syntax

Private Folders

Exclude folders with _ prefix

Basic Folder Routing

Each folder inside src/app/ becomes a URL segment. Add a page.tsx file inside to make the route publicly accessible.

src/app/ ├── page.tsx → / ├── about/ │ └── page.tsx → /about ├── blog/ │ └── page.tsx → /blog └── contact/ └── page.tsx → /contact

This creates four routes: /, /about, /blog, and /contact.

Flat File Support

Bini.js also supports flat files at the root level. These work the same way as folder-based routes:

src/app/ ├── page.tsx → / ├── about.tsx → /about ├── blog.tsx → /blog └── contact.tsx → /contact
Both patterns work: You can mix folder-based and flat file routing. Choose whichever makes your project organization clearer.

Nested Routes

Nest folders inside each other to create nested URL segments. Each level adds another segment to the URL path.

src/app/ ├── blog/ │ ├── page.tsx → /blog │ ├── authors/ │ │ └── page.tsx → /blog/authors │ └── categories/ │ ├── page.tsx → /blog/categories │ └── [name]/ │ └── page.tsx → /blog/categories/tech

The folder structure directly mirrors the URL structure. Deep nesting is fully supported up to 100 levels deep.

Layout inheritance: Nested routes automatically inherit layouts from parent folders. Each folder can have its own layout.tsx that wraps all routes in that folder.

Dynamic Segments

Use square brackets [param] to create dynamic route segments that match any value. Access the value with useParams() (auto-imported).

Dynamic Folder Example

src/app/ ├── blog/ │ └── [slug]/ │ └── page.tsx → /blog/hello-world │ → /blog/getting-started │ → /blog/any-value ├── products/ │ └── [id]/ │ └── page.tsx → /products/123 │ → /products/abc-456 └── users/ └── [userId]/ ├── page.tsx → /users/john └── settings/ └── page.tsx → /users/john/settings

Using Dynamic Params

app/blog/[slug]/page.tsx
// src/app/blog/[slug]/page.tsx
export default function BlogPost() {
  const { slug } = useParams() // Auto-imported
  
  return (
    <article>
      <h1 className="text-3xl font-bold text-white">Post: {slug}</h1>
    </article>
  )
}

Flat File Dynamic Routes

src/app/ ├── blog/ │ └── [slug].tsx → /blog/:slug ├── user/ │ └── [id].tsx → /user/:id └── product/ └── [sku].tsx → /product/:sku
Param naming: Parameter names must match /^[a-zA-Z_][a-zA-Z0-9_]*$/ and are validated at scan time.

Catch-all Segments

Use [...segment] to match multiple URL segments. The parameter becomes an array of values.

Catch-all Example

src/app/ ├── docs/ │ └── [...slug]/ │ └── page.tsx → /docs/getting-started │ → /docs/api/reference │ → /docs/guides/routing/basics
app/docs/[...slug]/page.tsx
// src/app/docs/[...slug]/page.tsx
export default function DocsPage() {
  const { slug } = useParams()
  // slug is an array: ['api', 'reference']
  
  return (
    <div>
      <h1 className="text-2xl font-bold text-white">Documentation</h1>
      <p className="text-slate-400">Path: {slug?.join(' / ')}</p>
    </div>
  )
}
URLslug value
/docs/getting-started['getting-started']
/docs/api/reference['api', 'reference']
/docs/guides/routing/basics['guides', 'routing', 'basics']

Flat File Catch-all

src/app/ ├── docs/ │ └── [...slug].tsx → /docs/* (catch-all) └── api/ └── [...path].ts → /api/* (catch-all API route)

Optional Catch-all Segments

Use [[...segment]] to make the catch-all optional. The route will also match the parent path.

Optional Catch-all Example

src/app/ ├── shop/ │ └── [[...slug]]/ │ └── page.tsx → /shop │ → /shop/clothing │ → /shop/clothing/shirts
app/shop/[[...slug]]/page.tsx
// src/app/shop/[[...slug]]/page.tsx
export default function ShopPage() {
  const { slug } = useParams()
  // slug is undefined for /shop
  // slug is ['clothing'] for /shop/clothing
  
  if (!slug) {
    return <h1 className="text-2xl font-bold text-white">Shop Home</h1>
  }
  
  return (
    <div>
      <h1 className="text-2xl font-bold text-white">
        Category: {slug.join(' / ')}
      </h1>
    </div>
  )
}
URLslug value
/shopundefined
/shop/clothing['clothing']
/shop/clothing/shirts['clothing', 'shirts']

Route Groups

Use parentheses (group) to organize routes without affecting the URL. Perfect for grouping related pages or applying shared layouts.

Route Group Example

src/app/ ├── (marketing)/ │ ├── page.tsx → / │ ├── about/ │ │ └── page.tsx → /about │ └── pricing/ │ └── page.tsx → /pricing ├── (shop)/ │ ├── page.tsx → / │ ├── products/ │ │ └── page.tsx → /products │ └── cart/ │ └── page.tsx → /cart └── (admin)/ ├── layout.tsx ← Layout only for admin routes ├── page.tsx → / └── dashboard/ └── page.tsx → /dashboard

Notice how (marketing), (shop), and (admin) don't appear in the URLs. They're purely for organization.

Route Groups with Layouts

Route groups are especially useful for applying different layouts to different sections:

src/app/ ├── (marketing)/ │ ├── layout.tsx ← Marketing layout (different header/footer) │ └── page.tsx → / ├── (dashboard)/ │ ├── layout.tsx ← Dashboard layout (sidebar + header) │ └── settings/ │ └── page.tsx → /settings └── layout.tsx ← Root layout (applies to all)
Layout inheritance: Route groups are great for organizing layouts. Each group can have its own layout.tsx that only applies to routes in that group.

Private Folders

Prefix a folder with an underscore _folder to exclude it from routing. Perfect for components, utilities, and other non-route files.

Private Folder Example

src/app/ ├── _components/ ← Not routable │ ├── Header.tsx │ ├── Footer.tsx │ └── Button.tsx ├── _lib/ ← Not routable │ ├── api.ts │ └── utils.ts ├── _hooks/ ← Not routable │ └── useAuth.ts ├── blog/ │ ├── _components/ ← Not routable │ │ └── PostCard.tsx │ └── page.tsx → /blog └── page.tsx → /

Private folders can be placed anywhere in the app directory and are completely ignored by the router.

Ignored Patterns

  • Folders starting with _ (underscore)
  • Folders starting with . (dot)
  • Files starting with _ or .
  • The api/ directory (reserved for API routes)

Nearest Wins with Folders

loading.tsx, not-found.tsx, and error.tsx follow the folder hierarchy using "nearest wins" resolution. A file in a subfolder only affects that subfolder and shadows (without deleting) the same file in any ancestor folder.

Folder Hierarchy and Boundaries

  • Each folder can define its own loading.tsx, error.tsx, and not-found.tsx
  • A file in a subfolder only affects routes inside that subfolder
  • It shadows the same file in ancestor folders for routes in that subfolder
  • Routes without a closer match fall through to the nearest ancestor
  • If no file exists anywhere in the hierarchy, the built-in default is used

Example Structure

src/app/ ├── layout.tsx ├── page.tsx ├── loading.tsx ← Default loading for all routes ├── not-found.tsx ← Default 404 for all routes ├── error.tsx ← Default error for all routes ├── dashboard/ │ ├── layout.tsx │ ├── page.tsx │ ├── loading.tsx ← Only affects /dashboard/* │ ├── error.tsx ← Only affects /dashboard/* │ └── settings/ │ └── page.tsx ← Uses dashboard/loading.tsx and dashboard/error.tsx └── blog/ ├── page.tsx ├── loading.tsx ← Only affects /blog/* └── [slug]/ └── page.tsx ← Uses blog/loading.tsx

Resolution Flow

When a route needs a boundary file, the router checks:

  1. The route's own folder first
  2. Each parent folder (going up the hierarchy)
  3. The built-in default if no file is found
Nearest Wins: A file in a subfolder shadows ancestor files for routes in that subfolder, but doesn't delete them for other routes. This is the same mental model as layouts.

Route Priority

When multiple routes could match a URL, Bini.js resolves them in this order:

  1. Static routesexact matches — e.g., /about
  2. Dynamic single segments[slug] — e.g., /blog/:slug
  3. Catch-all segments[...slug] — e.g., /docs/*
  4. Optional catch-all segments[[...slug]] — e.g., /shop/* (optional)

Priority Example

Consider this folder structure:

src/app/ ├── blog/ │ ├── page.tsx → /blog │ ├── [slug]/ │ │ └── page.tsx → /blog/:slug │ └── [...slug]/ │ └── page.tsx → /blog/* (catch-all)
URLMatched Route
/blog/blog (static)
/blog/hello-world/blog/:slug (dynamic)
/blog/hello/world/blog/* (catch-all)

This ensures predictable routing behavior and prevents conflicts between different route types.

Complete Example

Here is a comprehensive folder structure showing all routing patterns:

src/app/ ├── (marketing)/ ← Route group (not in URL) │ ├── layout.tsx ← Layout for marketing pages │ ├── page.tsx → / │ ├── about/ │ │ └── page.tsx → /about │ └── _components/ ← Private folder │ └── Hero.tsx ├── blog/ │ ├── layout.tsx ← Layout for blog section │ ├── page.tsx → /blog │ ├── loading.tsx ← Blog loading UI (nearest wins) │ ├── [slug]/ ← Dynamic segment │ │ └── page.tsx → /blog/:slug │ ├── authors/ │ │ └── page.tsx → /blog/authors │ └── categories/ │ └── [...slug]/ ← Catch-all │ └── page.tsx → /blog/categories/tech/news ├── docs/ │ └── [[...slug]]/ ← Optional catch-all │ └── page.tsx → /docs │ → /docs/getting-started ├── api/ ← API routes │ ├── hello.ts → /api/hello │ └── users/ │ └── [id].ts → /api/users/:id ├── layout.tsx ← Root layout ├── page.tsx → / ├── loading.tsx ← Global loading UI ├── error.tsx ← Global error UI └── not-found.tsx ← Custom 404 page

Route Mapping

Folder PathURLType
app/page.tsx/Static
app/about/page.tsx/aboutStatic
app/blog/page.tsx/blogStatic
app/blog/[slug]/page.tsx/blog/:slugDynamic
app/blog/authors/page.tsx/blog/authorsStatic
app/blog/categories/[...slug]/page.tsx/blog/categories/*Catch-all
app/docs/[[...slug]]/page.tsx/docs/*Optional Catch-all
app/api/hello.ts/api/helloAPI
app/api/users/[id].ts/api/users/:idAPI Dynamic