Next.js Dynamic Routes
Create dynamic pages with brackets, access route params, and generate static paths with generateStaticParams.
TL;DR
- 01Wrap folder names in brackets to create dynamic segments.
- 02Access params through the params prop passed to pages.
- 03Use generateStaticParams to pre-build dynamic pages at compile time.
Tips
- 01Use <code>generateStaticParams()</code> for high-traffic pages like blog posts to pre-build them at deploy time for instant page loads.
- 02Combine <code>generateStaticParams()</code> with a <code>revalidate</code> export so high-traffic dynamic pages stay pre-built while still picking up fresh content automatically.
Warnings
- 01If generateStaticParams doesn't include a route, it will be generated on first request which causes a slow cold start on serverless.
- 02The object keys returned by <code>generateStaticParams()</code> must exactly match the dynamic segment names in the folder, or Next.js throws a build error: "generateStaticParams returned invalid params."
Basic Dynamic Routes
- One folder name decides whether a route is static or matches infinite URLs — wrap it in square brackets to make it dynamic.
app/ posts/ [id]/ page.tsx - This matches
/posts/1,/posts/2, or any value for theidsegment. - Access the dynamic parameter through the
paramsprop, which is now an asyncPromiseyou must await.export default async function Post({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; return <h1>Post {id}</h1>; } - Each dynamic segment creates a separate route that can load different data.
- URL parameters are always strings, so parse them as needed.
Accessing Route Parameters
- Nest two dynamic segments and the
paramsobject resolves both at once, once you await it.// Route: app/users/[userId]/posts/[postId]/page.tsx export default async function PostPage({ params }: { params: Promise<{ userId: string; postId: string }> }) { const { userId, postId } = await params; return <div>User {userId}, Post {postId}</div>; } - Fetch data using the resolved params to load content specific to that route.
const { id } = await params; const post = await getPost(id); - Pass params to layout components if they need this information.
- Nested dynamic segments work just like single-level ones.
Catch-All Routes
- Need one file to match
/docs/a,/docs/a/b, and/docs/a/b/calike? Catch-all segments do exactly that with[...slug].app/ docs/ [...slug]/ page.tsx - This matches
/docs/a,/docs/a/b,/docs/a/b/c, etc. - The
slugparam is always an array of path segments, available after awaitingparams.export default async function Docs({ params }: { params: Promise<{ slug: string[] }> }) { const { slug } = await params; const path = slug.join("/"); return <h1>{path}</h1>; } - Use catch-all routes for nested documentation or content sites.
- Combine with optional catch-all
[[...slug]]for breadcrumb navigation.
Optional Catch-All Routes
- What if one page needs to handle both
/blogand/blog/2025/january/post? Double the brackets to[[...slug]]and the catch-all becomes optional.app/ blog/ [[...slug]]/ page.tsx - This matches
/blog,/blog/post-1,/blog/2025/january/post, etc. - The slug param is an array only if segments exist, otherwise undefined — await
paramsfirst to read it.const { slug } = await params; const segments = slug ?? []; const depth = segments.length; - Use optional catch-all when a single page handles multiple URL patterns.
- Great for flexible navigation menus and hierarchical content.
Static Generation with Dynamic Routes
- Skip the per-request server round trip entirely — export
generateStaticParams()to pre-build specific dynamic pages at deploy time.export async function generateStaticParams() { const posts = await getPosts(); return posts.map((post) => ({ id: post.id.toString(), })); } export default async function Post({ params }: { params: Promise<{ id: string }> }) { const { id } = await params; // This page is pre-built for every post at build time return <h1>Post {id}</h1>; } - Pre-built pages load instantly with no server delay.
- Pages not in generateStaticParams are generated on-demand the first time.
- Use
revalidatefor incremental static regeneration to update stale pages.export const revalidate = 3600; // Rebuild every hour - Great for blogs, product pages, and public documentation.
FAQ
Create a folder with the segment name wrapped in square brackets, like app/blog/[slug]/page.tsx. The bracket syntax tells Next.js that this segment is dynamic and will match any value at that URL position.
Dynamic page components receive a params prop, which is an async Promise containing your segment names as keys. For a route like [slug], type it as async function Page({ params }: { params: Promise<{ slug: string }> }), then run const { slug } = await params before using it.
The [...slug] (catch-all) requires at least one segment and returns 404 for the base path, while [[...slug]] (optional catch-all) also matches the parent route with no segments. Use optional catch-all when you want one component to handle both /docs and /docs/getting-started.
Yes, you can nest multiple dynamic folders, such as app/shop/[category]/[productId]/page.tsx. Both category and productId will be available on the params object simultaneously.
By default, Next.js falls back to server-rendering the missing page on first request. You can change this behavior by exporting export const dynamicParams = false from the page, which will make unlisted routes return a 404 instead.