Next.js
Dynamic Routes Done Right: Slugs, Params, and Clean URLs
How to structure dynamic segments, handle slugs and params, and keep URLs clean and predictable in the App Router.

Dynamic routes in the App Router are defined by folder names in square brackets. Getting params, validating slugs, and returning 404 when content is missing keeps URLs clean and behavior predictable.
Folder convention: [slug] and [id]
A folder named [slug] or [id] creates a dynamic segment. The param name is the folder name without the brackets. You get it in page.tsx (and layout.tsx) via the params prop. In the App Router, params are a Promise in async components, so await them before use.
// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) notFound();
return <Article post={post} />;
}Use slug when the URL is human-readable (e.g. /blog/hello-world); use id when it’s an opaque identifier. Prefer slugs for public-facing content and SEO.
Validating and notFound()
Always validate that the resource exists. If the CMS or DB returns null, call notFound() so Next.js renders the nearest not-found.tsx and returns a 404. Don’t render a fallback with “Post not found” and still return 200—that confuses users and crawlers.
const post = await getPostBySlug(slug);
if (!post) notFound();Use generateStaticParams for static generation: return an array of known slugs (or ids) so Next.js can pre-render those paths at build time. For on-demand or revalidated content, you can still use notFound() when data is missing at request time.
Clean URLs and trailing slashes
Keep URLs consistent: either with or without a trailing slash, and match your config. Avoid duplicate URLs (e.g. /blog/post and /blog/post/) by redirecting one to the other if needed. Use the same slug in links and in canonical metadata so there’s a single URL per piece of content.
Summary
Use [slug] or [id] for dynamic segments; await params in async components. Validate existence and call notFound() when content is missing. Use generateStaticParams for static generation and keep URLs and canonicals consistent. That’s dynamic routes done right.