cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

Dynamic routes

Make one Next.js page for every tea with [slug] folders: catch-all and optional catch-all segments, params as a promise, useParams, and typed routes.

By the end, you can
  • 01
    Name the folder for an addressChoose between [slug], [...slug] and [[...slug]], and say what params holds and which addresses answer 404.
  • 02
    Read params the Next.js 16 wayAwait the params promise in a Server Component, list known values or use Suspense, and read them with useParams in a Client Component.
  • 03
    Let TypeScript check your routesType a page with PageProps and turn on typed routes, so a wrong link or a wrong key stops the build.

One folder for every tea

In File-system routing, the tea shop gave each tea a folder of its own: app/teas/green and app/teas/mint, each with the same kind of page inside. That works for two teas. For two hundred, you would copy the same page two hundred times. A dynamic route needs one folder instead. Its name is a placeholder, and Next.js fills it in from the address.

Here is the shop with three teas and one tea page. Like every example in this lesson, it's a real Next.js 16.4.3 project, built with next build and run with next start, and what it sent back was recorded. Read the highlighted lines, then open the Browser tab, pick /teas above the page, and click a tea.

One page in app/teas/[slug] for every teaNext.js
app/teas/[slug]/
app/teas/[slug]/page.tsx
import { notFound } from "next/navigation";import { getTea, teas } from "../data"; export function generateStaticParams() {  return teas.map((tea) => ({ slug: tea.slug }));} export default async function TeaPage({ params }: PageProps<"/teas/[slug]">) {  const { slug } = await params;  const tea = getTea(slug);  if (!tea) notFound();   return (    <main>      <h1>{tea.name}</h1>      <p>        {tea.about} ${tea.price} a box.      </p>    </main>  );}
Recorded from Next.js 16.4.3
localhost:3000/teasTea shop
Home
Our teas

The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

The folder's name is [slug], in square brackets. Square brackets make a folder a dynamic segment: a placeholder that matches any one segment of the address. (A segment is one part of the path between two / characters.) So app/teas/[slug]/page.tsx answers /teas/green, /teas/mint and /teas/oolong. Here is what its file does, line by line:

  • Line 8: Next.js calls TeaPage with a prop called params. Its type, PageProps<"/teas/[slug]">, is one that Next.js writes for this route, so TypeScript knows that params holds a slug.
  • Line 9 waits for params and takes slug out of it. For /teas/green, slug is "green". The await is there because params is a promise of the values, not the values themselves.
  • Line 10 finds the tea with that slug, with getTea from app/teas/data.ts.
  • Line 11: if no tea has that slug, notFound() from next/navigation stops the page and shows the 404 page instead.
  • Lines 15 and 17 show the tea's name, its description and its price. The Browser tab shows Green tea and Light and fresh. $4 a box. for /teas/green.
  • Lines 4 to 6, generateStaticParams, return the slugs of the shop's teas. They tell Next.js which teas exist before any visitor asks, so Next.js can make each tea's page during the build.

In app/teas/page.tsx, line 11 builds each tea's link from its slug: /teas/green, /teas/mint and /teas/oolong. Now open the Terminal tab. The route table lists /teas/[slug] once, as one route. Under it are the pages Next.js made from it: ○ /teas/green, ○ /teas/mint and ○ /teas/oolong. The legend under the table says what ○ means: (Static) prerendered as static content. Here, each tea's HTML was made once, during the build, one for each slug on lines 4 to 6. The line ◐ /teas/[slug] is for every other slug, and ◐ means (Partial Prerender) prerendered as static HTML with dynamic server-streamed content: a part made ahead of time, called the shell, with the rest made for each request.

Three address cards, slash teas slash green, slash teas slash mint and slash teas slash oolong, each with an arrow into one editor window titled [slug] that holds page.tsx. Three arrows leave the window to three cards: slug green, slug mint and slug oolong.the addresswhat params holds/teas/green{ slug: "green" }/teas/mint{ slug: "mint" }/teas/oolong{ slug: "oolong" }[slug]page.tsxone page for every tea
Three addresses, one folder. [slug] takes the segment after /teas, and the one page.tsx gets it in params: green, mint or oolong.
Real-life analogyName badges at a tea festival

