Suggest an editImprove this articleRefine the answer for “Dynamic routes and dynamic segments in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Dynamic routes in Next.js** use bracket notation in file names to match variable URL segments. One file handles unlimited URLs via `params`. ```tsx // app/blog/[slug]/page.tsx export default async function BlogPost({ params }) { const { slug } = await params; // /blog/hello → slug = "hello" return <h1>{slug}</h1>; } ``` **Key:** `params` is a Promise in App Router. Always `await` it.Shown above the full answer for quick recall.Answer (EN)Image**Dynamic routes in Next.js** use bracket notation in file names to match variable URL segments, capturing them as `params` for rendering pages without creating a separate file per path. ## Theory ### TL;DR - File `app/blog/[slug]/page.tsx` handles `/blog/anything` from one file - The URL segment maps to `params` as a string: `/blog/hello` gives `slug = "hello"` - `[...slug]` catches one or more segments as an array; `[[...slug]]` catches zero or more - Always `await params` in App Router (Next.js 13+) - it is a Promise, not a plain object - Combine with `generateStaticParams` to pre-build known paths at build time ### Quick example ```tsx // app/blog/[slug]/page.tsx export default async function BlogPost({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; // /blog/hello-world → slug = "hello-world" // /blog/my-post → slug = "my-post" return <h1>Post: {slug}</h1>; } ``` One file. Unlimited URLs. That is the whole point. ### When to use - **Fixed known paths** (`/about`, `/contact`) - static files are faster to build and cache better - **User-generated content** (post slugs, product handles) - use `[slug]`, it scales to any number of items - **Nested unknown paths** (docs trees, category hierarchies) - use `[...slug]` to capture everything as an array - **Optional segments** where `/shop` and `/shop/clothes/men` both need to work - use `[[...slug]]` - **Paginated lists** - `[page]/page.tsx` gives you `/posts/1`, `/posts/2` from one file ### Segment patterns Three patterns cover every case you will run into. **`[slug]` - single segment** Matches exactly one URL part. `/blog/hello` works. `/blog/hello/world` returns 404. ``` app/blog/[slug]/page.tsx → /blog/hello-world ✓ → /blog/hello/world ✗ (404) ``` **`[...slug]` - catch-all** Matches one or more segments, gives you an array. Zero segments (just `/docs`) returns 404. ``` app/docs/[...slug]/page.tsx → /docs/react ✓ slug = ["react"] → /docs/react/hooks ✓ slug = ["react", "hooks"] → /docs ✗ (404) ``` **`[[...slug]]` - optional catch-all** Same as catch-all, but also matches zero segments. The root path works too. ``` app/shop/[[...categories]]/page.tsx → /shop ✓ categories = undefined → /shop/clothes ✓ categories = ["clothes"] → /shop/clothes/men ✓ categories = ["clothes", "men"] ``` ### Multiple dynamic segments You can nest bracket folders. Each one adds a key to `params`. ``` app/[locale]/blog/[slug]/page.tsx → /en/blog/hello, /ua/blog/hello ``` ```tsx export default async function BlogPost({ params, }: { params: Promise<{ locale: string; slug: string }>; }) { const { locale, slug } = await params; // /en/blog/hello → { locale: "en", slug: "hello" } const post = await getPost(slug, locale); return <article>{post.title}</article>; } ``` ### generateStaticParams By default, dynamic routes render on demand at request time. If you know the slugs ahead of time, export `generateStaticParams` to pre-build those pages at build time. Unknown slugs still fall through to server rendering. ```tsx // app/blog/[slug]/page.tsx export async function generateStaticParams() { const posts = await getAllPosts(); return posts.map((post) => ({ slug: post.slug, })); } // Builds /blog/post-1, /blog/post-2 at build time // New slugs still work via server rendering ``` This is what `getStaticPaths` did in the Pages Router, just with a cleaner API. ### How Next.js resolves dynamic routes The file-based router scans `app/` at build or dev start, mapping bracketed folders to regex patterns internally. `[slug]` becomes `/([^/]+)`, `[...slug]` becomes `/(.*+)`. On each request, the router matches the URL, extracts captures into a `params` Promise, and passes it to your `page.tsx` as a Server Component prop. One detail worth knowing: if you have both `app/blog/featured/page.tsx` and `app/blog/[slug]/page.tsx`, the static route wins. Next.js always prefers exact matches over dynamic ones. ### Common mistakes **1. Forgetting `await params`** `params` is a Promise in App Router. Destructuring it without `await` gives you `undefined` on every property. ```tsx // Wrong const { slug } = params; // undefined // Correct const { slug } = await params; ``` This is the most reported issue after upgrading from Next.js 12. **2. Using `useRouter()` from `next/router` in App Router** ```tsx // Wrong - Pages Router pattern import { useRouter } from 'next/router'; const { slug } = useRouter().query; // Correct - App Router const { slug } = await params; // in a Server Component ``` **3. Static export with dynamic routes** Setting `output: 'export'` in `next.config.js` breaks all dynamic routes that lack `generateStaticParams`. Next.js cannot generate HTML files for paths it does not know at build time. **4. Assuming params are numbers** All `params` values come in as strings. Even `/users/42` gives `id = "42"`, not `42`. ```tsx // Wrong const id = params.id + 1; // "421" (string concatenation) // Correct const id = Number(await params.id); ``` **5. Catch-all without handling the empty case** `[[...slug]]` produces `categories = undefined` on the root path. If your component calls `categories.length`, it crashes. ```tsx const { categories = [] } = await params; // default to empty array ``` ### Real-world usage - Vercel Commerce template: `app/products/[handle]/page.tsx` for Shopify product slugs - Nextra docs: `[[...slug]]/page.tsx` for MDX documentation trees - Supabase Starter: `app/users/[id]/page.tsx` for user profile pages - T3 Stack apps: `app/posts/[id]/page.tsx` with tRPC queries behind it ### Follow-up questions **Q:** What file structure handles `/blog/2024/01/post-1`? **A:** `app/blog/[year]/[month]/[slug]/page.tsx`. The `params` object gives `{ year: "2024", month: "01", slug: "post-1" }`. All values are strings, even the numeric-looking ones. **Q:** What is the difference between `[...slug]` and `[[...slug]]`? **A:** `[...slug]` requires at least one segment, so hitting `/docs` with only the catch-all file returns 404. `[[...slug]]` also matches zero segments, so `/docs` works and gives `slug = undefined`. **Q:** How do you return a 404 when a dynamic slug has no matching data? **A:** Add `not-found.tsx` next to your `page.tsx`, then call `notFound()` from `next/navigation` inside the page when your data fetch returns nothing. Next.js renders the not-found UI automatically. **Q:** Can `generateStaticParams` and server rendering work together on the same route? **A:** Yes. Paths you return from `generateStaticParams` get built at compile time. Any slug outside that list still gets server-rendered on the first request. Set `export const dynamicParams = false` to explicitly 404 unknown slugs instead. **Q:** (Senior) How would you handle caching for a dynamic route that fetches from a CMS? **A:** Use `fetch` with `next: { revalidate: 3600 }` for ISR - the cache refreshes every hour. For on-demand purging, tag fetches with `next: { tags: ['posts'] }` and call `revalidateTag('posts')` from a Server Action or Route Handler when content changes in the CMS. That way you get fresh data in seconds, not after the next scheduled revalidation. ## Examples ### Basic: Blog post page ```tsx // app/blog/[slug]/page.tsx import { notFound } from 'next/navigation'; async function getPost(slug: string) { const res = await fetch(`https://api.example.com/posts/${slug}`, { next: { revalidate: 3600 }, }); if (!res.ok) return null; return res.json(); } export default async function BlogPost({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; const post = await getPost(slug); if (!post) notFound(); return ( <article> <h1>{post.title}</h1> <div dangerouslySetInnerHTML={{ __html: post.content }} /> </article> ); } // /blog/nextjs-dynamic-routes → fetches and renders that specific post ``` The `notFound()` call is what turns a missing slug into an actual 404 page instead of a blank render. ### Intermediate: Optional catch-all for a docs site ```tsx // app/docs/[[...slug]]/page.tsx export default async function Docs({ params, }: { params: Promise<{ slug?: string[] }>; }) { const { slug = [] } = await params; // /docs → slug = [] (docs home) // /docs/nextjs → slug = ["nextjs"] // /docs/nextjs/app-router → slug = ["nextjs", "app-router"] const content = await getDocContent(slug); return ( <div> <h1>{slug.length ? slug.join(' / ') : 'Documentation'}</h1> <div>{content}</div> </div> ); } ``` The `= []` default on the destructuring matters. Without it, `slug` is `undefined` at `/docs` and `slug.length` throws immediately. ### Advanced: Static generation with dynamic fallback ```tsx // app/blog/[slug]/page.tsx export async function generateStaticParams() { const posts = await getAllPosts(); // Pre-build only the 20 most recent posts at build time return posts.slice(0, 20).map((post) => ({ slug: post.slug })); } // Older posts still render via the server on first request export const dynamicParams = true; // default behavior export default async function BlogPost({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; const post = await getPost(slug); if (!post) notFound(); return <article><h1>{post.title}</h1></article>; } ``` Pre-building the most-visited pages at build time while still serving everything else dynamically is a pattern I have used on several content-heavy Next.js projects. Build times stay short and nothing returns 404 unexpectedly.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.