Catch-All Routes

Learn how to use catch-all routes to match multiple URL segments in Bini.js. Perfect for documentation, nested categories, and flexible URL structures.

Overview

Catch-all routes allow you to match multiple URL segments in a single route. They are defined using the [...name] syntax, where the parameter becomes an array of the matched segments.

Variable Depth

Match any number of URL segments

Array Parameters

Access segments as an array

Flexible Structure

Perfect for documentation and nested categories

Multi-language

Handle language prefixes with variable paths

Auto-import: useParams() is auto-imported in all pages — no import statement needed to access catch-all parameters.

What are Catch-All Routes?

Catch-all routes are a powerful feature that allows you to match any number of URL segments after a specific path. They are defined using the [...name] syntax in folder or file names.

Key Characteristics

  • Matches multiple segments: Any number of URL segments after the parent path
  • Array parameter: The parameter becomes an array of all matched segments
  • Lower priority: Static and dynamic routes are matched first
  • Optional version: Use [[...name]] for optional catch-all
src/app/ └── docs/ └── [...slug]/ └── page.tsx → /docs/getting-started → /docs/api/reference → /docs/guides/routing/basics

In this example, /docs/getting-started matches with slug = ['getting-started'], while /docs/guides/routing/basics matches with slug = ['guides', 'routing', 'basics'].

Basic Usage

Create a catch-all route by naming a folder or file with square brackets and three dots: [...name].

Catch-All Examples

src/app/ ├── blog/ │ └── [...slug]/ │ └── page.tsx → /blog/a/b/c │ → /blog/2024/01/hello-world ├── products/ │ └── [...path]/ │ └── page.tsx → /products/electronics/phones │ → /products/clothing/men/shirts └── users/ └── [...ids]/ └── page.tsx → /users/1/2/3

The route will match any URL that starts with the parent path and has at least one segment. This is different from optional catch-all routes, which match even with zero segments.

At least one segment required: A regular catch-all route [...slug] requires at least one segment. Use [[...slug]] for optional catch-all that matches the parent path too.

Accessing Parameters

Use useParams() (auto-imported) to access the catch-all parameter as an array. The parameter name becomes a property on the params object.

Basic Access

