Next.js Dynamic Routes and Catch-All Segments Explained
Master dynamic routes, catch-all segments, and optional catch-alls in Next.js. Learn how the file-based router maps URLs to components with concrete examples.
What you'll learn
- ✓How file names map to URL patterns
- ✓The difference between [slug], [...slug], and [[...slug]]
- ✓Generating static paths at build time
- ✓Handling 404s and fallbacks
- ✓Reading params safely in Server Components
Prerequisites
- •Familiar with HTTP and JS
- •Some Next.js experience
What and Why
Next.js uses a file-system router. The folder layout under app/ is the URL structure. That sounds simple until you need wildcards: a blog where any depth is valid, a docs site where the same template renders dozens of pages, or a catch-all 404 fallback. Dynamic routes and catch-all segments cover all of these without a custom router.
Understanding the rules saves you from sprinkling if statements through your pages and lets you push routing concerns into the file system, where they belong.
Mental Model
There are three kinds of dynamic segments in Next.js:
[slug]matches exactly one path segment. The value arrives as a string.[...slug]matches one or more segments. The value arrives as an array of strings.[[...slug]]matches zero or more segments. It also matches the parent route itself.
The router is greedy in the obvious direction: more specific routes win over more general ones. So app/blog/hello/page.tsx beats app/blog/[slug]/page.tsx, which beats app/blog/[...slug]/page.tsx.
Hands-on Example
Let us build a documentation site where URLs can be one or several segments deep, plus a special landing page at /docs.
URL File
/docs app/docs/[[...slug]]/page.tsx
/docs/getting-started app/docs/[[...slug]]/page.tsx
/docs/api/auth/tokens app/docs/[[...slug]]/page.tsx
/blog/2026/launch app/blog/[...slug]/page.tsx
/users/42 app/users/[id]/page.tsx
/users/42/settings app/users/[id]/settings/page.tsx Here is the docs page using an optional catch-all so the index URL also resolves.
// app/docs/[[...slug]]/page.tsx
import { notFound } from "next/navigation";
import { getDoc, getAllDocPaths } from "@/lib/docs";
export async function generateStaticParams() {
const paths = await getAllDocPaths();
return paths.map((segments) => ({ slug: segments }));
}
type Props = { params: { slug?: string[] } };
export default async function DocPage({ params }: Props) {
const slug = params.slug ?? ["index"];
const doc = await getDoc(slug.join("/"));
if (!doc) notFound();
return (
<article>
<h1>{doc.title}</h1>
<div dangerouslySetInnerHTML={{ __html: doc.html }} />
</article>
);
}
A few things to notice. generateStaticParams returns one entry per page Next.js should pre-render at build time. The slug param is optional thanks to the double brackets, so we coalesce to "index" when undefined. Unknown slugs call notFound, which renders the nearest not-found.tsx boundary.
For a blog requiring at least one segment, we use a regular catch-all:
// app/blog/[...slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string[] } }) {
const [year, ...rest] = params.slug;
return <h1>{year} — {rest.join("/")}</h1>;
}
If a user visits /blog, this route does not match. You would need an explicit app/blog/page.tsx to handle the index.
Common Pitfalls
- Treating
[...slug]as equivalent to[[...slug]]. The first does not match the empty path; the second does. - Reading
params.slugas a string when it is actually a string array. TypeScript will warn you if you type it. - Forgetting to call
notFound()for unknown content. Without it, you serve a half-rendered page with broken data. - Building too many pages with
generateStaticParamsand timing out CI. For large content sets, use ISR or dynamic rendering. - Putting authentication checks in dynamic routes only. Static catch-alls bypass auth at build time, so add a runtime guard in middleware.
- Conflicting routes: a static
app/blog/featured/page.tsxnext toapp/blog/[slug]/page.tsxis fine, but two dynamic siblings at the same level will throw.
Practical Tips
- Use the optional catch-all for routes where the parent URL is also a real page, like docs or marketplaces.
- Co-locate
loading.tsxanderror.tsxinside the dynamic segment folder so they cover every match. - Generate sitemaps from the same source you pass to
generateStaticParams. One source of truth prevents drift. - For previewable content, combine dynamic routes with draft mode so editors can see unpublished content via the same template.
- Keep slug normalization (case, trailing slashes) in middleware. That way your route code only sees canonical URLs.
- Type
paramsexplicitly. The Next.js types are permissive, and a small generic saves you bugs.
Wrap-up
Dynamic and catch-all segments make Next.js routing expressive without forcing you to write a router. Once you know the three flavors and the specificity rules, almost any URL shape becomes a folder layout. Combine them with generateStaticParams, notFound, and middleware, and you can serve everything from a marketing landing page to a sprawling documentation site with one consistent pattern.
Related articles
- Next.js Next.js Data Fetching Patterns: A Practical Guide
Compare the main Next.js data fetching strategies and learn when to use server components, route handlers, SWR, or static generation in your apps.
- Next.js Next.js Image Optimization Deep Dive
Learn how the next/image component handles responsive sizing, lazy loading, format conversion, and caching to ship faster sites.
- Next.js Next.js Internationalization: A Hands-on Tutorial
Add multi-language support to a Next.js App Router project with locale routing, message catalogs, and SEO-friendly URLs that scale to dozens of languages.
- Next.js Parallel and Intercepting Routes in Next.js
How parallel slots and intercepting routes power dashboards, modals, and tabbed UIs in the Next.js App Router — the file conventions, when to reach for each, and the patterns that hold up in production.