Skip to content
C Codeloom
Next.js

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.

·4 min read · By Codeloom
Intermediate 9 min read

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
How URL paths map to dynamic route files

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.slug as 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 generateStaticParams and 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.tsx next to app/blog/[slug]/page.tsx is 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.tsx and error.tsx inside 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 params explicitly. 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.