In enterprise-scale web applications, the majority of page URLs cannot be statically hardcoded at build time. E-commerce catalogs, editorial blogs, API reference documentation, and user profiles operate across hundreds of thousands of distinct entities. Generating physical files for every distinct URL is technically impossible and unmaintainable.
The Next.js App Router resolves this challenge through parameterized filesystem segments — Dynamic Routes. Instead of fixed directory names, developers declare parameterized templates wrapped in brackets. The Turbopack compiler compiles this structure into dynamic nodes within a prefix tree (Radix Tree), extracting values from the URL and passing them type-safely into page components, layouts, or generateMetadata functions.
In the App Router, a Dynamic Segment is created by wrapping a folder name in square brackets. For example, app/products/[id] matches routes such as /products/101 or /products/macbook-pro, assigning the segment value to the key id.
[param]): Captures strictly a single URL segment between two slashes (/blog/nextjs).[...param]): Captures arbitrary nested segments as an array of strings. Accessing the base /docs path without parameters returns a 404.[[...param]]): Matches nested subpaths as well as the bare root route itself (where the parameter resolves to undefined).When an incoming URL matches multiple candidate folder structures, Turbopack resolves collisions deterministically according to strict Route Precedence: Static routes > Dynamic segments [id] > Catch-all [...slug] > Optional Catch-all [[...slug]].
Turbopack splits the incoming HTTP URI into segment tokens by slash delimiter: /docs/api/v1 -> ['docs', 'api', 'v1'].
The engine traverses the Radix prefix tree, evaluating static segments first, followed by single [param] nodes, and finally greedy [...slug] patterns.
Extracted segment tokens are packaged into a Promise<params> to enable non-blocking static shell streaming (PPR).
The server initiates concurrent execution of page.tsx and generateMetadata. Invoking await params unlocks path variables.
The RSC Flight Payload streams to the client browser. Client components unwrap parameters via the React 19 use(paramsPromise) hook.
Why is the params property in page.tsx defined as a Promise<{ slug: string }> rather than a synchronous object in Next.js 15+ and React 19?
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
interface BlogPostPageProps {
params: Promise<{ slug: string }>;
searchParams: Promise<{ ref?: string }>;
}
export async function generateMetadata(props: BlogPostPageProps): Promise<Metadata> {
const { slug } = await props.params;
return {
title: `Article: ${slug}`,
description: `Deep-dive architectural breakdown of ${slug}`,
};
}
export default async function BlogPostPage(props: BlogPostPageProps) {
const { slug } = await props.params;
const { ref } = await props.searchParams;
if (slug === 'forbidden-topic') {
notFound();
}
return (
<article className="p-6">
<h1 className="text-2xl font-bold">Publication: {slug}</h1>
{ref && <p className="text-xs text-text-muted">Referral Source: {ref}</p>}
</article>
);
}When a dynamic parameter promise must be consumed directly inside a Client Component without drilling primitives:
'use client';
import { use } from 'react';
interface SlugBadgeProps {
paramsPromise: Promise<{ slug: string }>;
}
export function SlugBadge({ paramsPromise }: SlugBadgeProps) {
// use() unwraps the Promise directly inside the Client Component render cycle
const { slug } = use(paramsPromise);
return (
<span className="px-2 py-1 rounded bg-accent-violet/10 text-accent-violet border border-accent-violet/20 font-mono text-xs">
Active Slug: {slug}
</span>
);
}The generateStaticParams() function pre-renders static HTML pages for dynamic routes at build time (next build), unlocking CDN-grade SSG performance:
// 1. Declare candidate paths for build-time prerendering
export async function generateStaticParams() {
const products = [
{ id: 'prod-1' },
{ id: 'prod-2' },
{ id: 'prod-3' },
];
return products.map((product) => ({
id: product.id,
}));
}
// 2. Control runtime fallback for unregistered parameters
// true: generate the page on-demand upon first visit (Incremental Static Regeneration, ISR)
// false: immediately return 404 for any parameter not declared in generateStaticParams
export const dynamicParams = true;
interface ProductPageProps {
params: Promise<{ id: string }>;
}
export default async function ProductPage({ params }: ProductPageProps) {
const { id } = await params;
return <div>Product: {id}</div>;
}What happens if the same fetch('https://api.example.com/products') function is called in both generateStaticParams() and the ProductPage component body?
interface DocsPageProps {
params: Promise<{ slug: string[] }>;
}
export async function generateStaticParams() {
return [
{ slug: ['getting-started'] },
{ slug: ['architecture', 'routing'] },
{ slug: ['architecture', 'rendering', 'rsc'] },
];
}
export default async function DocsPage({ params }: DocsPageProps) {
const { slug } = await params;
const currentPath = slug.join(' / ');
return (
<div className="space-y-3 p-6">
<div className="text-xs text-text-muted font-mono">
Breadcrumbs: docs / {currentPath}
</div>
<h1 className="text-2xl font-bold">
Section: {slug[slug.length - 1]}
</h1>
<p className="text-sm text-text-secondary">
Segment Nesting Depth: {slug.length}
</p>
</div>
);
}The segment app/shop/[[...slug]]/page.tsx handles both the top-level catalog /shop and deeply nested categories like /shop/shoes/nike:
interface ShopPageProps {
params: Promise<{ slug?: string[] }>;
}
export default async function ShopPage({ params }: ShopPageProps) {
const { slug } = await params;
// Base route /shop (slug resolves to undefined)
if (!slug || slug.length === 0) {
return <h1>Store Frontpage (All Categories)</h1>;
}
// Nested route /shop/category/subcategory
return (
<div>
<h2>Filtered Catalog: {slug.join(' > ')}</h2>
</div>
);
}Accessing props.params.slug without await or the use() hook produces runtime warnings and build failures:
Error: Route "[slug]" used params.slug. params should be awaited before using its properties.
Mitigation: Always unwrap params asynchronously: const { slug } = await props.params;.
When both app/posts/new/page.tsx and app/posts/[id]/page.tsx exist, requests to /posts/new resolve to the static route due to Route Precedence. However, if a developer assumes dynamic parameter logic executes for new, unexpected bugs occur.
Mitigation: Define static utility routes (new, edit, settings) in explicit folders and avoid naming dynamic IDs after reserved keywords.
Setting export const dynamicParams = false; instructs the server to return 404 for any URL not declared in generateStaticParams(). If a new database record is created after deployment, users cannot access it until the next build.
Mitigation: Maintain dynamicParams = true (default) whenever on-demand incremental static generation (ISR) is required.