Lesson 7 of 2226 min read

Dynamic Routing in Next.js ([slug], [...slug], [[...slug]])

Learn how to build dynamic routes in Next.js using dynamic segments, catch-all routes, and optional catch-all routes with real examples.

Author: CodersNexus

Dynamic Routing in Next.js ([slug], [...slug], [[...slug]])

Static and nested routes work well when you know every URL in advance — an About page, a Contact page, a fixed set of category pages. But most real applications have content that's generated dynamically: thousands of blog posts, millions of products, or user profiles that don't exist as individual folders in your codebase. You can't manually create a folder for every product ID in an e-commerce catalog.

Next.js solves this with dynamic routing — special folder naming conventions using square brackets that let a single file handle an entire category of URLs. This lesson covers the three dynamic routing patterns: the basic dynamic segment ([slug]) for one variable URL part, the catch-all segment ([...slug]) for capturing multiple URL parts at once, and the optional catch-all segment ([[...slug]]) which additionally matches the parent route itself.

Learning Objectives

  • Create a basic dynamic route using a single bracketed segment like [slug].
  • Access dynamic URL values inside a page component via the params prop.
  • Build catch-all routes that capture multiple URL segments as an array.
  • Understand how optional catch-all routes differ from regular catch-all routes.
  • Choose the right dynamic routing pattern for a given real-world scenario.

Core Definitions

  • Dynamic segment: A folder named in square brackets, like [slug], that matches any single value at that position in the URL.
  • params: A prop automatically passed to a dynamic page component, containing the actual matched values from the URL as key-value pairs.
  • Catch-all segment: A folder named with square brackets and three dots, like [...slug], that matches any number of URL segments at and beyond that position, captured as an array.
  • Optional catch-all segment: A folder named with double square brackets and three dots, like [[...slug]], that behaves like a catch-all segment but also matches the parent route with zero additional segments.
  • generateStaticParams: A function used with dynamic routes to pre-render a known, fixed set of dynamic pages at build time instead of on every request.

Detailed Explanation

A dynamic segment is created by wrapping a folder name in square brackets. A folder named app/blog/[slug]/page.tsx will match ANY single URL segment in that position — /blog/hello-world, /blog/my-first-post, /blog/anything-at-all — all route to the same page.tsx file. Inside that component, Next.js automatically supplies a params prop containing the actual matched value, so app/blog/[slug]/page.tsx receives params.slug equal to 'hello-world' when visiting /blog/hello-world. You then use that value, typically to fetch the specific blog post's data from a database or API.

A catch-all segment goes further: app/docs/[...slug]/page.tsx matches not just one segment but ANY number of segments after /docs/. Visiting /docs/getting-started matches with params.slug equal to ['getting-started']. Visiting /docs/guides/advanced/setup matches too, with params.slug equal to ['guides', 'advanced', 'setup'] — an array of every segment captured. This is ideal for documentation sites or CMS-driven pages with arbitrarily deep, unpredictable nesting that you don't want to hardcode as individual folders.

However, a plain catch-all segment [...slug] does NOT match the base /docs URL itself with zero additional segments — visiting exactly /docs would 404 unless a separate app/docs/page.tsx exists. This is where the optional catch-all segment, written with double brackets [[...slug]], comes in: it matches everything a regular catch-all matches, PLUS the base route with zero segments, making params.slug undefined in that case. This single file can then handle /docs, /docs/intro, and /docs/guides/setup all at once.

For performance, dynamic routes can be combined with generateStaticParams, a function you export that tells Next.js exactly which dynamic values should be pre-rendered as static HTML at build time (turning what would otherwise be SSR into SSG for known values), while still gracefully handling any values not included in that list.

Diagram Description

Visualize matching behavior for each pattern:

[slug] app/blog/[slug]/page.tsx
/blog/post-1 → matches, params.slug = 'post-1'
/blog/post-1/extra → does NOT match (too many segments)

[...slug] app/docs/[...slug]/page.tsx
/docs/intro → matches, params.slug = ['intro']
/docs/guides/setup → matches, params.slug = ['guides','setup']
/docs → does NOT match (zero segments)

[[...slug]] app/docs/[[...slug]]/page.tsx
/docs → matches, params.slug = undefined
/docs/intro → matches, params.slug = ['intro']
/docs/guides/setup → matches, params.slug = ['guides','setup']

Comparison Table

PatternFolder SyntaxMatchesparams Shape
Dynamic segment[slug]Exactly one URL segment at this positionString, e.g. 'post-1'
Catch-all segment[...slug]One or more segments; NOT zero segmentsArray, e.g. ['guides','setup']
Optional catch-all[[...slug]]Zero or more segments (matches base route too)Array or undefined

