Lesson 8 of 81
Every route in the App Router quietly gets classified as static or dynamic before it ever serves a request — and one line of code, a single cookies() call, is enough to flip that classification for the whole route.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A route has no cookies(), headers(), searchParams, or draftMode() calls anywhere, and its one fetch() call uses default options. How is it rendered?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Static rendering means Next.js renders a route once, at build time, and serves that same prerendered HTML to every visitor — instant, cacheable, servable straight from a CDN. Dynamic rendering means the route is rendered fresh on the server for every single request, because something about it can only be known once a real request arrives.
Next.js decides which one a route gets automatically, per route, based on what the route's code actually does — you don't declare it up front. The trigger is a specific category of function called Request-time APIs: cookies(), headers(), searchParams, and draftMode(). Any of these accesses request-specific data that doesn't exist until a real request shows up, so a route that calls one of them can't be fully resolved at build time — it opts the whole route into dynamic rendering. Separately, a fetch() call that's explicitly told not to cache (cache: 'no-store', or revalidate: 0) has the same effect, even with zero Request-time API calls anywhere in the route.
// app/dashboard/page.tsx — static: no request-time data used
export default async function DashboardPage() {
const stats = await fetch("https://api.example.com/stats");
const data = await stats.json();
return <StatsView data={data} />;
}
A route with only a default fetch() call — no Request-time APIs anywhere.
// app/dashboard/page.tsx — now dynamic: cookies() reads request data
import { cookies } from "next/headers";
export default async function DashboardPage() {
const theme = (await cookies()).get("theme")?.value ?? "light";
const stats = await fetch("https://api.example.com/stats");
const data = await stats.json();
return <StatsView data={data} theme={theme} />;
}
Nothing else changed between these two versions — same fetch, same JSX shape — but adding one cookies() call is enough to move the whole route from "prerendered once, served from cache" to "rendered on the server for every visitor." That's the entire point of the model: Next.js doesn't ask you to declare static-or-dynamic, it infers it from what your code touches, and the inference is route-wide, not line-by-line.
cookies(), headers(), searchParams, and draftMode() count as Request-time APIs. Reading an environment variable or a build-time config file doesn't touch this at all.fetch(url, { cache: 'no-store' }) forces dynamic rendering on its own, with zero cookies/headers/searchParams calls anywhere in the route.cookies() call in a deeply nested component can move the entire route to dynamic rendering, not just that component.fetch() call is not automatically dynamic on its own — Next.js fetches it once at build time if the route has no Request-time APIs, and re-fetches per request only if the route is already dynamic for some other reason. The fetch call inherits the route's classification; it doesn't set it by itself unless explicitly told to opt out of caching.export const dynamic = 'force-dynamic' (always per-request) or 'force-static' (always prerendered, with cookies()/headers()/searchParams returning empty values) — useful when you want to override what the inference would have chosen."Why did adding a cookies() call suddenly make my whole page slow / stop being cached at the CDN?" is a real production question, and answering it well means explaining the inference model, not just naming cookies() as a "dynamic function" — the route-wide scope of the effect is the part that actually catches people off guard.