Dynamic Routes

Learn how to create dynamic routes with parameters, catch-all segments, and optional catch-all segments in Bini.js.

Overview

Dynamic routes allow you to create pages that match a pattern rather than a static path. This is essential for pages like blog posts, product pages, user profiles, and documentation.

Dynamic Segments

Single parameter routes with [param]

Catch-all Routes

Match multiple segments with [...]

Optional Catch-all

Optional multi-segment routes with [[...]]

Auto-import: useParams() is auto-imported in all pages and layouts — no import statement needed.

Dynamic Segments

Create a dynamic segment by wrapping a folder or file name in square brackets: [name]. The parameter name must match /^[a-zA-Z_][a-zA-Z0-9_]*$/.

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 → /users/mary

Accessing Parameters

Access the parameter value using useParams(), which is auto-imported:

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

With Data Fetching

app/user/[id]/page.tsx
// src/app/user/[id]/page.tsx
export default function UserProfile() {
  const { id } = useParams()
  const [user, setUser] = useState(null)
  
  useEffect(() => {
    fetchUser(id).then(setUser)
  }, [id])
  
  if (!user) return <Loading />
  
  return (
    <div>
      <h1 className="text-2xl font-bold text-white">{user.name}</h1>
      <p className="text-slate-400">{user.email}</p>
    </div>
  )
}

Multiple Parameters

You can have multiple dynamic segments in a single route. Each segment becomes a property in the useParams() object.

Example with Multiple Params

src/app/ └── blog/ └── [category]/ └── [slug]/ └── page.tsx → /blog/tech/hello-world → /blog/lifestyle/travel-tips
app/blog/[category]/[slug]/page.tsx
// src/app/blog/[category]/[slug]/page.tsx
export default function BlogPost() {
  const { category, slug } = useParams()
  
  return (
    <div>
      <p className="text-cyan-400">Category: {category}</p>
      <h1 className="text-3xl font-bold text-white">Post: {slug}</h1>
    </div>
  )
}
URLparams
/blog/tech/hello-world{ category: "tech", slug: "hello-world" }
/blog/lifestyle/travel{ category: "lifestyle", slug: "travel" }
/blog/design/ux-tips{ category: "design", slug: "ux-tips" }

Catch-all Segments

Use [...name] to match any number of segments. The parameter becomes an array of the matched segments. This is perfect for documentation, file paths, or any multi-level navigation.

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, e.g., ['api', 'reference']
  
  return (
    <div>
      <h1 className="text-2xl font-bold text-white">Documentation</h1>
      <p className="text-slate-400">Path: {slug?.join(' / ')}</p>
      
      <div className="mt-4 p-4 bg-slate-900/50 rounded-lg">
        <p className="text-slate-300 text-sm">
          {slug?.length || 0} segment(s) in the path
        </p>
      </div>
    </div>
  )
}
URLslug value
/docs/getting-started['getting-started']
/docs/api/reference['api', 'reference']
/docs/guides/routing/basics['guides', 'routing', 'basics']
/docs/advanced/custom/hooks['advanced', 'custom', 'hooks']

Flat File Catch-all