Next.js Practical Example

// app/blog/[slug]/page.tsx — basic dynamic segment
export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPostBySlug(slug); // e.g., a DB or CMS lookup
  return <article><h1>{post.title}</h1><p>{post.content}</p></article>;
}

// Optional: pre-render known posts at build time
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

// app/docs/[...slug]/page.tsx — catch-all segment
export default async function DocsPage({
  params,
}: {
  params: Promise<{ slug: string[] }>;
}) {
  const { slug } = await params; // e.g. ['guides', 'setup']
  const path = slug.join('/');
  return <h1>Docs page for: {path}</h1>;
}

BlogPostPage handles every possible blog post URL through a single file, extracting the specific slug from params and using it to fetch that post's real content — this is what makes dynamic routing scale to thousands of posts without thousands of manually created files. generateStaticParams tells Next.js which slugs are known ahead of time so those specific pages can be pre-rendered as static HTML at build time for maximum speed, while any slug not in that list can still be handled (typically rendered on-demand). DocsPage demonstrates the catch-all pattern: no matter how many segments follow /docs/, this one file receives them all as an array in params.slug, letting a documentation site handle arbitrarily deep nested paths without a folder for every possible depth.

Industry Examples

  • E-commerce platforms use app/products/[id]/page.tsx to handle millions of individual product pages through a single dynamic route file.
  • Blogging platforms and CMS-driven sites use app/blog/[slug]/page.tsx so publishing a new post never requires deploying new code — only new data.
  • Documentation sites like those for open-source libraries use catch-all routes (app/docs/[...slug]/page.tsx) to map an arbitrarily nested content structure without hardcoding every folder depth.
  • Social platforms use dynamic segments like app/[username]/page.tsx to give every user a profile page without creating a folder per user.
  • Headless CMS-powered marketing sites often use an optional catch-all route (app/[[...slug]]/page.tsx) at the root to let non-technical content editors create arbitrary page paths entirely from the CMS, with the same Next.js file rendering all of them.

Interview Questions and Answers

Q1. What is a dynamic segment in Next.js and how do you create one?

A dynamic segment is a folder name wrapped in square brackets, such as [slug], that matches any single value at that position in the URL. Creating app/blog/[slug]/page.tsx means any URL like /blog/anything routes to that same file, with the actual matched value available via the params prop.

Q2. How do you access the dynamic value in a Next.js App Router page?

Next.js automatically passes a params prop to dynamic route page components, containing the matched URL segment(s) as key-value pairs matching the bracketed folder name. For example, [slug] produces params.slug with the actual matched string value.

Q3. What is the difference between a catch-all route and a dynamic segment?

A dynamic segment like [slug] matches exactly one URL segment. A catch-all route like [...slug] matches one or more segments at and beyond that position, capturing them as an array in params, making it suitable for arbitrarily deep or nested paths.

Q4. What does an optional catch-all route add compared to a regular catch-all route?

An optional catch-all route, written as [[...slug]], matches everything a regular catch-all route matches, plus the base route itself with zero additional segments — something a regular catch-all route ([...slug]) does not match.

Q5. What is generateStaticParams used for with dynamic routes?

generateStaticParams lets you specify a known list of dynamic values (e.g., all blog post slugs) that Next.js should pre-render as static HTML at build time, combining the performance benefits of static generation with the flexibility of dynamic routing.

MCQs With Answers

1. Which folder syntax creates a basic dynamic route matching exactly one URL segment?

  1. [[slug]]
  2. [slug]
  3. {slug}
  4. [...slug]

Answer: B. [slug]

Explanation: A single pair of square brackets creates a dynamic segment that matches exactly one value at that position in the URL.

2. What does app/docs/[...slug]/page.tsx match?

  1. Only /docs exactly
  2. One or more segments after /docs/
  3. Zero or more segments after /docs/
  4. Exactly two segments after /docs/

Answer: B. One or more segments after /docs/

Explanation: A catch-all route ([...slug]) requires at least one segment after the base path; it does not match the base path alone with zero segments.

3. What additional case does [[...slug]] handle compared to [...slug]?

  1. Matching two segments only
  2. Matching the base route with zero segments
  3. Matching only numeric values
  4. Matching query parameters

Answer: B. Matching the base route with zero segments

Explanation: The optional catch-all route additionally matches the base route itself with no extra segments, which a plain catch-all route does not.

