Test Yourself!

Ten questions on the App Router, one per area of this chapter. None of them is trivia about an option name: each turns on a behavior that contradicts what the code looks like it does, and every one of them has sent somebody hunting through the wrong file for an afternoon. Work out your answer before you run anything. The answers, with the reasoning and the section that explains each one, are in Appendix G.

A scratch project is all you need: npx create-next-app@16.3.5 with the App Router, then npm 2,036 run build and npx next start. Serve a production build rather than using next dev: prerendering and cache headers behave as described here only in a production server. Throughout, notFound and redirect come from next/navigation, connection from next/server, and Suspense from react. Check your work with curl 3,008 -i, which prints the status line and the headers that several of these questions turn on.

Routing, rendering, and data

Questions 1-4: slots, status codes, props, and one fetch too manyShell
// 1. The only files under app/dash are this layout, page.js, settings/page.js and
//    @side/page.js. Loading /dash puts "Side: live" in the <aside>. What lands
//    there on a hard load of /dash/settings, and what does `next build` say?
export default function DashLayout({ children, side }) {
  return <div>{children}<aside>{side}</aside></div>
}
// 2. Two pages, both ending in notFound(); Book() sleeps 300 ms and then calls it.
//    curl reports a different status code for each. Which is which, and why?
export default async function Early() { await connection(); notFound() }
export default async function Late() {
  await connection()
  return <Suspense fallback={<p>Loading...</p>}><Book /></Suspense>
}
// 3. Price is a 'use client' component that renders {money.format()}. What does a
//    request to /money return, and what does the value echoed in the error prove
//    about how far the prop got?
class Money {
  constructor(cents) { this.cents = cents }
  format() { return '$' + (this.cents / 100).toFixed(2) }
}
export default function Page() { return <Price money={new Money(1999)} /> }
// 4. A dynamic page renders two <Row/>s, and each one awaits hits(). The API
//    counts every request it serves. What number does each row print, how many
//    lines does one page load add to the API's log, and does 'no-store' change it?
const hits = () =>
  fetch('http://localhost:4170/hits', { cache: 'no-store' }).then((r) => r.json())

The next three are about time and about verbs: when a cached page is allowed to look out of date, what a control-flow exception does to a try block, and which HTTP methods a route file answers that you never wrote. Question 5 is worth running with a stopwatch — request the page every two seconds and write down both the timestamp in the body and the x-nextjs-cache header, because the interesting request is the one where they disagree.

Questions 5-7: revalidation, a swallowed redirect, and free verbsShell
// 5. Requests every 2 s against `next start`; the page was prerendered at build
//    time. At t=6s the response carries x-nextjs-cache: STALE. Which timestamp
//    does that response show, which one does t=8s show, and which file changed?
export const revalidate = 5
export default function Page() {
  return <p>generated at {new Date().toISOString()}</p>
}
// 6. What status and what body does GET /api/go return, and what is err.digest?
export async function GET() {
  try {
    redirect('/thanks')
  } catch (err) {
    return Response.json({ redirected: false })
  }
}
// 7. This route file exports GET and POST and nothing else. What do OPTIONS,
//    PUT and HEAD on /api/products answer, and which response header on the
//    first of the three tells you where its answer came from?
export async function GET() { return Response.json({ products: ['p1', 'p2'] }) }
export async function POST(req) { return Response.json(await req.json(), { status: 201 }) }

The last three are the ones that pass every test on your machine and then break in front of users: a security header that works only on the routes you did not prerender, a documented Sass 22,539 feature that the default bundler does not implement, and a deployment that serves perfect HTML and nothing else.

Questions 8-10: a nonce, a Sass export, and a deploymentJavaScript
// 8. proxy.js mints a nonce per request and sets it in the CSP header for /login.
//    /login is a Server Component holding one 'use client' button, and `next build`
//    marks it ○ (Static). How many nonce= attributes does the served HTML carry,
//    what does Chrome do with it, and which one-line change fixes it?
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
const csp = `script-src 'self' 'nonce-${nonce}' 'strict-dynamic';`
// 9. panel.module.scss, imported as `styles`, declares $accent: #1565c0, then
//    ":export { accent: $accent }" and ".title { color: $accent }", and the heading
//    really is blue. What do these two lines print, and what does `next build` warn?
<p>accent is {String(styles.accent)}</p>
<p>keys: {Object.keys(styles).join(', ')}</p>
// 10. next.config.mjs sets output: 'standalone'. You copy .next/standalone to a
//     server and run `node server.js`. GET / answers 200 with exactly the right
//     markup, but the page is unstyled and inert and /robots.txt is 404. What did
//     the file trace leave behind, and which two paths must you copy yourself?

One habit settles most of these without guesswork: read the next build route table before you blame the browser. The marker beside a route (○, ●, ƒ) decides Questions 5 and 8 on its own, and a route that is missing from that table altogether is the whole of Question 1.