Dynamic Routes
Generating routes from data with bracketed folder names, and why params arrives as a Promise.
阅读需 2 分钟
Most real apps can't have a hand-written folder for every URL — a blog with a thousand posts needs one route definition that handles all of them. Next.js does this with dynamic segments: wrap a folder name in square brackets, and it becomes a placeholder that matches any value in that position of the URL.
The [slug] convention
app/blog/[slug]/page.tsxThis single file handles /blog/hello-world, /blog/second-post, /blog/anything — whatever the visitor requests. The captured value is delivered to your component through the params prop:
// app/blog/[slug]/page.tsx
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPostBySlug(slug);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}Why params is a Promise
This trips up almost everyone coming from an older Next.js tutorial: params isn't a plain object, it's a Promise you have to await. That's intentional — Next.js needs the flexibility to resolve route params asynchronously (for example, while it's still figuring out which parts of a page can be served from a static cache versus rendered per-request), and making the type a Promise everywhere keeps that behavior consistent whether or not a given route actually needs to wait. The folder name in the URL matches the key in the object you get back — [slug] gives you { slug: string }, [id] gives you { id: string }.
Multiple dynamic segments
You can nest more than one dynamic segment in a path, and each becomes its own key:
// app/shop/[category]/[itemId]/page.tsx
export default async function ItemPage({
params,
}: {
params: Promise<{ category: string; itemId: string }>;
}) {
const { category, itemId } = await params;
return <p>{category} / {itemId}</p>;
}Catch-all segments
Prefixing the folder name with ... captures every remaining segment as an array instead of matching exactly one:
app/docs/[...slug]/page.tsx/docs/a→{ slug: ['a'] }/docs/a/b/c→{ slug: ['a', 'b', 'c'] }
Wrapping it in a second pair of brackets — [[...slug]] — makes it optional, so the route also matches /docs itself with slug as undefined. This is the pattern behind documentation sites and CMS-driven page trees, where the same template renders arbitrarily deep URLs.
Validating params you don't control
Anyone can type any string into the address bar, so a dynamic segment's value is never guaranteed to correspond to real data. Always handle the "not found" case explicitly rather than letting a lookup fail silently:
import { notFound } from "next/navigation";
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) {
notFound(); // renders the nearest not-found.tsx
}
return <article>{/* ... */}</article>;
}Dynamic segments are also what generateStaticParams (covered later in this course) uses to decide which specific URLs to pre-render at build time, rather than rendering every request from scratch.