src/app/ ├── docs/ │ └── [...slug].tsx → /docs/* (catch-all) └── api/ └── [...path].ts → /api/* (catch-all API route)
Priority: Catch-all segments have lower priority than static routes and dynamic single segments. For example, /blog/featured will match a static route if it exists, falling back to the catch-all only if no more specific route matches.

Optional Catch-all Segments

Use [[...name]] to make the catch-all optional. The route matches even without any segments, making it perfect for multi-level navigation like documentation or shop categories.

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()
  
  if (!slug) {
    return (
      <div>
        <h1 className="text-2xl font-bold text-white">Shop Home</h1>
        <p className="text-slate-400">Browse all categories</p>
      </div>
    )
  }
  
  return (
    <div>
      <h1 className="text-2xl font-bold text-white">
        Category: {slug.join(' / ')}
      </h1>
      <p className="text-slate-400">Depth: {slug.length}</p>
    </div>
  )
}
URLslug value
/shopundefined
/shop/clothing['clothing']
/shop/clothing/shirts['clothing', 'shirts']
/shop/electronics/phones/iphone['electronics', 'phones', 'iphone']

Dynamic Segments in Layouts

Layouts can also access dynamic parameters using useParams(), which is auto-imported. This is useful for displaying contextual information in headers, sidebars, or breadcrumbs.

Layout with Dynamic Params

app/blog/[slug]/layout.tsx
// src/app/blog/[slug]/layout.tsx
export default function BlogLayout() {
  const { slug } = useParams()
  
  return (
    <div>
      <header className="border-b border-slate-800 p-4">
        <h2 className="text-xl font-bold text-white">
          Post: {slug}
        </h2>
        <nav className="text-sm text-slate-400">
          <Link to="/blog">← Back to blog</Link>
        </nav>
      </header>
      <main className="p-4">
        <Outlet />
      </main>
    </div>
  )
}

File Structure

src/app/ └── blog/ └── [slug]/ ├── layout.tsx ← Layout with access to {slug} └── page.tsx ← Main content
Layout inheritance: The layout wraps the page and any nested routes, providing consistent UI across the dynamic route section.

Flat File Dynamic Routes

Dynamic routes can also be created as flat files without folders. This is especially useful for simpler pages where a folder structure would be unnecessary overhead.

Flat File Examples

src/app/ ├── blog/ │ ├── [slug].tsx → /blog/hello-world │ └── [category]-[slug].tsx → /blog/tech-hello-world ├── products/ │ └── [id].tsx → /products/123 ├── users/ │ └── [userId].tsx → /users/john └── docs/ └── [...slug].tsx → /docs/* (catch-all)
app/blog/[slug].tsx
// src/app/blog/[slug].tsx
export default function BlogPost() {
  const { slug } = useParams()
  return (
    <h1 className="text-3xl font-bold text-white">Post: {slug}</h1>
  )
}

When to Use Flat Files

  • Simple pages that don't need nested layouts
  • API routes with dynamic parameters
  • Single-level dynamic pages (e.g., /post/:id)
  • When you want to reduce folder nesting

Route Priority

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

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

Priority Example

Consider this folder structure with overlapping routes:

src/app/blog/ ├── featured/ │ └── page.tsx → /blog/featured (static - highest priority) ├── [slug]/ │ └── page.tsx → /blog/anything-else (dynamic) └── [...slug]/ └── page.tsx → /blog/a/b/c (catch-all)
URLMatched RoutePriority
/blog/featuredfeatured/page.tsxStatic
/blog/hello-world[slug]/page.tsxDynamic
/blog/a/b/c[...slug]/page.tsxCatch-all
/blog/latest/post[slug]/page.tsxDynamic

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

Complete Example

Here is a comprehensive example showing all dynamic route patterns in a real-world application:

src/app/ ├── blog/ │ ├── featured/ │ │ └── page.tsx → /blog/featured (static) │ ├── [slug]/ │ │ ├── layout.tsx ← Layout for single post with {slug} │ │ └── page.tsx → /blog/:slug (dynamic) │ ├── [category]/ │ │ └── [slug]/ │ │ └── page.tsx → /blog/:category/:slug (multiple dynamic) │ └── [...slug]/ │ └── page.tsx → /blog/a/b/c (catch-all) ├── docs/ │ └── [[...slug]]/ │ ├── layout.tsx ← Layout for docs with optional catch-all │ └── page.tsx → /docs (optional catch-all) │ → /docs/getting-started ├── products/ │ ├── page.tsx → /products │ ├── [id].tsx → /products/:id (flat file) │ └── categories/ │ └── [name]/ │ └── page.tsx → /products/categories/:name ├── users/ │ └── [userId]/ │ ├── page.tsx → /users/:userId │ └── settings/ │ └── page.tsx → /users/:userId/settings └── api/ ├── posts/ │ └── [id].ts → /api/posts/:id └── users/ └── [...path].ts → /api/users/* (catch-all API)

Route Mapping

PatternExample URLType
/blog/featured/blog/featuredStatic
/blog/:slug/blog/hello-worldDynamic Single
/blog/:category/:slug/blog/tech/hello-worldMultiple Dynamic
/blog/*/blog/a/b/cCatch-all
/docs/* (optional)/docsOptional Catch-all
/docs/* (optional)/docs/getting-startedOptional Catch-all
/products/:id/products/123Flat File Dynamic
/users/:userId/settings/users/john/settingsNested Dynamic