Regular dynamic routes match one segment. But what if you need to match any number of segments? Documentation sites with nested pages, file browsers with deep folder structures, or category systems with unlimited nesting all need this flexibility. That's where catch-all routes come in, using the [...slug] and [[...slug]] patterns.
The Problem with Regular Dynamic Routes
Let's say you're building a documentation site. You want these URLs to all work:
/docs/introduction/docs/getting-started/installation/docs/api/components/button/docs/guides/deployment/vercel/setup
With regular dynamic routes, you'd need to know the maximum depth ahead of time:
app/
docs/
[level1]/
page.tsx ā /docs/introduction
[level2]/
page.tsx ā /docs/getting-started/installation
[level3]/
page.tsx ā /docs/api/components/button
[level4]/
page.tsx ā /docs/guides/deployment/vercel/setup
[level5]/ ā What if you need more depth?
page.tsxThis is terrible! You're duplicating code, limiting depth, and making maintenance a nightmare. There has to be a better way...
The Solution: Catch-All Routes
Catch-all routes use [...slug] to match any number of segments:
app/
docs/
[...slug]/
page.tsx ā One file handles ALL nested paths!Now this single route handles:
/docs/introduction/docs/getting-started/installation/docs/api/components/button/docs/guides/deployment/vercel/setup/advanced/optimization- ...any depth you need!
The Three Dots (...)
The ... syntax is called the "spread" or "rest" operator. It means "capture all remaining segments." Think of it as saying "and everything after this."
Basic Catch-All: [...slug]
The [...slug] pattern matches one or more segments:
Creating a Basic Catch-All Route
app/
docs/
page.tsx ā /docs (home page)
[...slug]/
page.tsx ā /docs/* (everything else)interface PageProps {
params: {
slug: string[]; // Array of all segments!
};
}
export default function DocsPage({ params }: PageProps) {
// URL: /docs/getting-started
// params.slug = ["getting-started"]
// URL: /docs/api/components/button
// params.slug = ["api", "components", "button"]
// URL: /docs/advanced/features/auth/setup
// params.slug = ["advanced", "features", "auth", "setup"]
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-4">Documentation</h1>
{/* Show the path */}
<div className="text-gray-600 mb-8">
Path: {params.slug.join(' / ')}
</div>
{/* Render content based on slug */}
<div className="prose max-w-none">
<p>Showing content for: {params.slug.join('/')}</p>
</div>
</div>
);
}Catch-All Route Matching
See how [...slug] captures multiple segments
š File Structure
app/
docs/
[...slug]/
page.tsxš URL Path
/docs/getting-startedparams.slug = ["getting-started"]
š Slug is an Array
Unlike regular dynamic routes where params.slug is a string, catch-all routes return an array containing all matched segments.
Optional Catch-All: [[...slug]]
The [[...slug]] pattern (double brackets) matches zero or more segments:
The Key Difference
[...slug] - Required
Matches:
- ā /docs/intro
- ā /docs/api/setup
- ā /docs (no match)
Requires at least one segment. You need a separate page.tsx for /docs
[[...slug]] - Optional
Matches:
- ā /docs (no segments)
- ā /docs/intro
- ā /docs/api/setup
Matches the parent route too! No separate page.tsx needed.
app/
docs/
[[...slug]]/
page.tsx ā Handles /docs AND /docs/* (everything!)interface PageProps {
params: {
slug?: string[]; // Optional! Can be undefined
};
}
export default function DocsPage({ params }: PageProps) {
// URL: /docs
// params.slug = undefined
// URL: /docs/getting-started
// params.slug = ["getting-started"]
// URL: /docs/api/components/button
// params.slug = ["api", "components", "button"]
// Handle the root case
if (!params.slug) {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-4">Documentation Home</h1>
<p>Welcome to our documentation!</p>
</div>
);
}
// Handle nested paths
const path = params.slug.join('/');
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-4">
{params.slug[params.slug.length - 1]}
</h1>
<p className="text-gray-600">Path: {path}</p>
</div>
);
}Optional Catch-All Matching
[[...slug]] matches the parent route too
š File Structure
app/
docs/
[[...slug]]/
page.tsxš URL Path
/docsparams.slug = undefined (optional catch-all matches root)
Working with Segment Arrays
Since params.slug is an array, you can use array methods to navigate and build logic:
Common Operations
export default function DocsPage({
params
}: {
params: { slug?: string[] }
}) {
const slug = params.slug || [];
// Get the number of segments
const depth = slug.length;
// /docs/api/auth ā depth = 2
// Get first segment (category)
const category = slug[0];
// /docs/api/auth ā category = "api"
// Get last segment (page name)
const pageName = slug[slug.length - 1];
// /docs/api/auth ā pageName = "auth"
// Join into path
const fullPath = slug.join('/');
// /docs/api/auth ā fullPath = "api/auth"
// Check if in specific section
const isApiDocs = slug[0] === 'api';
// Get parent path
const parentPath = slug.slice(0, -1).join('/');
// /docs/api/auth ā parentPath = "api"
return <div>{/* Use these values */}</div>;
}Building Breadcrumbs
import Link from 'next/link';
export default function DocsPage({
params
}: {
params: { slug?: string[] }
}) {
const slug = params.slug || [];
// Build breadcrumb items
const breadcrumbs = [
{ label: 'Docs', href: '/docs' },
...slug.map((segment, index) => ({
label: segment.replace(/-/g, ' '),
href: `/docs/${slug.slice(0, index + 1).join('/')}`,
})),
];
return (
<div className="container mx-auto px-4 py-8">
{/* Breadcrumbs */}
<nav className="flex items-center gap-2 text-sm mb-6">
{breadcrumbs.map((crumb, index) => (
<div key={crumb.href} className="flex items-center gap-2">
{index > 0 && <span className="text-gray-400">/</span>}
<Link
href={crumb.href}
className="text-blue-600 hover:underline capitalize"
>
{crumb.label}
</Link>
</div>
))}
</nav>
{/* Page content */}
<h1 className="text-4xl font-bold">
{slug[slug.length - 1]?.replace(/-/g, ' ') || 'Home'}
</h1>
</div>
);
}Fetching Data with Catch-All Routes
Use the slug array to fetch the right content:
import { notFound } from 'next/navigation';
interface DocContent {
title: string;
content: string;
category: string;
}
interface PageProps {
params: { slug?: string[] };
}
export default async function DocsPage({ params }: PageProps) {
const slug = params.slug || [];
const path = slug.join('/');
// Fetch content based on the path
const res = await fetch(
`https://api.example.com/docs/${path || 'index'}`
);
if (!res.ok) {
notFound();
}
const doc: DocContent = await res.json();
return (
<article className="container mx-auto px-4 py-8 max-w-4xl">
<header className="mb-8">
<div className="text-sm text-gray-600 mb-2">
{doc.category}
</div>
<h1 className="text-4xl font-bold">{doc.title}</h1>
</header>
<div className="prose max-w-none">
{doc.content}
</div>
{/* Navigation based on depth */}
{slug.length > 0 && (
<footer className="mt-12 pt-6 border-t">
<Link
href={slug.length > 1
? `/docs/${slug.slice(0, -1).join('/')}`
: '/docs'
}
className="text-blue-600 hover:underline"
>
ā Back to {slug.length > 1 ? 'Parent' : 'Home'}
</Link>
</footer>
)}
</article>
);
}Real-World Use Cases
1. Documentation Site
app/
docs/
[[...slug]]/
page.tsx
Handles:
/docs ā Home
/docs/introduction ā Introduction
/docs/getting-started/install ā Installation guide
/docs/api/components/button ā Button API docs2. File Browser/Explorer
app/
files/
[...path]/
page.tsx
Handles:
/files/documents ā Documents folder
/files/documents/2024 ā 2024 subfolder
/files/documents/2024/reports ā Reports subfolder
/files/photos/vacation/hawaii ā Deep nestingexport default async function FileBrowserPage({
params,
}: {
params: { path: string[] };
}) {
const currentPath = params.path.join('/');
// Fetch files and folders at this path
const items = await fetch(`/api/files?path=${currentPath}`)
.then(r => r.json());
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">
/{currentPath}
</h1>
<div className="grid grid-cols-4 gap-4">
{items.map((item) => (
<Link
key={item.name}
href={`/files/${currentPath}/${item.name}`}
className="p-4 border rounded hover:shadow-lg"
>
<div className="text-4xl mb-2">
{item.type === 'folder' ? 'š' : 'š'}
</div>
<div className="font-semibold">{item.name}</div>
</Link>
))}
</div>
</div>
);
}3. Category Hierarchy (E-commerce)
app/
shop/
categories/
[...category]/
page.tsx
Handles:
/shop/categories/electronics
/shop/categories/electronics/computers
/shop/categories/electronics/computers/laptops
/shop/categories/electronics/computers/laptops/gaming4. Multi-Language Routes
app/
[locale]/
[[...slug]]/
page.tsx
Handles:
/en ā English home
/en/about ā English about
/es/productos/electronicos ā Spanish electronics
/fr/docs/api/authentication ā French API docsCatch-All Routes in Practice
See real-world catch-all route structures
Select a file or folder to see details
Generating Static Paths
For catch-all routes you want to pre-render, use generateStaticParams:
// Generate all doc paths at build time
export async function generateStaticParams() {
const docs = await fetch('https://api.example.com/docs/all')
.then(res => res.json());
// Return array of slug arrays
return docs.map((doc: { path: string }) => ({
slug: doc.path.split('/'), // Convert "api/auth" to ["api", "auth"]
}));
}
// Example return value:
// [
// { slug: ["introduction"] },
// { slug: ["getting-started", "installation"] },
// { slug: ["api", "components", "button"] },
// { slug: ["guides", "deployment", "vercel"] },
// ]
export default async function DocsPage({
params,
}: {
params: { slug: string[] };
}) {
const path = params.slug.join('/');
const doc = await fetch(`https://api.example.com/docs/${path}`)
.then(res => res.json());
return <article>{doc.content}</article>;
}Combining Route Patterns
You can mix catch-all with regular routes and dynamic routes:
app/
blog/
page.tsx ā /blog (listing)
[id]/
page.tsx ā /blog/123 (single post by ID)
category/
[...slug]/
page.tsx ā /blog/category/tech/tutorials
archive/
[[...date]]/
page.tsx ā /blog/archive or /blog/archive/2024/01Route Priority:
- Static routes (exact matches)
- Dynamic routes [id]
- Catch-all routes [...slug]
- Optional catch-all [[...slug]]
Be Careful with Overlaps
If you have both /blog/[id]/page.tsx and /blog/[...slug]/page.tsx, the single dynamic route takes priority. So /blog/123 matches [id], but /blog/tech/tutorials matches [...slug].
Practice: Build a Documentation Site
Let's build a complete documentation site with catch-all routes:
Documentation Site with Optional Catch-All
Try navigating: /docs, /docs/introduction, /docs/api/authentication
Output Preview
šÆ Challenge Exercise
Build these in your Next.js project:
- A file explorer:
app/files/[...path]/page.tsx - A category browser:
app/categories/[...category]/page.tsx - A blog archive:
app/blog/archive/[[...date]]/page.tsx
Best Practices
1. Always Handle the Empty Case
export default function Page({
params
}: {
params: { slug?: string[] }
}) {
const slug = params.slug || [];
// Always have a fallback
if (slug.length === 0) {
return <HomePage />;
}
// Handle nested paths
return <NestedPage slug={slug} />;
}2. Validate Segment Depth
export default function Page({ params }) {
const slug = params.slug || [];
// Limit maximum depth
if (slug.length > 5) {
notFound();
}
// Or require minimum depth
if (slug.length < 2) {
return <div>Please specify a category and subcategory</div>;
}
}3. Sanitize Slugs
export default function Page({ params }) {
const slug = params.slug || [];
// Sanitize each segment
const safeSlugs = slug.map(segment =>
segment.toLowerCase().replace(/[^a-z0-9-]/g, '')
);
// Use sanitized slugs
const path = safeSlugs.join('/');
}4. Provide Clear Navigation
Always show breadcrumbs or a way to navigate up the hierarchy:
// Build parent link
const parentHref = slug.length > 1
? `/docs/${slug.slice(0, -1).join('/')}`
: '/docs';
return (
<div>
<Link href={parentHref}>ā Back</Link>
{/* content */}
</div>
);Quick Reference: Route Pattern Comparison
| Pattern | Matches | Params Type | Use Case |
|---|---|---|---|
[slug] | One segment | string | Single dynamic segment |
[...slug] | One or more segments | string[] | Multi-level paths |
[[...slug]] | Zero or more segments | string[] | undefined | Optional multi-level |
Key Takeaways
- [...slug] matches one or more segments - requires at least one
- [[...slug]] matches zero or more segments - optional, includes parent route
- params.slug is an array - contains all matched segments
- Perfect for documentation, file browsers, categories - any deep nesting
- Use array methods - slice, join, map to work with segments
- Always handle empty case - especially with optional catch-all
- Validate depth and sanitize - prevent security issues
- Provide clear navigation - breadcrumbs and parent links
What's Next?
You've now mastered all types of dynamic routing in Next.js! From single segments with [slug] to unlimited nesting with [...slug]. But there's another powerful organizational feature to learn: route groups.
In the next lesson, we'll explore how to use parentheses (folder) to organize your routes into logical groups without affecting the URL. This is perfect for organizing large applications, applying different layouts to different sections, and keeping your file structure clean!
š You're Becoming a Routing Expert!
Catch-all routes might seem complex at first, but they're incredibly powerful once you understand them. They're the secret behind flexible, scalable routing systems in production apps. Keep practicing!