Routing and navigation

Build dynamic routes

Represent resource identifiers with dynamic segments and read the resolved parameters in an App Router page.

You do not create one file for every note. A dynamic segment such as [slug] captures that part of the URL and lets one page render many resources.

Create app/notes/[slug]/page.tsx. In current Next.js, params is asynchronous, so await it before reading the value. Validate the slug before using it in a query. A value from the URL is untrusted input.

Dynamic does not necessarily mean request-only. If the set of slugs is known at build time, generateStaticParams can return them for prerendering. Other values may still render on demand unless the route configuration rejects them. Choose that behavior from the data lifecycle, not from the bracket syntax alone.

export default async function NotePage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  return <p>Reading {slug}</p>
}

Open /notes/first-note and /notes/another-note. Each should render Reading first-note and Reading another-note. If you forget await params, you may read undefined or hit a type error depending on your Next.js version.

Decode and validate the slug before it reaches the data layer. Return notFound() for a valid-looking slug with no record. Add generateStaticParams for two known notes and compare npm run build output with an on-demand slug you did not list.

Try odd slugs like hello%20world or %2e%2e in dev and confirm your validation rejects garbage before it touches a database query.

If you pass the raw slug straight into a filesystem path or SQL string, you invite bugs that have nothing to do with Next.js. Treat params like any other external input.

Log the resolved slug on the server during development if lookups fail mysteriously. A trailing space in the URL often means your validator trimmed the value but the database key did not.

Lesson completed