app/docs/[...slug]/page.tsx
// src/app/docs/[...slug]/page.tsx
export default function DocsPage() {
  const { slug } = useParams()
  // slug is an array of the URL segments
  
  return (
    <div>
      <h1 className="text-2xl font-bold text-white">Documentation</h1>
      <p className="text-slate-400">Path: {slug?.join(' / ')}</p>
      <p className="text-slate-400">Depth: {slug?.length || 0}</p>
    </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']

With Data Fetching

app/blog/[...slug]/page.tsx
// src/app/blog/[...slug]/page.tsx
export default function BlogArchive() {
  const { slug } = useParams()
  const [posts, setPosts] = useState([])
  
  useEffect(() => {
    // Fetch posts based on the path segments
    const path = slug?.join('/')
    fetchPosts(path).then(setPosts)
  }, [slug])
  
  return (
    <div>
      <h1 className="text-2xl font-bold text-white">
        Archive: {slug?.join(' / ') || 'Home'}
      </h1>
      <div className="space-y-2">
        {posts.map(post => (
          <div key={post.id} className="p-4 bg-slate-900/50 rounded">
            <h2 className="text-white font-medium">{post.title}</h2>
          </div>
        ))}
      </div>
    </div>
  )
}

Nested Catch-All Routes

Catch-all routes can be combined with other dynamic and static segments to create complex routing patterns.

Combining with Dynamic Segments

src/app/ ├── blog/ │ ├── featured/ │ │ └── page.tsx → /blog/featured (static - highest priority) │ └── [...slug]/ │ └── page.tsx → /blog/a/b/c (catch-all) ├── products/ │ └── [category]/ │ └── [...slug]/ │ └── page.tsx → /products/electronics/phones/iphone │ → /products/clothing/men/shirts └── users/ └── [userId]/ └── [...posts]/ └── page.tsx → /users/john/posts/1
app/products/[category]/[...slug]/page.tsx
// src/app/products/[category]/[...slug]/page.tsx
export default function ProductPage() {
  const { category, slug } = useParams()
  
  return (
    <div>
      <h1 className="text-2xl font-bold text-white">Category: {category}</h1>
      <p className="text-slate-400">Path: {slug?.join(' / ')}</p>
      <p className="text-slate-400">Segments: {slug?.length || 0}</p>
    </div>
  )
}

Optional Catch-All Routes

Use [[...name]] to make the catch-all optional. The route will match both the parent path and any nested paths.

Optional Catch-All Example

src/app/ ├── shop/ │ └── [[...slug]]/ │ └── page.tsx → /shop │ → /shop/clothing │ → /shop/clothing/shirts └── docs/ └── [[...path]]/ └── page.tsx → /docs → /docs/getting-started → /docs/api/reference
app/shop/[[...slug]]/page.tsx
// src/app/shop/[[...slug]]/page.tsx
export default function ShopPage() {
  const { slug } = useParams()
  
  if (!slug || slug.length === 0) {
    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 (or empty array)
/shop/clothing['clothing']
/shop/clothing/shirts['clothing', 'shirts']
Use case: Optional catch-all routes are perfect for documentation pages where the root path (/docs) should show a landing page, and nested paths (/docs/getting-started) show specific content.

File-Based Catch-All Routes

Catch-all routes can also be defined as flat files without folders. This reduces folder nesting for simpler use cases.

Flat File Examples

src/app/ ├── docs/ │ └── [...slug].tsx → /docs/getting-started │ → /docs/api/reference ├── products/ │ └── [...path].tsx → /products/electronics/phones │ → /products/clothing/men └── blog/ └── [...slug].tsx → /blog/2024/01/hello-world
app/blog/[...slug].tsx
// src/app/blog/[...slug].tsx
export default function BlogArchive() {
  const { slug } = useParams()
  
  return (
    <div>
      <h1 className="text-2xl font-bold text-white">Blog Archive</h1>
      <p className="text-slate-400">Path: {slug?.join(' / ')}</p>
    </div>
  )
}

Route Priority

Catch-all routes have lower priority than static routes and dynamic single segments. The router resolves matches 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/hello-world (dynamic) └── [...slug]/ └── page.tsx → /blog/2024/01/hello-world (catch-all)
URLMatched RoutePriority
/blog/featuredfeatured/page.tsxStatic
/blog/hello-world[slug]/page.tsxDynamic
/blog/2024/01/hello-world[...slug]/page.tsxCatch-all

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

Use Cases

Catch-all routes are ideal for:

Documentation

Multi-level documentation with variable depth

/docs/guides/routing/basics
E-commerce Categories

Nested category structures

/products/electronics/phones/iphone
Blog Archives

Date-based archives

/blog/2024/01/hello-world
Multi-language Sites

Language prefixes with variable paths

/en/docs/getting-started

Additional Use Cases

  • CMS Content: Content pages with flexible URL structures
  • API Versioning: API routes with version segments like /api/v1/users/123
  • File Browser: Directory browsing with arbitrary depth
  • Wiki Pages: Multi-level wiki documentation
  • Path-Based Navigation: Any URL structure where depth varies

Complete Example

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

src/app/ ├── blog/ │ ├── featured/ │ │ └── page.tsx → /blog/featured (static) │ ├── [slug]/ │ │ └── page.tsx → /blog/:slug (dynamic) │ └── [...slug]/ │ └── page.tsx → /blog/2024/01/hello-world (catch-all) ├── docs/ │ └── [[...slug]]/ │ ├── layout.tsx ← Layout for docs │ └── page.tsx → /docs (optional catch-all) │ → /docs/getting-started ├── products/ │ └── [category]/ │ └── [...slug]/ │ └── page.tsx → /products/electronics/phones/iphone ├── shop/ │ └── [[...slug]]/ │ └── page.tsx → /shop (optional catch-all) │ → /shop/clothing │ → /shop/clothing/shirts ├── wiki/ │ └── [[...path]]/ │ └── page.tsx → /wiki (optional catch-all) │ → /wiki/guides/routing └── api/ └── v1/ └── [...path].ts → /api/v1/users/123 (flat file catch-all)

Route Mapping

PatternExample URLType
/blog/featured/blog/featuredStatic
/blog/:slug/blog/hello-worldDynamic Single
/blog/*/blog/2024/01/hello-worldCatch-all
/docs/* (optional)/docsOptional Catch-all
/docs/* (optional)/docs/getting-startedOptional Catch-all
/products/:category/*/products/electronics/phones/iphoneNested Catch-all
/shop/* (optional)/shop/clothing/shirtsOptional Catch-all
/api/v1/*/api/v1/users/123Flat File Catch-all