Dynamic Routes
Use bracket syntax to create routes that match dynamic URL segments - single params, catch-all segments, and optional catch-all patterns.
Search across all documentation pages
Use bracket syntax to create routes that match dynamic URL segments - single params, catch-all segments, and optional catch-all patterns.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Quick-reference recipe card - copy-paste ready.
app/
├── blog/[slug]/page.tsx # /blog/hello-world → { slug: "hello-world" }
├── docs/[...path]/page.tsx # /docs/a/b/c → { path: ["a", "b", "c"] }
└── shop/[[...categories]]/page.tsx # /shop or /shop/a/b → { categories: ["a", "b"] } or {}
// app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>Post: {slug}</h1>;
}When to reach for this: Any route where the URL contains a variable - product IDs, usernames, documentation paths, or locale prefixes.
// app/blog/[slug]/page.tsx - Single dynamic segment
import { notFound } from "next/navigation";
interface Post {
slug: string;
title: string;
content: string;
}
async function getPost(slug: string): Promise<Post | null> {
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 BlogPostPage({
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>
);
}
// Static generation for known slugs
export async function generateStaticParams() {
const posts: Post[] = await fetch("https://api.example.com/posts").then((r) =>
r.json()
);
return posts.map((post) => ({ slug: post.slug }));
}// app/docs/[...path]/page.tsx - Catch-all route
export default async function DocsPage({
params,
}: {
params: Promise<{ path: string[] }>;
}) {
const { path } = await params;
// /docs/getting-started/install → path = ["getting-started", "install"]
const fullPath = path.join("/");
return (
<div>
<h1>Docs: {fullPath}</h1>
<p>Segments: {path.length}</p>
</div>
);
}// app/shop/[[...categories]]/page.tsx - Optional catch-all
export default async function ShopPage({
params,
}: {
params: Promise<{ categories?: string[] }>;
}) {
const { categories } = await params;
if (!categories || categories.length === 0) {
return <h1>All Products</h1>;
}
return (
<div>
<h1>Shop: {categories.join(" > ")}</h1>
<p>Filtering by {categories.length} categories</p>
</div>
);
}[slug] matches a single segment. /blog/hello matches, /blog/hello/comments does not.[...path] matches one or more segments. /docs/a matches, /docs/a/b/c matches, but /docs alone does NOT match.[[...path]] matches zero or more segments. Same as catch-all, but also matches the base path (/shop with no segments).Promise in Next.js 15+. You must await params in Server Components or use(params) in Client Components.generateStaticParams pre-renders dynamic routes at build time. Return an array of param objects, and Next.js generates a static page for each.app/[lang]/blog/[slug]/page.tsx produces params: { lang, slug }.not-found.tsx when no page matches, or when you call notFound().// Multiple dynamic segments
// app/[locale]/blog/[slug]/page.tsx
export default async function LocalizedPost({
params,
}: {
params: Promise<{ locale: string; slug: string }>;
}) {
const { locale, slug } = await params;
return <h1>{locale}: {slug}</h1>;
}
export async function generateStaticParams() {
return [
{ locale: "en", slug: "hello" },
{ locale: "fr", slug: "bonjour" },
];
}// generateStaticParams with parent params
// app/[category]/[product]/page.tsx
export async function generateStaticParams({
params,
}: {
params: { category: string };
}) {
const products = await getProductsByCategory(params.category);
return products.map((p) => ({ product: p.slug }));
}// Dynamic route with generateMetadata
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post?.title ?? "Not Found",
description: post?.content?.slice(0, 160),
};
}// Param types by pattern
type SingleParam = { slug: string }; // [slug]
type CatchAllParam = { path: string[] }; // [...path]
type OptionalCatchAll = { categories?: string[] }; // [[...categories]]
type MultiParam = { locale: string; slug: string }; // [locale]/[slug]
// All wrapped in Promise for Next.js 15+ pages
type PageProps = {
params: Promise<SingleParam>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};
// generateStaticParams return type
// Returns param objects WITHOUT Promise wrapper
type StaticParams = SingleParam[];[...path] does NOT match the base route. /docs will 404 unless you have app/docs/page.tsx separately. Use [[...path]] if you need the base./product/42 gives { id: "42" }, not a number. Parse manually.generateStaticParams runs at build time. If your data source is unavailable during build, the build fails. Use dynamicParams = true (the default) to allow on-demand rendering./blog/about matches app/blog/about/page.tsx before app/blog/[slug]/page.tsx.{ params: { slug } } directly in the function signature no longer works in Next.js 15+. Always await.dynamicParams = false returns 404 for unlisted params. Only paths returned by generateStaticParams will work.// Opt out of on-demand dynamic rendering
export const dynamicParams = false;| Approach | When to Use |
|---|---|
Static route (app/about/page.tsx) | URL is fixed and known ahead of time |
Route Groups (group) | Organize without adding URL segments |
| Middleware rewrites | Map custom URLs to existing dynamic routes |
searchParams instead of path params | Filtering or sorting that does not need unique URLs |
[slug] matches exactly one URL segment (e.g., /blog/hello)[...path] matches one or more segments (e.g., /docs/a/b/c) but not the base route[[...path]] matches zero or more segments, including the base route (e.g., /shop or /shop/a/b)Catch-all [...path] requires at least one segment. /docs alone does not match. Either create a separate app/docs/page.tsx for the base route or switch to optional catch-all [[...path]].
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}You must await params because they are now a Promise.
dynamicParams = falseYes. Even if the URL contains a number like /product/42, the param value is the string "42". You must parse it manually (e.g., Number(id) or parseInt(id)).
Static routes take priority. /blog/about matches app/blog/about/page.tsx before app/blog/[slug]/page.tsx.
Only paths returned by generateStaticParams will work. Any other dynamic path returns a 404.
export const dynamicParams = false;import type { Metadata } from "next";
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return { title: post?.title ?? "Not Found" };
}type SingleParam = { slug: string };
type CatchAllParam = { path: string[] };
type OptionalCatchAll = { categories?: string[] };
type MultiParam = { locale: string; slug: string };
// All wrapped in Promise for page props
type PageProps = {
params: Promise<SingleParam>;
};Not in Next.js 15+. The synchronous destructuring pattern { params: { slug } } no longer works because params is now a Promise. Always await params inside the function body.
// app/[category]/[product]/page.tsx
export async function generateStaticParams({
params,
}: {
params: { category: string };
}) {
const products = await getProductsByCategory(params.category);
return products.map((p) => ({ product: p.slug }));
}The parent segment's params are passed into the child's generateStaticParams.
Call notFound() from next/navigation to trigger the nearest not-found.tsx.
import { notFound } from "next/navigation";
const post = await getPost(slug);
if (!post) notFound();Reviewed by Chris St. John·Last updated Jul 10, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