If you wrote a separate page file for every single note, a thousand notes would mean a thousand files. A dynamic route reads the id out of the URL and lets one page template display many different pieces of data.
The key idea
app/notes/[id]/page.tsx matches URLs like /notes/routing. In the current App Router, params is a Promise, so you await it to get the id. When data isn't found, don't just render an empty page — call notFound() so the nearest not-found.tsx UI shows instead. If you want to pre-build certain routes at build time, you can use generateStaticParams.
Let's try it together
import { notFound } from "next/navigation";
import { getNote } from "@/lib/notes";
export default async function NotePage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const note = await getNote(id);
if (!note) notFound();
return (
<article>
<h1>{note.title}</h1>
<p>{note.content}</p>
</article>
);
}How the code works
getNote represents the data layer. It awaits params and looks up the record by id. If the record isn't found, it goes into the 404 flow; if it is found, it renders the title and content.
The note detail shows up for an id that exists, and 404 UI shows up for an id that doesn't.5-Minute Try-It
Build a route at app/users/[username]/page.tsx and display the username from the URL in a heading.
Next.js — Layouts and Pages — Next.js