A tea festival doesn't design a new badge for every guest. It designs one badge with a blank line, and writes each guest's name in the blank. Guests on the list get theirs printed before the doors open. Anyone else gets one written at the door.

In real life: One badge design with a blank line: "Hello, ___"
In Next.js: One page.tsx in a [slug] folder
In real life: The blank line on the badge
In Next.js: The [slug] in the folder's name
In real life: The name written on one guest's badge
In Next.js: params.slug for one address, like "green"
In real life: Badges printed before the festival, for the guests on the list
In Next.js: Pages made during the build, for the slugs generateStaticParams lists
The main idea

A folder name in square brackets, like [slug], is a placeholder for one segment of the address. One page in that folder answers every address of that shape, and Next.js passes it the segment's text in the params prop: { slug: "green" } for /teas/green.

Square brackets make a placeholder

The word inside the brackets is up to you, and it becomes the key in params. [slug] gives params.slug; a folder named [teaId] would give params.teaId. Slug is a common name for a short, address-friendly name of a thing, like green or green-tea, so [slug] is the name you'll see most.

A route can have more than one placeholder: one per folder. Each fills one segment, and each adds its own key. Each row is one address from this lesson's recordings, the folder that answered it, and what params held:

Addresses and their params
AddressThe folder that answersWhat params holds
/teas/greenapp/teas/[slug]{ slug: "green" }
/teas/oolongapp/teas/[slug]{ slug: "oolong" }
/teas/green/reviews/2app/teas/[slug]/reviews/[reviewId]{ slug: "green", reviewId: "2" }
/teas/saleapp/teas/saleNo slug: a folder with a real name wins.
/teas/green/reviewsNoneNo page: it answers 404.

Three rules come out of that table:

  • Values are strings. The review page printed {"slug":"green","reviewId":"2"}: "2" is text, not the number 2. An address is text, so every value in it is too.
  • A placeholder fills exactly one segment. /teas/green/reviews has one segment more than [slug] takes. In the overview's example, pick it above the page: it answered 404 with the not-found page, and the Network tab shows the status.
  • A folder with a real name wins. Next.js tries the folders with real names first, and only then the placeholder.

The last rule matters when the shop adds a sale page at /teas/sale, right next to the teas. Here it is, beside [slug]:

A sale page next to [slug]Next.js
app/teas/[slug]/sale/
app/teas/sale/page.tsx
export default function SalePage() {  return (    <main>      <h1>Tea sale</h1>      <p>Every tea is $1 off this week.</p>    </main>  );}
Recorded from Next.js 16.4.3
localhost:3000/teas/saleTea shop
Home Teas
Tea sale

Every tea is $1 off this week.

The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

app/teas/sale/page.tsx is an ordinary page: line 4 is its heading, Tea sale. The Terminal lists both routes, /teas/[slug] with its three teas and ○ /teas/sale. The Browser shows Tea sale for /teas/sale, so [slug] never saw the word sale, and /teas/green still shows its tea.

A tea the shop doesn't have

/teas/rooibos has the right shape, so [slug] takes it, and the page runs. It finds no tea, and line 11 calls notFound(). Which status the visitor gets follows one rule, from the notFound() docs: when the response is streamed and its headers have already gone out, the status stays 200, and Next.js adds <meta name="robots" content="noindex">, which tells search engines to skip the page. When the not-found answer isn't streamed, it can be a 404.

Here is what that rule gave in this example. Rooibos isn't one of the slugs on lines 4 to 6, so for the first request, Next.js streamed the shell for other slugs, before the page had run. In the overview's example, pick /teas/rooibos, first request, and open the Network tab: the status is 200, and the HTML has the noindex tag. Next.js then kept the finished result in its cache, so the same address, asked for again, was answered from the cache, not streamed, and its status is 404. Both recorded pages are empty: as the notFound() docs describe, React draws the 404 page in the browser, and recordings run no JavaScript. Not found, forbidden and unauthorized covers status codes in full.

Catch-all and optional catch-all segments

The shop wants tea guides, sorted in topics as deep as they need: /guides/brewing, and /guides/brewing/green-tea under it. [slug] takes exactly one segment, so it can't answer both. Two more folder names can:

  • A catch-all segment, [...topic], with three dots inside the brackets, takes one segment or more.
  • An optional catch-all segment, [[...topic]], in double brackets, takes zero segments or more: it also answers the folder's own address, /guides.