4. In app/products/[id]/page.tsx, what shape does params.id have when visiting /products/42?

  1. An array ['42']
  2. The string '42'
  3. The number 42
  4. undefined

Answer: B. The string '42'

Explanation: A basic dynamic segment provides its matched value as a string (not automatically converted to a number) in the params object.

5. What is the purpose of generateStaticParams?

  1. To validate form input
  2. To pre-render a known set of dynamic pages as static HTML at build time
  3. To style dynamic pages
  4. To create a new database table

Answer: B. To pre-render a known set of dynamic pages as static HTML at build time

Explanation: generateStaticParams lets you specify which dynamic route values should be built as static pages ahead of time, improving performance for known content.

Common Mistakes to Avoid

  • Expecting a plain catch-all route [...slug] to also match the base path with zero segments — that requires the optional catch-all [[...slug]] instead.
  • Forgetting that catch-all route params are always arrays, and trying to use them like a plain string without joining or indexing.
  • Using a basic dynamic segment [slug] when the actual data has variable, unpredictable nesting depth that really calls for a catch-all route.
  • Not awaiting the params prop in recent Next.js versions where params is delivered as a Promise in Server Components.
  • Overusing generateStaticParams for extremely large datasets (millions of pages), which can make builds impractically slow — dynamic (on-demand) rendering is often more appropriate at that scale.

Interview Notes

  • [slug] matches exactly one URL segment; the matched value is a string in params.
  • [...slug] (catch-all) matches one or more segments as an array; it does NOT match the base route with zero segments.
  • [[...slug]] (optional catch-all) matches everything a catch-all matches, plus the base route with zero segments (params.slug is undefined in that case).
  • params is automatically supplied to dynamic route components with keys matching the bracketed folder name(s).
  • generateStaticParams pre-renders known dynamic values as static HTML at build time, blending SSG benefits into dynamic routes.

Key Takeaways

  • Dynamic routing is what allows a single Next.js file to serve potentially unlimited unique URLs driven by real data.
  • Choosing between [slug], [...slug], and [[...slug]] depends entirely on how many URL segments you need to capture and whether the base route itself should also match.
  • The params prop is the bridge between the dynamic URL a user visited and the actual data your page needs to fetch and render.
  • generateStaticParams lets you combine the flexibility of dynamic routes with the performance of static generation for content you know in advance.

Summary

Dynamic routing lets a single Next.js file handle an entire category of URLs instead of requiring a folder per individual page. A dynamic segment, written as [slug], matches exactly one URL segment and delivers its value via the params prop. A catch-all segment, written as [...slug], matches one or more segments as an array, ideal for arbitrarily nested content like documentation. An optional catch-all segment, written as [[...slug]], extends this further by also matching the base route with zero segments. Combined with generateStaticParams, dynamic routes can pre-render known values as static HTML at build time, blending the scalability of dynamic routing with the performance benefits of static generation.

Frequently Asked Questions

Dynamic routing lets a single file in the app/ directory handle many different URLs by using bracketed folder names like [slug], which capture the actual URL value and make it available to the page component via the params prop.

Next.js automatically passes a params prop to dynamic route page components. For a folder named [slug], you'd access the matched value as params.slug (awaiting it first, since params is delivered as a Promise in recent versions).

[slug] matches exactly one URL segment and provides it as a single string. [...slug] is a catch-all route that matches one or more segments and provides them as an array, useful for arbitrarily nested paths.

Use [[...slug]] when you need the same file to also handle the base route with zero additional segments — for example, a single file handling both /docs and /docs/anything/nested/deeply.

Yes, this is a very common and recommended pattern. generateStaticParams lets you list known values (like all blog post slugs) that should be pre-rendered as static HTML at build time, while unlisted values can still be handled dynamically.

Next.js renders a not-found page, either a custom not-found.tsx file if you've defined one or the framework's built-in default 404 page.

No. In a catch-all route ([...slug]), params.slug is always an array of strings, even if only one segment was matched. In a basic dynamic segment ([slug]), params.slug is a single string, not an array.

Yes. For example, app/shop/[category]/[productId]/page.tsx creates a route with two dynamic segments, giving you params.category and params.productId both available in the same page.

Not inherently. Whether a dynamic route hurts or helps SEO depends on the rendering strategy applied to it — a dynamic route rendered via SSR or pre-rendered via generateStaticParams still delivers complete HTML to crawlers, just like a static route would.

A documentation site where content editors can create nested pages of arbitrary depth (like /docs/guides/setup/advanced) is a strong catch-all use case, since a basic [slug] segment could only ever match exactly one level deep.