Most routes in a web application are not known in advance. A blog does not have a fixed number of posts. A product catalogue does not have a fixed number of products. A user profile page does not have a fixed number of users. The URLs for these resources follow a pattern — /blog/[slug], /products/[id], /users/[username] — but the actual values that fill those patterns are determined by data, not by the developer at build time.
Dynamic routes are how Next.js handles this reality. Rather than requiring a separate file for every possible URL, a single file with a placeholder segment in its path handles the entire pattern. The placeholder captures whatever appears in the URL at that position and makes it available to the page as a parameter.
Defining a Dynamic Route
A dynamic route segment is created by wrapping a folder name in square brackets. The name inside the brackets becomes the parameter name — the key used to access the captured value.
app/
└── blog/
└── [slug]/
└── page.tsx → /blog/anything-here
Any URL matching /blog/* renders the component at app/blog/[slug]/page.tsx. The value at the * position is accessible in the component through the params prop, under the key slug.
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>Post: {slug}</h1>;
}
Visiting /blog/introduction-to-nodejs renders a page with the heading Post: introduction-to-nodejs. Visiting /blog/understanding-react renders Post: understanding-react. The same component handles both — and every other slug — with the actual value arriving through params.
Fetching Data for a Dynamic Route
The real power of dynamic routes is combining the captured parameter with a data fetch. The slug, ID, or username captured from the URL is used to fetch the specific record that the page needs.
// app/blog/[slug]/page.tsx
type Post = {
slug: string;
title: string;
content: string;
};
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post: Post = await fetch(
`https://api.example.com/posts/${slug}`
).then(r => r.json());
if (!post) {
notFound();
}
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
This is the complete pattern for a data-driven dynamic page: capture the parameter, fetch the corresponding resource, handle the case where it does not exist, render the result. It is clean, direct, and runs entirely on the server.
Dynamic Metadata
A dynamic route typically needs dynamic metadata — the page title should reflect the specific post or product being viewed, not a generic heading. The generateMetadata function, covered in the Pages and Layouts topic, accepts the same params as the page and returns a metadata object populated with real data.
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await fetch(
`https://api.example.com/posts/${slug}`
).then(r => r.json());
if (!post) return { title: 'Post Not Found' };
return {
title: post.title,
description: post.excerpt,
};
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await fetch(
`https://api.example.com/posts/${slug}`
).then(r => r.json());
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
Next.js deduplicates the fetch requests — despite the same URL being fetched in both generateMetadata and the page component, the network request happens only once. The result is cached and shared between both functions during the same render.
Generating Static Pages from Dynamic Routes — generateStaticParams
By default, dynamic route pages are rendered on the server for each incoming request — the URL arrives, the parameter is captured, the data is fetched, the page is rendered. This is correct for content that changes frequently or that depends on the specific user making the request.
For content that is stable — blog posts, documentation pages, product listings — a better approach is to pre-render the pages at build time. generateStaticParams is the function that makes this possible. It runs at build time, returns an array of parameter values, and tells Next.js which pages to pre-render before any user ever visits them.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map((post: { slug: string }) => ({
slug: post.slug,
}));
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await fetch(
`https://api.example.com/posts/${slug}`
).then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
At build time, Next.js calls generateStaticParams, retrieves all post slugs, and pre-renders a static HTML file for each one. When a user visits /blog/introduction-to-nodejs, Next.js serves the pre-built HTML immediately — no server rendering, no data fetching at request time. The response is fast, the page is fully formed, and the server does minimal work.
Handling New Content After Build
When new content is added after the application has been built and deployed, the pre-rendered pages do not include it yet. Next.js handles this with the dynamicParams option:
// app/blog/[slug]/page.tsx
export const dynamicParams = true; // default — server-renders unknown slugs
// export const dynamicParams = false; // returns 404 for unknown slugs
With dynamicParams = true (the default), any slug not included in generateStaticParams is server-rendered on demand when first requested and optionally cached. With dynamicParams = false, an unknown slug returns a 404. The right choice depends on whether the application should handle new content gracefully or treat unknown URLs as definitively absent.
Multiple Dynamic Segments
A route can have more than one dynamic segment. Each segment is captured independently.
app/
└── shop/
└── [category]/
└── [product]/
└── page.tsx → /shop/electronics/headphones
→ /shop/clothing/running-shoes
// app/shop/[category]/[product]/page.tsx
export default async function ProductPage({
params,
}: {
params: Promise<{ category: string; product: string }>;
}) {
const { category, product } = await params;
return (
<h1>
{product} in {category}
</h1>
);
}
params contains both category and product, each populated from the corresponding position in the URL.
Catch-All Segments
A standard dynamic segment captures exactly one URL segment. A catch-all segment, created with three dots before the parameter name ([...slug]), captures all remaining URL segments into an array.
app/
└── docs/
└── [...slug]/
└── page.tsx → /docs/getting-started
→ /docs/api/reference
→ /docs/api/reference/hooks
// app/docs/[...slug]/page.tsx
export default async function DocsPage({
params,
}: {
params: Promise<{ slug: string[] }>;
}) {
const { slug } = await params;
// /docs/api/reference/hooks → slug = ['api', 'reference', 'hooks']
return <h1>Docs: {slug.join(' / ')}</h1>;
}
Catch-all segments are useful for documentation sites, content management systems, or any application where the depth of the URL hierarchy is variable and determined by content structure rather than fixed application routes.
Optional Catch-All Segments
Wrapping the catch-all in double brackets — [[...slug]] — makes the segment optional. The route matches both the base path and any sub-paths beneath it.
app/
└── docs/
└── [[...slug]]/
└── page.tsx → /docs
→ /docs/getting-started
→ /docs/api/reference
When the base path is matched — /docs with no further segments — params.slug is undefined. The page component handles both cases.
// app/docs/[[...slug]]/page.tsx
export default async function DocsPage({
params,
}: {
params: Promise<{ slug?: string[] }>;
}) {
const { slug } = await params;
if (!slug) {
return <h1>Documentation Home</h1>;
}
return <h1>Docs: {slug.join(' / ')}</h1>;
}
Query Parameters
Dynamic route segments capture values from the URL path. Query parameters — the key-value pairs after the ? in a URL — are separate and accessed differently. In a Server Component, they arrive through the searchParams prop:
// app/search/page.tsx
// URL: /search?q=nodejs&page=2
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; page?: string }>;
}) {
const { q, page } = await searchParams;
return (
<div>
<p>Query: {q}</p>
<p>Page: {page ?? '1'}</p>
</div>
);
}
searchParams is available on page.tsx files only — not on layouts, since a layout wraps multiple routes that may have different query strings, and associating a single query string with a layout would be ambiguous.
Summary
| Pattern | Folder Name | Matches | params type |
|---|---|---|---|
| Single dynamic segment | [slug] | /blog/post-one | { slug: string } |
| Multiple segments | [category]/[id] | /shop/books/42 | { category: string; id: string } |
| Catch-all | [...slug] | /docs/a/b/c | { slug: string[] } |
| Optional catch-all | [[...slug]] | /docs or /docs/a/b | { slug?: string[] } |
| Feature | Purpose |
|---|---|
params prop | Provides captured URL segments to the page |
searchParams prop | Provides query string parameters to the page |
generateStaticParams | Pre-renders dynamic pages at build time |
dynamicParams | Controls behaviour for slugs not covered by generateStaticParams |
generateMetadata | Produces dynamic page titles and metadata per route |