The page below shows what it gets. Line 15 turns params into text with JSON.stringify, inside a Suspense on lines 7 to 9, so the build has something to show while the page waits for params. Pick a folder name, then each address above the page. Before you click, guess which addresses answer 404.

Playground: which addresses does the guides folder take?
The guides page, in this folder inside app/guidesNext.js
app/guides/[topic]/
app/guides/[topic]/page.tsx
import { Suspense } from "react"; export default function GuidePage({ params }: PageProps<"/guides/[topic]">) {  return (    <main>      <h1>Tea guides</h1>      <Suspense fallback={<p>Loading…</p>}>        <ShowParams params={params} />      </Suspense>    </main>  );} async function ShowParams({ params }: { params: Promise<object> }) {  return <p>params: {JSON.stringify(await params)}</p>;}
Recorded from Next.js 16.4.3
localhost:3000/guides/brewing/green-tea404: This page could not be found.
Home Teas
404
This page could not be found.

The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

Try it yourself
[topic]

[topic] fills exactly one segment: /guides/brewing gets {"topic":"brewing"}, a string. /guides/brewing/green-tea has one segment too many, and /guides one too few: both answer 404.

Each version was built with next build from Next.js 16.4.3 and run with next start, which was asked for /guides/brewing, /guides/brewing/green-tea and /guides. Only the folder's name changes. Pick an address above the page. Reset goes back to the first version.

[topic] is a plain dynamic segment: /guides/brewing printed params: {"topic":"brewing"}, and the two other addresses answered 404. [...topic] gives an array, one string for each segment: ["brewing"], then ["brewing","green-tea"], while /guides still answers 404. [[...topic]] gives the same arrays, and answers /guides too, with params: {}: no topic at all. When you read it, topic is undefined.

A grid. The rows are three folder names in app/guides: [topic], [...topic] and [[...topic]]. The columns are two addresses: slash guides, and slash guides slash brewing slash green-tea. For [topic], both are crossed out: 404. For [...topic], slash guides is crossed out, and the deeper address gives the array brewing, green-tea. For [[...topic]], slash guides gives an empty object, and the deeper address gives the same array./guides/guides/brewing/green-tea[topic]404404[...topic]404["brewing","green-tea"][[...topic]]{}["brewing","green-tea"]
Each row is one folder name in app/guides, and each column one address. A crossed-out card is a 404. Only [[...topic]] answers /guides, and both catch-alls give the deeper address as an array of its segments.

Each row is one folder name: the segments it takes, what topic holds, and what /guides answers.

Three folder names in app/guides
FolderWhich segments it takesWhat topic holdsWhat /guides answers
[topic]Exactly one segment.A string: "brewing".404
[...topic]One segment or more.An array: ["brewing", "green-tea"].404
[[...topic]]Zero segments or more.An array, or no topic at all.200, with {}

Use a catch-all when the depth isn't fixed, like topics inside topics, or a path of folders in a file browser. Use the optional one when the folder's own address should show the same page, like a shop at /shop that filters by /shop/green and /shop/green/loose.

params is a promise

In the tea page, line 9 waits: const { slug } = await params. params isn't the values. It's a promise of them. That's because Next.js can make a page at two moments:

  • During the build, before any visitor exists. There's no address yet, so there's no slug, unless your code lists them.
  • When a request comes in, with an address, and so with a slug.

A promise lets Next.js see which parts of a page wait for the address, and make everything else during the build. Pages got params as a promise in Next.js 15. Next.js 16 removed the old way, reading params.slug without await, which you'll still see in older tutorials. A Server Component waits with await, which is why TeaPage is async.

