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
| Pattern | Folder Syntax | Matches | params Shape |
|---|---|---|---|
| Dynamic segment | [slug] | Exactly one URL segment at this position | String, e.g. 'post-1' |
| Catch-all segment | [...slug] | One or more segments; NOT zero segments | Array, 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?
- [[slug]]
- [slug]
- {slug}
- [...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?
- Only /docs exactly
- One or more segments after /docs/
- Zero or more segments after /docs/
- 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]?
- Matching two segments only
- Matching the base route with zero segments
- Matching only numeric values
- 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?
- An array ['42']
- The string '42'
- The number 42
- 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?
- To validate form input
- To pre-render a known set of dynamic pages as static HTML at build time
- To style dynamic pages
- 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.