New projects have Cache Components on (it's in next.config.ts). With it, Next.js makes as much of every page as it can during the build, and checks that the rest has something to show while it waits. Each button below is one version of the tea page, built with real Next.js. Before you click, guess what the build prints for each.

Playground: what does the build decide for /teas/[slug]?
The tea shop, with this version of app/teas/[slug]/page.tsxNext.js
app/teas/[slug]/
app/teas/[slug]/page.tsx
import { notFound } from "next/navigation";import { getTea } from "../data"; export default async function TeaPage({ params }: PageProps<"/teas/[slug]">) {  const { slug } = await params;  const tea = getTea(slug);  if (!tea) notFound();   return (    <main>      <h1>{tea.name}</h1>      <p>        {tea.about} ${tea.price} a box.      </p>    </main>  );}
next build failed · Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...Error: Route "/teas/[slug]": Next.js encountered uncached or runtime data during prerendering. `fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience. Ways to fix this:  - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access  - [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"` (does not apply to `connection()`)  - [block] Set `export const instant = false` to allow a blocking route Learn more: https://nextjs.org/docs/messages/blocking-prerender-dynamic    at body (<anonymous>)    at html (<anonymous>)To get a more detailed stack trace and pinpoint the issue, try one of the following:  - Start the app in development mode by running `next dev`, then open "/teas/[slug]" in your browser to investigate the error.  - Rerun the production build with `next build --debug-prerender` to generate better stack traces.Error occurred prerendering page "/teas/[slug]". Read more: https://nextjs.org/docs/messages/prerender-errorExport encountered an error on /teas/[slug]/page: /teas/[slug], exiting the build.⨯ Next.js build worker exited with code: 1 and signal: null
Try it yourself
await params, nothing else

Line 5 waits for params, and nothing says which teas exist. During the build, no visitor has asked for an address yet, so there's no slug to wait for, and nothing to show meanwhile. Next.js stops the build and names the fixes.

Every version was built with next build from Next.js 16.4.3; the Terminal shows what it printed, and the other tabs what next start sent back for /teas/green. Only app/teas/[slug]/page.tsx changes. Reset goes back to the first version.

The first version stops the build. Open its Terminal: Route "/teas/[slug]": Next.js encountered uncached or runtime data during prerendering. The page waits for params at the top, and nothing tells Next.js which slugs exist or what to show meanwhile. The error lists the fixes, and the other two versions are the two this lesson needs:

  • List the values. With generateStaticParams, the build runs the page once for each slug it returns, and saves each page: ○ /teas/green, ○ /teas/mint and ○ /teas/oolong. For any other slug, it keeps a shell, ◐ /teas/[slug], and runs the page when that request comes in.
  • Wait inside Suspense. Move the part that awaits params into its own component, and put it inside Suspense. The build makes the shell with the fallback, Loading the tea…, and streams the tea in for each request: ◐ /teas/[slug]. Open the Network tab to see the fallback in the HTML first, and the tea further down.
Left, a terminal window titled next build lists four cards: circle slash teas slash green, circle slash teas slash mint, circle slash teas slash oolong, and half circle slash teas slash [slug]. Right, on a request: slash teas slash mint has an arrow to a card, circle page, made during the build. Slash teas slash rooibos has an arrow to a card, half circle shell, then the page runs.during the buildnext build○ /teas/green○ /teas/mint○ /teas/oolong◐ /teas/[slug]on a request/teas/mint○ pagemade during the build/teas/rooibos◐ shellthen the page runs
During the build, generateStaticParams names three teas, and Next.js makes a page for each, plus a shell for any other slug. A request for a listed tea gets its page at once. Any other slug gets the shell, ◐ /teas/[slug], and the page runs for that request.

Each row is one way to read params, and what the build does with it.

Reading params, and what the build does
How the page reads paramsWhat next build does
await params at the top, nothing elseThe build stops: there's no slug yet, and nothing to show while the page waits.
generateStaticParams lists the teasEach listed tea's page is made during the build (○). Any other slug gets a shell (◐), filled in on request.
await params inside SuspenseThe shell, with the fallback, is made during the build (◐). The tea streams in for each request.

A shop that knows its teas lists them. A page whose values can be anything, like a search, waits inside Suspense. generateStaticParams has a lesson of its own, generateStaticParams, with what it does for slugs it doesn't list, and slugs inside slugs.

useParams in Client Components

params is a prop of the page. A component deep inside the page doesn't get it unless you pass it down. A Client Component can read it itself, with the useParams hook. Pages and layouts start as Server Components, which run only on the server. A file that starts with "use client" marks where the client part begins: its components, and everything the file imports, also run in the browser, so they can use hooks and handle clicks. Here, the tea page gets an Add to cart button:

An Add to cart button that reads the slug itselfNext.js
app/teas/[slug]/
app/teas/[slug]/add-to-cart.tsx
"use client"; import { useParams } from "next/navigation"; export default function AddToCart() {  const { slug } = useParams<{ slug: string }>();   return (    <button onClick={() => (document.cookie = `cart=${slug}`)}>      Add {slug} to cart    </button>  );}
Recorded from Next.js 16.4.3
localhost:3000/teas/greenTea shop
Home Teas
Green tea

Light and fresh. $4 a box.

Add green to cart

The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.

In app/teas/[slug]/add-to-cart.tsx, line 1 makes it a Client Component, and line 3 imports useParams from next/navigation. Line 6 calls it, and gets the same object as params, with no await: { slug: "green" } on /teas/green. Line 9 saves the slug in a cookie when the button is clicked, and line 10 puts it in the label. Open app/teas/[slug]/page.tsx: line 20 adds <AddToCart /> with no props at all.

The Browser tab shows Add green to cart on /teas/green and Add mint to cart on /teas/mint. That text is in the HTML: the slugs are listed, so Next.js knew each one during the build, and rendered the button with it. The button runs in the browser too, so its code is sent there: the recording checked the JavaScript files the page loads, and they contain cart= from line 9.

The value follows the address as the visitor moves around. Open the Steps tab: these steps were taken in real Chrome. The first opens /teas/green. The second clicks Teas, and the third clicks Mint tea. After the last click, the address is /teas/mint, and the button says Add mint to cart: useParams gave it the new slug. The page didn't load again. After the first step, the steps list no page load, only requests from the page's own JavaScript: Next.js's router asking the server for RSC payloads, the data that describes a page, instead of whole HTML pages. The first look at how Next.js works introduced them.

When the slugs aren't listed, useParams has nothing to give during the build, and it needs a Suspense around it, like await params does: without one, the build stops. Which to use? In a Server Component, read the params prop. In a Client Component deep in the page, call useParams instead of passing the values down through every component. Hooks only run in Client Components (Rules of hooks).

Typed routes check links and params

A route's placeholders are known from its folders, so TypeScript can check them. Next.js writes types for every route during next build (and next dev). Two of them matter here:

  • PageProps<"/teas/[slug]">, on line 8 of the tea page, is the type of that route's page props. It says params is a promise of { slug: string }. You never import it: Next.js makes it available in every file, like LayoutProps in the root layout.
  • Typed routes: with typedRoutes: true in next.config.ts, Next.js also writes the list of every route. Then the type check checks the destinations TypeScript can see: the href of each Next.js Link, and the addresses passed to router.push and router.replace. An address built at run time from a plain string isn't checked unless you mark it with the Route type. The option became stable in Next.js 15.5.

next build runs the type check before it makes any page, so a type error stops the build. Pick each version, and read the Terminal's Running TypeScript step.

Playground: what does the type check catch?
The tea shop, with this changeNext.js
app/teas/[slug]/
next.config.ts
import type { NextConfig } from "next"; const nextConfig: NextConfig = {  /* config options here */  cacheComponents: true,  partialPrefetching: true,  typedRoutes: true,}; export default nextConfig;
Recorded from Next.js 16.4.3
$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled   Creating an optimized production build ...✓ Compiled successfully  Running TypeScript ...  Finished TypeScript ...  Collecting page data ...✓ Generating static pages (8/8)  Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found├ ○ /teas└   /teas/[slug]  ├ ◐ /teas/[slug]  ├ ○ /teas/green  ├ ○ /teas/mint  └ ○ /teas/oolong ○  (Static)             prerendered as static content◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content
Try it yourself
Typed routes, every link right

Line 7 turns on typed routes. During the build, Next.js writes a type that lists every route, and the type check compares each Link address it can see with it. Every link here points at a real route, so the build passes.

Every version was built with next build from Next.js 16.4.3, which checks the types before it makes any page. Each changes one file of the shop. Reset goes back to the first version.

With every link right, the type check passes, and the shop builds as before. With <Link href="/tea"> on line 10 of the layout, it stops at that line: Type '"/tea"' is not assignable to type 'UrlObject | RouteImpl<"/tea">'. Did you mean '"/teas"'? And the third version reads id on line 9, from a folder named [slug]. PageProps catches it, with typed routes off: Property 'id' does not exist on type '{ slug: string; }'. Without the type, id would be undefined, and every tea would get the 404 page. A link built with a template literal, like `/teas/${tea.slug}` on line 11 of the teas page, passes the check: it has the shape of /teas/[slug]. (Why TypeScript? explains what a type check is.)

Planning the shop's dynamic routes

To choose a folder for an address, count the segments you don't know ahead of time. Exactly one: [slug]. One or more: [...slug]. Zero or more: [[...slug]]. A segment you do know, like sale, gets a folder with a real name. Each row is one kind of address the shop wants, the file that answers it, and what params holds there, as the recordings showed.

The shop's addresses and their folders
AddressFileWhat params holds
/teas/green, /teas/mint, …app/teas/[slug]/page.tsx{ slug: "green" }
/teas/saleapp/teas/sale/page.tsx{}: no dynamic segment.
/teas/green/reviews/2app/teas/[slug]/reviews/[reviewId]/page.tsx{ slug: "green", reviewId: "2" }
/guides/brewing/green-tea, any depthapp/guides/[...topic]/page.tsx{ topic: ["brewing", "green-tea"] }
/guides and everything under itapp/guides/[[...topic]]/page.tsx{} for /guides, an array below it.

Now sort some addresses. The shop has app/teas/[slug] and app/guides/[...topic]. Which folder takes each address, and which ones have no page at all?

Which folder takes this address?
  • /teas/green
  • /teas/rooibos
  • /guides/brewing
  • /guides/brewing/green-tea
  • /teas/green/reviews
  • /guides
Try it yourself
0 of 6 correct

Sort each address: does app/teas/[slug] take it, does app/guides/[...topic] take it, or does it answer 404?

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "params is a plain object." No. It's a promise. Await it in a Server Component, or use useParams in a Client Component. Reading params.slug directly was removed in Next.js 16.
  • "/teas/green/reviews/2 gives reviewId: 2." No. Every value is a string: the recording printed "2". Convert it with Number() when you need a number.
  • "[slug] takes the rest of the address." No. It takes exactly one segment. /teas/green/reviews answered 404.
  • "[...slug] answers the folder's own address." No. It needs at least one segment: /guides answered 404. Only [[...slug]] answers it.
  • "/teas/sale goes to [slug]." No. A folder with a real name wins over a placeholder in the same folder.
  • "notFound() always sends a 404." No. When the response is already streaming, its headers are gone, so it keeps 200 and gets a noindex tag. In this example, the first /teas/rooibos was streamed and answered 200; the cached second one answered 404.

Practice exercises

These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.

Exercise 1 · Warm-upRead params from two placeholders

The shop adds a page for each review of a tea. A visitor opens /teas/green/reviews/2. What is params.reviewId in app/teas/[slug]/reviews/[reviewId]/page.tsx?

Starter codeText
app/
  teas/
    [slug]/
      reviews/
        [reviewId]/
          page.tsx
      page.tsx
    data.ts
    page.tsx
  layout.tsx
  page.tsx

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upPick the folder for the guides

    One page in app/guides must answer /guides, /guides/brewing and /guides/brewing/green-tea, and its key must be topic. What's the folder's name?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticePredict what /guides answers

      This page is app/guides/[...topic]/page.tsx, and the shop has no app/guides/page.tsx. What does /guides answer?

      Starter codeTSX
      import { Suspense } from "react";
      
      export default function GuidePage({ params }: PageProps<"/guides/[...topic]">) {
        return (
          <main>
            <h1>Tea guides</h1>
            <Suspense fallback={<p>Loading…</p>}>
              <ShowParams params={params} />
            </Suspense>
          </main>
        );
      }
      
      async function ShowParams({ params }: { params: Promise<object> }) {
        return <p>params: {JSON.stringify(await params)}</p>;
      }
      

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFix the key

        This is app/teas/[slug]/page.tsx, and next build stops with Property 'id' does not exist on type '{ slug: string; }'. What should line 9 read?

        Starter codeTSX
        import { notFound } from "next/navigation";
        import { getTea, teas } from "../data";
        
        export function generateStaticParams() {
          return teas.map((tea) => ({ slug: tea.slug }));
        }
        
        export default async function TeaPage({ params }: PageProps<"/teas/[slug]">) {
          const { id } = await params;
          const tea = getTea(id);
          if (!tea) notFound();
        
          return (
            <main>
              <h1>{tea.name}</h1>
              <p>
                {tea.about} ${tea.price} a box.
              </p>
            </main>
          );
        }
        

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeFind why the build stops

          The tea page, now without generateStaticParams, gets the Add to cart button from the useParams section. The build stops. What's the smallest fix?

          Starter codeTSX
          import { Suspense } from "react";
          import { notFound } from "next/navigation";
          import { getTea } from "../data";
          import AddToCart from "./add-to-cart";
          
          export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {
            return (
              <main>
                <Suspense fallback={<p>Loading the tea…</p>}>
                  <TeaDetails params={params} />
                </Suspense>
                <AddToCart />
              </main>
            );
          }
          
          async function TeaDetails({ params }: { params: Promise<{ slug: string }> }) {
            const { slug } = await params;
            const tea = getTea(slug);
            if (!tea) notFound();
          
            return <h1>{tea.name}</h1>;
          }
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeTwo pages for /shop

            With these files, next build stops before it makes any page. Which file do you delete to fix it?

            Starter codeText
            app/
              shop/
                [[...filters]]/
                  page.tsx
                page.tsx
              layout.tsx
              page.tsx

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, count the address's segments, and ask when each value is known: during the build, or on a request.

              The Dynamic routes quiz · 8 questionsScore: first tries count
              1. Question 1 of 8Which folder gives one page to /teas/green, /teas/mint and every other tea?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does params hold for /guides/brewing/green-tea with this file as app/guides/[...topic]/page.tsx?

                Read the code, then predictTSX
                import { Suspense } from "react";
                
                export default function GuidePage({ params }: PageProps<"/guides/[...topic]">) {
                  return (
                    <main>
                      <h1>Tea guides</h1>
                      <Suspense fallback={<p>Loading…</p>}>
                        <ShowParams params={params} />
                      </Suspense>
                    </main>
                  );
                }
                
                async function ShowParams({ params }: { params: Promise<object> }) {
                  return <p>params: {JSON.stringify(await params)}</p>;
                }
                

                Choose an answer to see the explanation.

              3. Question 3 of 8With app/guides/[...topic]/page.tsx, what does /guides answer?

                Choose an answer to see the explanation.

              4. Question 4 of 8The shop has app/teas/sale/page.tsx and app/teas/[slug]/page.tsx. What does /teas/sale show?

                Choose an answer to see the explanation.

              5. Question 5 of 8Why do you write await params in a Next.js 16 page?

                Choose an answer to see the explanation.

              6. Question 6 of 8A tea page awaits params at the top, with no generateStaticParams and no Suspense. What does next build do?

                Choose an answer to see the explanation.

              7. Question 7 of 8Where does useParams work?

                Read the code, then predictTSX
                "use client";
                
                import { useParams } from "next/navigation";
                
                export default function AddToCart() {
                  const { slug } = useParams<{ slug: string }>();
                
                  return (
                    <button onClick={() => (document.cookie = `cart=${slug}`)}>
                      Add {slug} to cart
                    </button>
                  );
                }
                

                Choose an answer to see the explanation.

              8. Question 8 of 8Typed routes are on, and the layout has <Link href="/tea">. The shop only has /teas. What happens?

                Choose an answer to see the explanation.

              Key takeaways

              • A folder name in square brackets is a dynamic segment: app/teas/[slug] answers /teas/green and every other address with one segment after /teas.
              • The page gets the values in params, keyed by the words in the brackets. Every value is a string.
              • [...slug] takes one segment or more, as an array. [[...slug]] takes zero or more, so it also answers the folder's own address. Addresses with a shape no folder takes answer 404.
              • params is a promise. With Cache Components, list the values with generateStaticParams, or wait for them inside Suspense; otherwise the build stops.
              • Client Components read the same values with useParams. PageProps types params, and typedRoutes checks the addresses of Link, router.push and router.replace during the build.

              Remember the one-liner.
              Brackets make a placeholder, and params tells the page what filled it.

              Coming next: Search params and URL state. It reads the part of the address after the ?, for search, filters and pages.

              CompleteFrontend Clear concepts. Working examples.