Everything the adaptive question bank can teach and test in Next.js, from foundations through advanced practice. Work through it in order, or start practicing and let the questions find your level.
Both routers can run side by side, which is how teams migrate one page at a time. The catch is a shared URL: if app/about/page.tsx and pages/about.tsx both exist, Next.js 16.4 fails the build with a conflict error. Moving between a route served by app and one served by pages is also a hard navigation, and next/link does not prefetch across the two.
Moving app or pages under src relocates only application code. package.json, next.config.*, tsconfig.json, .env* files and public/ stay at the root, while proxy.ts moves into src beside the app. If a root-level app folder also exists, src/app is ignored, and an @/* alias in tsconfig.json has to be repointed at src/.
A folder becomes reachable only once it holds a page or route file, and even then only what that file returns is sent to the client. So Button.tsx or utils.test.ts beside a page stays private. The trap is the reserved names themselves: a helper file called page.tsx, route.ts, layout.tsx or loading.tsx is picked up as a special file.
The underscore opts the folder and every subfolder out of routing, even if a page.tsx sits inside. Colocation is already safe without it, so the real payoff is separating UI code from routes and avoiding clashes with special file names Next.js may add later. If you need a URL segment that really starts with _, name the folder %5Fname, the encoded underscore.
public/avatars/me.png is served at /avatars/me.png with no build hash in its name. Because the file could change between deploys, Next.js sends Cache-Control: public, max-age=0, so browsers revalidate it every time. To cache it for long, version the filename yourself. next/image can still optimize a public image through its own endpoint; the raw URL serves the original bytes.
The default list is tsx, ts, jsx and js, and a file with any other suffix is invisible to routing. Adding md and mdx with @next/mdx lets a page.mdx become a route. The setting is global: it also decides how proxy and instrumentation files must be named, so a pattern such as page.ts forces names like proxy.page.ts.
Next.js reads paths and baseUrl from tsconfig.json or jsconfig.json itself, so @/components/button resolves in dev and build without a webpack or Turbopack alias. Targets resolve relative to baseUrl when one is set. The usual breakage is moving code into src/ without updating the mapping, or adding TypeScript while the paths stay behind in an old jsconfig.json.
next dev, next build and next typegen rewrite next-env.d.ts, which wires in Next's types for image and stylesheet imports, so hand edits vanish; keep it out of Git and inside the tsconfig include list. The bundled plugin, switched on by picking the workspace TypeScript version in your editor, flags invalid segment config values and client hooks used outside Client Components.
B. Layouts, pages and the segment tree
app/blog/authors/ maps to /blog/authors, but the folder alone serves nothing: until a page or route file lands in it, a request gets a 404. That is why a folder can exist only to hold a layout.tsx shared by deeper pages, or to keep colocated components together, without exposing a URL of its own.
Within its own folder, a page is always the innermost file: that segment's layout, template, error boundary and loading boundary all wrap it. It is a Server Component unless marked 'use client', and it receives params and searchParams as Promises. A Client Component page cannot await them in render, so it unwraps them with React's use().
Whatever sits below fills the children prop: a nested layout, the page, or a loading or error UI while one is active. Forget to render {children} and no page under that folder ever appears. A layout cannot pass props or data down to its page, so fetch the same data in both and let fetch memoization or React.cache dedupe the call.
app/layout.tsx must return <html> and <body>, and head tags such as <title> belong in the Metadata API, not a hand-written <head>. Any layout with no layout above it is a root, so route groups can each have one. Two files render their own <html> and <body> because they replace the root layout: global-error.tsx and the experimental global-not-found.tsx.
On a client navigation only the segments that change are re-rendered, so a shared layout keeps its React state, such as an open menu or a typed search term. The flip side is that it never sees the new URL, which is why it gets no searchParams or pathname. Put URL-dependent pieces in a Client Component using usePathname or useSearchParams, which do re-render on navigation.
A template is keyed to the segment directly below it, so it remounts, resetting child state, re-running effects and rebuilding DOM, whenever that segment or its params change. A search-param change does not remount it, and navigating deeper inside the same segment leaves higher templates alone. It sits between the layout and the error boundary, handy for per-page enter effects.
app/layout.tsx wraps app/dashboard/layout.tsx, which wraps every page under dashboard; the chain always runs from the root down. A page cannot opt out of an ancestor's layout. To give some routes a different shell, restructure with route groups: move the routes that share a layout into (shop), leave the rest outside, and the layout wraps only the group.
Delete the top-level app/layout.tsx and give each group, such as (marketing) and (shop), a layout that renders the document tags itself. Navigating between pages under different root layouts reloads the whole document instead of doing a client transition, so client state is lost. Without a top-level layout, the / page has to live inside one of the groups.
Layouts are reused across navigations and cached on the client, so a query string read once would go stale the moment the user changes ?page=2. Next.js therefore withholds searchParams, and the pathname, from layouts. Read the query in the page through its searchParams prop, or in a Client Component with useSearchParams, both of which update on navigation.
A segment is either UI or an endpoint. Put page.tsx and route.ts in the same folder and Next.js 16.4 refuses to build, even when the route file exports only POST. Whichever file stands alone handles every HTTP verb for that path, so an API usually lives in a sibling segment such as app/api/items/route.ts.
Each file wraps everything after it, so its reach is set by position. Because error sits inside layout and template, an error thrown by the layout of the same folder escapes to the parent's boundary, or to global-error at the root. Likewise loading cannot show a fallback for a slow layout in its own segment, only for the page and deeper segments.
C. Dynamic segments and route organization
A folder named [slug] captures one path segment as a string, so /blog/hello gives { slug: 'hello' } while /blog/a/b does not match it at all. The same params reach the layout, page, route handler and generateMetadata for that path. Users can type any value into the URL, so validate it and call notFound() for unknown ones instead of trusting the type.
app/shop/[...slug] matches /shop/a as ['a'] and /shop/a/b/c as ['a', 'b', 'c'], always an array even when only one segment is present. It does not match /shop itself; that path needs its own page.tsx or the optional form. Code that treats slug as a single string works on short paths and quietly breaks on deeper ones.
The double brackets make the segment list optional, so app/shop/[[...slug]] serves /shop with slug set to undefined, plus every deeper path as an array. Check for undefined before indexing. Because it already owns /shop, adding app/shop/page.tsx beside it is a build error in Next.js 16.4 about two routes with the same specificity.
Next.js 15 turned params, searchParams, cookies(), headers() and draftMode() into Promises with a temporary synchronous fallback, and Next.js 16 removed the fallback, so reading params.slug directly no longer works. Await them in a Server Component, or unwrap them with React's use() in a Client Component. The global PageProps<'/blog/[slug]'> helper types them.
Each object you return becomes one prerendered URL, and a child segment's function runs once for each set its parent returns. Paths you leave out are not blocked: by default they render on first request and are then stored. Its fetch calls are memoized with the rest of the render in either model, and with Cache Components on it must return at least one entry, since an empty array is a build error.
It shuts out paths that generateStaticParams did not list, giving a 404 where the default of true would render the page on demand. It plays the role fallback: false had in getStaticPaths. The export belongs to the previous caching model and is unavailable with cacheComponents; there an unlisted URL renders on demand, and with Partial Prefetching, added in 16.3, it gets the App Shell first.
Parentheses keep a folder out of the URL, so app/(shop)/cart/page.tsx serves /cart. Groups let one set of routes share a layout that their siblings skip, or let each carry a separate root layout. Two groups must never resolve to the same path: (marketing)/about and (shop)/about both mean /about, and that collision is an error.
Each @slot folder becomes a prop on the parent layout next to children, and slots are not URL segments, so app/@analytics/views answers /views. Every slot renders on the server even when the layout shows only one, so authorize inside each slot. Slots render together with the page, so if one slot at a level is dynamic, all of them are.
During client navigation each slot remembers its last active subpage, even when that no longer matches the URL. A reload cannot recover that memory, so Next.js renders default for every slot without a match. Since Next.js 16, a slot missing one fails the build. Return null to show nothing, or call notFound() to keep the older 404 behavior.
The markers count route segments, not folders: (.) is the same level, (..) one level up, (..)(..) two, and (...) the app root, while @slot folders do not count. Interception happens only on a soft navigation, so clicking a photo opens it over the feed while a refresh or a shared link renders the full photo page. Pair it with a parallel @modal slot.
The i18n key in next.config serves the Pages Router, and its locale fields are gone from the App Router. Instead, nest every special file under app/[lang], let proxy.ts read Accept-Language and redirect a bare path to /en/..., and prerender the locales with generateStaticParams. Validate lang, because a typo in the URL still matches the segment.
D. Server and Client Components
Layouts, pages and whatever they import render on the server, and their code never ships to the browser. A component becomes a Client Component when its own file starts with 'use client' or when such a file imports it, directly or through other imports. The Pages Router never had this split: every one of its components ships to the browser and hydrates.
The directive tags a module as an entry into the client graph. Everything that module imports is bundled for the browser too, whether or not those files carry the directive, so you need it only where a Server Component imports a client piece. Server Components handed in as children are the exception: they arrive as rendered output, not as imported code.
At file level, 'use client' and 'use server' count only as the first statement of the module; comments may sit above, and the quotes must be single or double, never backticks. Next.js 16.4 does not silently ignore a misplaced directive: a 'use client' written after an import stops the build with an error saying it must come before other expressions.
A Server Component renders on the server, so React lets it be an async function that awaits a query in its body. A Client Component re-renders in the browser and cannot await during render; it receives a promise and unwraps it with use() under a Suspense boundary instead. Marking a client component async is unsupported, not a shortcut.
Server Component code never reaches the browser, so nothing is there to hold state or answer a click: useState, useEffect and onClick all need a Client Component. Plenty of interaction needs no client code, though. A <details> element toggles itself, and a <form> can submit straight to a Server Action. Reach for 'use client' when browser state changes over time.
Props cross the boundary inside the RSC payload, so they must be values React can encode: primitives, plain objects, arrays, Date, Map, Set, promises and JSX. Passing an onClick handler or a class instance throws. The one kind of function that may cross is a Server Function marked 'use server', which travels as a reference, not as code.
The owner, the component whose JSX creates an element, decides where it runs, not the parent that places it. When a Server Component page writes <Modal><Cart /></Modal>, Cart renders on the server and Modal only receives its output. Importing Cart inside the client Modal file instead would pull its code into the client bundle and make it a Client Component.
The payload carries each Server Component's rendered output, placeholders pointing at Client Component bundles, and the props passed to them. On first load it is reconciled against the HTML; on later navigations the router fetches only the payload, often prefetched, and React merges it into the live tree, so shared layouts and client state survive. Editing server-rendered DOM by hand drifts from that tree.
'use client' decides where code may run, not whether HTML is produced. On a direct visit a Client Component renders on the server to HTML, then again in the browser to hydrate, which is why its console.log shows in both the terminal and the browser, and why touching window during render breaks. On a client-side navigation it renders in the browser only.
Server Components can neither create nor read React context. Wrap the provider in a small Client Component that takes children, then render it from the layout; the server-rendered pages passed through it still render on the server. Place it as deep as you can, around {children} rather than the whole <html>, so Next.js keeps optimizing the static parts above it.
A shared module can be imported from both graphs, so a helper holding a database client or a secret can end up in client code, where any env var without the NEXT_PUBLIC_ prefix reads as an empty string. Adding the import makes that mistake fail at build time instead. Next.js handles server-only and client-only itself, so installing those npm packages is optional.
Every module below 'use client' ships to the browser, so marking a whole layout as client drags its static markup and imports along. Keep the layout a Server Component and mark only the interactive leaf, such as a search box or a like button. Wrap a third-party component that uses hooks but lacks the directive in your own small client file instead of converting its parent.
E. Rendering: prerender, dynamic and streaming
With Cache Components the build renders each route until it reaches work that cannot finish ahead of time. What completes, plus the fallbacks of any boundaries left open, becomes HTML for direct visits and an RSC payload for client navigations. Both can sit on a CDN and be served without touching your server, while the open holes stream from the server at request time.
These APIs read data that exists only once a real request arrives, so prerendering stops at the component that calls them. With Cache Components only that subtree waits, provided a <Suspense> boundary sits above it, and the rest of the route stays in the static shell. In the previous model, a single call made the whole route dynamic.
PPR moves the static-versus-dynamic decision from the route to the component. The prerendered shell goes out first, and each part that needs request data or a fresh uncached read streams into its Suspense slot within the same response. A mostly static page with one personal greeting no longer has to be fully dynamic or fetch that greeting from the browser.
Next.js 15 previewed PPR in canary builds behind experimental.ppr plus a per-route experimental_ppr = true export. Next.js 16 removed both, with a codemod to strip the export, and enabling cacheComponents now turns PPR on for every App Router route. PPR in 16 behaves differently from the 15 canaries, so an app on that preview needs a migration, not a flag rename.
During prerender the fallback is written into the shell in place of the child that could not finish. Visitors see it at once, then the real content replaces it as it streams in. Make the fallback roughly match the final layout to avoid a shift, keep it deterministic, and never pass {children} through it, or dynamic reads below leak into what should be static.
With Cache Components, an uncached fetch, a database call, or a runtime read such as cookies() or an unknown params outside any <Suspense> stops next build with an error naming the route. Uncached data has three fixes in 16.4: wrap it in Suspense, cache it with use cache, or export instant = false. Runtime data cannot be cached, so only Suspense or instant = false apply.
A boundary only marks where streaming would start if something inside cannot finish. If the child just computes, imports data or reads a file synchronously, it completes during prerender and its real output, not the fallback, lands in the static HTML. A page whose only Suspense child does arithmetic still builds as a static route. Use connection() or io() to push a subtree to request time.
loading.tsx is shorthand for one boundary around the segment layout's {children}, so the whole page waits behind a single fallback and the layout itself is never covered. An inline <Suspense> can wrap just the slow widget, keeping the page's static heading and text in the shell. Uncached reads in a layout need their own boundary, since loading sits below the layout.
Anything that returns the same value on every run, such as imports, pure functions, fs.readFileSync or a synchronous node:sqlite or better-sqlite3 query, finishes at build time and is baked into the shell. That is a trap for per-request data from a synchronous driver: call connection() before the query. An async read of a fixed config file is better done once at module scope.
A random value or the current time would differ for every visitor, so Next.js will not freeze one into the shell quietly: an unguarded call fails the build. Choose explicitly. Wrap it in use cache to share one value until revalidation, or await io() (added in 16.3) or connection() inside Suspense for a fresh value per request; connection() also blocks prefetching.
With a null fallback wrapping <body> in the root layout, the shell is empty, so every request waits for the full render and no route serves instant HTML. It still builds in Next.js 16.4, but it hits every route; separate root layouts can limit it. A narrower escape, stabilized in 16.3, is export const instant = false on one page or layout, which lets just that segment block.
F. Fetching data on the server
The component awaits the response right in its body, with no useEffect or isLoading flag, and the response stays on the server unless you pass it to a Client Component. The route still waits for that request, though. To show a fallback meanwhile, wrap the component in <Suspense> or add loading.tsx; under Cache Components an uncached fetch outside any boundary fails the build.
A Route Handler called from a Server Component adds an HTTP round trip to your own server, plus an endpoint you now have to secure, just to reach code that could have run in place. Credentials and query logic in a Server Component never reach the client bundle, so query through your data layer directly. Still check authentication there, since rendering a page is not authorization.
Calls to fetch with GET and the same URL and options run once per server render pass, whether they come from a layout, a page, generateMetadata or generateStaticParams. The sharing lasts only for that render, unlike a persistent cache. It does not apply in Route Handlers, which sit outside the React tree, and passing an AbortController signal opts a call out.
An ORM call is not fetch, so two components asking for the same user hit the database twice unless the function is wrapped in React's cache. Calls with the same arguments during one request then share a single result, so you can fetch where data is used instead of drilling props. The memo lives for one request only, is never shared across users, and is isolated inside use cache scopes.
Two await lines in a row mean the second request does not start until the first resolves, even when they are independent. Start both, then await Promise.all([a, b]), and the slowest call sets the total time. If one rejection should not sink the rest, use Promise.allSettled. Separate layouts and pages already render in parallel; waterfalls usually hide inside one component.
Call the data function without await before slower work, such as an availability check, then call it again where the result is needed. The second call reuses the in-flight request only if the function dedupes: fetch does on its own, an ORM call needs React.cache, and under Cache Components use cache or use cache: private does the job. Keep the preload next to its consumer.
Start the request in a Server Component and pass the pending promise down as a prop; the Client Component reads it with use() and suspends to the nearest boundary until it resolves. The fetch begins on the server before any client code runs, so there is no second request after mount. Whatever the promise resolves to must still be serializable.
Next.js 15 reversed the old default: a fetch with no options is no longer stored in the server cache. Without Cache Components, though, a route with no request-time APIs is still prerendered, so that uncached call runs once at build and its result is frozen into the static HTML. Opt in per request with cache: 'force-cache', or, with Cache Components on, wrap the call in use cache.
The query string is unknown at build time. In the previous model, awaiting searchParams anywhere in a page made the whole page dynamic. With Cache Components only the components that read it wait for the request, as long as a <Suspense> boundary sits above them, so pass the promise down instead of awaiting it at the top and the heading and layout stay static.
G. Caching with Cache Components and use cache
Next.js 16 promoted it to a top-level option, folding in experimental.dynamicIO and experimental.useCache. The 16.4 docs pair it with partialPrefetching: true, and in 16.4 create-next-app enables both under its recommended defaults. Turning it on in an existing app is not a rename: uncached data outside Suspense becomes a build error, and the Node.js runtime is required.
At file level it caches every export; at the top of an async component it caches the rendered output, keyed by props; at the top of an async function it caches the return value. A page or layout file is a module too, so a file-level directive caches that segment, and a whole route needs it in every segment file it renders. Whatever the scope, the function must be async.
Put the directive at the top of a module and each exported function becomes a cached one, so a synchronous export there is rejected. That includes framework exports in the same file, such as generateMetadata and generateStaticParams, which must then be async too. Move uncached helpers to another module, or mark individual functions instead of the whole file.
A cached data function such as getProducts() can feed many components that render it differently, and it is invalidated by the tags set on it. A cached component stores its rendered JSX keyed by its props, which skips both the query and the render on a hit but ties freshness to that one view. Data-level suits shared data; UI-level suits a self-contained widget.
The key combines the build ID (or deploymentId when set), a secure hash of the function's location and signature, its serialized arguments or props, and in development an HMR hash. Because the build ID is part of it, entries never carry over from one deploy to the next, even with a durable remote handler. Only root params the function actually reads join its key.
If a cached function defined inside a component reads userId from the enclosing scope, Next.js binds that value in as a hidden argument, giving each user a separate entry. That keeps results correct, but a wide captured value multiplies entries. Pass what you depend on as explicit arguments so the cache contract is visible. unstable_cache behaved the other way and needed such values listed in keyParts.
Arguments follow the stricter rules React uses for Server Function arguments: primitives, plain objects, arrays, Date, Map, Set and typed arrays are fine, but a class instance, a plain function, a symbol or a URL object is not. Pass url.href rather than the URL, and plain data rather than an ORM model instance. JSX may come in only as an untouched pass-through.
Return values use the looser rules for props sent to Client Components, which include JSX elements. So a cached component can return rendered markup even though it could not take that markup as an argument and inspect it. Everything returned is still serialized into the entry, so a class instance or a function is rejected as a return value, the same as for arguments.
A cached component may accept children or a Server Action and place it in its returned JSX without that slot affecting the cache key, as long as the body never reads, maps or calls it. The cached wrapper is reused while the passed-in part renders fresh each time. That is also why a cached layout does not cache the pages it wraps; each needs its own directive.
A cached result may be served to anyone, so request data cannot shape it from inside. Calling cookies() or headers() in the scope, or in any helper it calls, throws next-request-in-use-cache, and on a dynamic route that can pass next build yet fail under next start. Read the value outside and pass it in so it joins the key, or use use cache: private.
use cache runs its body in its own React.cache scope, so a value memoized or primed outside, such as a current user stashed in a cached getter, is not visible inside, and the cached function computes it afresh. This keeps an entry independent of whoever happened to trigger it. Pass the data in as an argument, which also puts it in the key where it belongs.
Without a configured handler, entries live in an in-process LRU store. On a long-running self-hosted server they persist across requests, and you size the store with cacheMaxMemorySize. On serverless each request may land on a fresh instance, so runtime entries are often lost between requests, while results captured into the static shell at build time still work.
'use cache: remote' stores entries through a cache handler such as Redis or a platform KV store, shared by every instance, which matters outside the static shell where each request runs the function. The price is a network lookup plus storage fees, so it pays only with high hit rates. Skip it for per-user keys or already-fast data, and expect entries to reset after each deploy.
'use cache: private' may read cookies(), headers() and searchParams directly, but in production its result is reused only within one request and kept in browser memory for its stale time; nothing lands in a shared server cache. It cannot use a custom handler or call connection(). Prefer passing runtime values into plain use cache; this exists for compliance cases.
While Draft Mode is on, every cached function and component runs again on each request and nothing is written back, so editors see fresh drafts without any change to caching code. Inside a cached scope you may read draftMode().isEnabled, but cookies() and headers() stay off limits, and calling enable() or disable() there throws.
H. Revalidation and invalidation
As of Next.js 16.4 the presets are seconds, minutes, hours, days, weeks and max, plus default (5 minutes stale, 15 minutes revalidate, no expiry), which applies whenever a scope calls no cacheLife. You can redefine any of them or add custom names in next.config, and omitted fields inherit from default. Call cacheLife inside the cached scope, never at module scope.
stale lets the client router reuse an entry without asking the server at all. Once revalidate has passed, the next request still gets the cached copy while the server regenerates it in the background. Once expire has passed with no traffic, the next request waits for fresh content. expire must be longer than revalidate, or Next.js rejects the profile.
An entry that may change within minutes would go stale on a CDN, so Next.js leaves it out of the build output. A revalidate of 0 or an expire under 5 minutes makes a dynamic hole, and a stale under 30 seconds also keeps it out of prerenders. Of the presets only seconds trips this, via its one-minute expire. Give the hole a Suspense fallback.
Call cacheTag('posts') inside a use cache scope to label that entry; one call can attach several tags, and one tag can mark many entries. Later, revalidateTag or updateTag with that name invalidates all of them. A single call takes up to 128 tags of at most 256 characters each, and a longer tag is skipped with a warning, so revalidating it does nothing.
revalidateTag(tag, 'max') marks entries stale, and the next request for each still gets the old data while regeneration runs, so pages refresh as they are visited rather than all at once. Since Next.js 16 the profile argument is required and the one-argument form is deprecated. Passing { expire: 0 } skips the stale window. It works in Route Handlers too, so webhooks use it.
updateTag expires the tagged entries at once, so the very next read, including the re-render that ships in the action's own response, waits for fresh data. That is what a user saving a form expects. Called from a Route Handler or a webhook it throws; use revalidateTag with { expire: 0 } there. It also works in the previous caching model with fetch tags.
revalidatePath('/blog') drops everything cached for that route file, and a layout path also drops every page beneath it. Other pages showing the same data stay stale, because the call targets routes, not data. With rewrites, pass the destination path rather than the browser URL, and add a 'page' or 'layout' type for a pattern such as /blog/[slug].
Inside a Server Action, refresh() makes the response carry a newly rendered payload for the current page, so UI built from data outside the cache, such as an uncached unread count, updates in the same round trip. Server entries made by use cache stay as they are, so tagged data still needs updateTag. The client router cache is cleared, and it works only in Server Actions.
The browser keeps server-cached content for its stale time, sent in the x-nextjs-stale-time header, but never for less than 30 seconds, so a prefetched link is still usable when clicked. That floor covers only time-based expiry: calling revalidateTag, revalidatePath, updateTag or refresh from a Server Action clears the whole client cache at once.
ISR is a cached page plus a revalidation rule: serve the stored copy, regenerate in the background once it is stale, and swap in the result. In the previous model export const revalidate and fetch options drive it; with Cache Components cacheLife and tags do, and with Partial Prefetching also on, an unlisted dynamic URL gets the App Shell first. If regeneration throws, the last good page keeps serving.
I. The previous caching model (pre-Cache-Components)
Without Cache Components, a fetch with cache: 'force-cache' is looked up in the server cache by URL, method, headers and body, so even a POST can be cached, and only 200 responses are stored. Unlike use cache, entries survive a new deploy, but each instance keeps its own copy on local disk unless a shared cacheHandler is configured. Draft Mode skips it, and in development a hard refresh bypasses it.
next: { revalidate: 3600 } keeps the response for at most an hour; 0 means never cache and false means indefinitely. A value lower than the route's own revalidate shortens the whole route's interval, and two fetches of the same URL with different values settle on the lower one. Pairing it with cache: 'no-store' is a conflict, and both options are then ignored.
fetch(url, { next: { tags: ['posts'] } }) labels that response in the Data Cache, so revalidateTag('posts', 'max'), or updateTag from a Server Action, can refresh it later. The limits match cacheTag: up to 128 tags, each at most 256 characters. After moving to Cache Components, put the request inside a use cache function and tag it with cacheTag instead.
unstable_cache(fn, keyParts, { tags, revalidate }) returns a cached version of a database or other non-fetch call. Its arguments join the key automatically, but variables read from outer scope do not, so list them in keyParts. It cannot read cookies() or headers() or call connection(). Next.js 16 names use cache as its replacement, yet it still works and, unlike use cache, survives deploys.
'auto', the default, caches what it can and lets components opt into dynamic behavior. 'force-dynamic' renders on every request and treats fetches as no-store. 'error' demands a fully prerendered route and fails the build on any request-time API or uncached data. 'force-static' prerenders by blanking request data. The export disappears once cacheComponents is enabled.
force-static keeps a route prerendered by feeding it empty request data instead of failing: no cookies, no headers, no query. Code that branches on a session cookie therefore renders the logged-out version for everyone. dynamic = 'error' is the strict sibling that fails the build instead. Time-based and on-demand revalidation still work on a force-static route.
export const revalidate = 3600 sets the default lifetime for a page or layout, and across one route the lowest value among its layouts and page wins, so a child never outlasts its parent. A single fetch with a lower value pulls the whole route's interval down with it. A value of 0 forces dynamic rendering. It must be a literal: 600 works, 60 * 10 is rejected.
With cacheComponents on, these exports are not supported, so migrate rather than keep them. Uncached work already runs at request time, which replaces force-dynamic and revalidate = 0; use cache with cacheLife replaces a positive revalidate and cached fetch defaults. Next.js 16.4 adds ensureStatic to enforce static output, and also wants any runtime export removed.
The compiler reads these exports from the file's source without running it, so each value must be a literal: revalidate = 600 works, while 60 * 10, an env var or a function call does not. They configure the whole segment and cannot react to the incoming request. For per-request behavior, use request-time APIs or per-fetch options instead.
J. Mutations: Server Actions and forms
Inline, the directive goes at the top of an async function body; at file level it turns every export into a server-callable function, so each must be async. A Client Component cannot define one inline and must import it from a 'use server' file. Each export is reachable by a direct POST, even one your UI never imports, so keep internal helpers out of 'use server' files.
React lets <form action={fn}> take a Server Action, and Next.js renders it so the browser can submit a real POST. From a Server Component the form works even with JavaScript off; in a Client Component, submissions made before hydration are queued and that form is hydrated first. After hydration the submission runs without a page reload, inside a transition.
Every field arrives as a string or a File, keyed by its name, so formData.get('qty') is '3', not 3, and an unchecked checkbox is absent altogether. Convert and validate on the server, for example with a schema library, before use. Object.fromEntries(formData) is handy but also carries internal keys that start with $ACTION_.
updateUser.bind(null, userId) returns an action whose first argument is the id, with FormData arriving last; under useActionState the order becomes id, previous state, then form data. Unlike a hidden input, the value does not appear as plain text in the HTML, and binding keeps progressive enhancement. Still check that the caller may change that record, as with any client input.
const [state, formAction, isPending] = useActionState(action, initialState) gives the action a new first parameter, the previous state, and keeps whatever it returns for the next render, such as field errors. Pass formAction to <form action> and the form still submits without JavaScript. When the action is a Server Function, the initial state must be serializable.
useFormStatus() reports pending, plus data, method and action, for the nearest form above the calling component. Call it in the same component that renders the <form> and it sees nothing; it has to live in a child, such as a SubmitButton placed inside the form, which is how one reusable button can disable itself during any submission.
useOptimistic overlays a guessed value while an action runs inside a transition, such as a filled heart or a new list item. When the action ends, React drops the overlay and shows the real state; if the server write failed and nothing changed, that looks like a revert. Calling the setter outside an action makes the value flash and vanish at once.
When the action calls updateTag, revalidatePath or refresh, or changes a cookie, Next.js re-renders the current route within the same POST and streams both results back in one Flight response, which the client commits like a navigation. No follow-up fetch is needed to see the updated page. Without such a trigger, only the return value comes back.
Setting or deleting a cookie re-renders the current page, since the UI may depend on it, and redirect streams the destination page instead. Returning data alone updates only what reads the result, so a list on the page stays old until something invalidates it. Because redirect throws, call any revalidation before it when the destination needs fresh data.
With a stale-while-revalidate profile such as 'max', the action marks the tag stale and returns without re-rendering the page, so the user who made the change may still see old content. The fresh version shows up on a later read, once regeneration has finished. When the person acting must see their own write, call updateTag in the action instead.
The client queues Server Actions: a second one waits for the first to finish, so the re-rendered tree always matches the result that produced it. Promise.all over three actions therefore runs them back to back. The server itself does not queue them. For parallel work, do it inside one action, fetch in parallel from a Server Component, or call a Route Handler.
redirect() throws a NEXT_REDIRECT error and is typed never, so nothing after it runs. Call it outside a try block, or a broad catch swallows the navigation. In a Server Action it defaults to a push history entry and performs a client navigation, and a form submitted without JavaScript gets a 303. Elsewhere it is a 307 with replace.
The compiler swaps an action's body in the client bundle for an ID that POSTs back to the server, and anyone can send that POST without your UI. Next.js compares Origin with Host, caps the body at 1MB by default and encrypts IDs and closure values, but none of that is authorization. Authenticate, authorize and validate inputs inside every action.
In React 19, once an action passed to <form action> succeeds, React resets every uncontrolled field in that form. Returning a validation error still counts as success, so the user's typing is wiped unless you send the values back and render them as defaultValue. Controlled inputs keep their React state, so you clear those yourself.
K. Route Handlers and the request/response layer
Export GET, POST, PUT, PATCH, DELETE, HEAD or OPTIONS by name; a request using a verb you did not export gets 405 Method Not Allowed. If you skip OPTIONS, Next.js answers it for you with an Allow header listing your methods. Route Handlers live only in app, replace pages/api, and take no part in layouts or client navigation.
The first argument is a NextRequest, a Web Request with nextUrl and cookies helpers, and you return a Response or NextResponse. Read bodies with await request.json(), .text() or .formData(); there is no bodyParser config. A Web body is a stream, so it can be read only once, and Express-style res.status() does not exist.
Next.js 15 flipped the default for GET handlers from static to dynamic. In the previous model, export const dynamic = 'force-static' caches a GET response at build time while other verbs never cache. With Cache Components the rule differs: a GET prerenders on its own unless it touches request or uncached data, and the dynamic export is gone.
Caching applies only to a GET response; POST, PUT and the other verbs usually change data or depend on a body, so they always run. Put GET and POST in one route.ts with force-static, and the GET is prerendered while the POST still runs on every request. The cached GET can then serve stale data right after the POST succeeds, unless the POST revalidates it.
With Cache Components on, next build runs each GET handler and stores the result if it finishes without request-time input. It stops at network or database calls, request.url or request.headers, cookies(), headers(), connection() or Math.random(), and the handler then runs per request instead. Move slow but shareable reads into a cached helper to keep the response static.
The directive is not allowed at the top of a GET function in route.ts. Move the query into a separate async function marked 'use cache', set cacheLife or cacheTag there, and call it from the handler. The cached result can then be part of the prerendered response, and it revalidates according to its profile when a new request arrives.
Next.js 15 still tolerated synchronous calls as a stopgap, but Next.js 16 requires the await, so write (await cookies()).get('theme'). The object headers() returns is read-only: to send a header back, return a Response that carries it. Calling either one also keeps a GET handler from prerendering under Cache Components.
In a handler, (await cookies()).set() from next/headers writes a Set-Cookie header on the outgoing response, and so do NextResponse.cookies.set() and a manual header. request.cookies.set() only edits the incoming request object in memory, so the browser never hears about it. Setting cookies while a Server Component renders is not supported, because streaming has already begun.
Pass a ReadableStream as the Response body and each chunk you enqueue is flushed to the client as it is produced, which is how token-by-token AI output is delivered. Close the stream when done, or the response never ends. A proxy or CDN that buffers responses can still hold chunks back, so check your hosting when output arrives all at once.
A browser calling your handler from another origin needs Access-Control-Allow-Origin and related headers, and Next.js sends none by default. The automatic OPTIONS reply only lists Allow, so a preflight still fails until you add CORS headers. Set them on the handler's Response, for many routes at once in the headers() option of next.config, or in proxy.ts.
after(callback) from next/server runs once the response or prerender has finished, even after an error, notFound() or redirect(). It is not a request-time API: on a static page the callback runs at build or revalidation. In a Server Component, read cookies() before and pass the value in, since calling it inside the callback throws. Serverless hosts need waitUntil support.
L. Proxy (formerly Middleware)
The request-time job is unchanged: rewrite, redirect, edit headers or answer directly. What changed is the runtime. proxy.ts always runs on Node.js and cannot be switched, so an app that depends on Edge middleware keeps the deprecated middleware.ts. The middleware-to-proxy codemod renames the file and the exported function, and flags such as skipMiddlewareUrlNormalize become skipProxyUrlNormalize.
Only one Proxy is supported per project, placed in the project root or in src, level with app or pages; there is no per-folder Proxy the way there are per-folder layouts. To keep it readable, split route-specific logic into plain modules and call them from that one entry. It exports a single function, named proxy or as the default export.
Leave out matcher and Proxy runs on every request, including _next/static, _next/image and public files, so an auth redirect can block your CSS and JS. Matcher values must be literal constants because they are read at build time; a variable is ignored. A path the matcher excludes also skips Server Functions, which are posted to that same path.
Tell them apart by what the address bar shows afterward. NextResponse.rewrite() serves another route's output under the URL the user requested, which suits A/B tests and multi-tenant routing. NextResponse.redirect() answers with a 3xx, so the browser makes a second request and displays the new URL. Choose a rewrite whenever the visible URL must stay stable.
Pass modified headers as NextResponse.next({ request: { headers } }) and only your routes see them. Passing them as NextResponse.next({ headers }) sets response headers instead, which reach the browser — a leak when the value is internal. Keep added headers small, because an oversized set can trigger a 431 error from the backend server.
Proxy can run on every matched request, prefetches included, so any slow call it makes is paid on nearly every navigation. Next.js ignores cache, next.revalidate and next.tags on a fetch made there, so you cannot cache your way out. A session lookup that queries a database belongs in the data layer, beside the read it protects.
Reading a session cookie in Proxy is fast and fine for redirecting signed-out visitors, but it proves nothing about permissions. A matcher edit or a moved Server Function can silently drop that coverage, since Server Functions arrive as POSTs to the page's route. Verify the user and their right to the specific resource inside every action and data read.
M. Navigation, links and prefetching
<Link> intercepts the click, fetches only the server payload for segments that change, and keeps shared layouts mounted, so their input fields and scroll position survive. A plain <a> requests a fresh HTML document and resets client state. <Link> still renders an <a>, so attributes such as target and className pass straight through.
A <Link> that scrolls into view queues a background fetch of its route, and the scheduler favors visible links, then hovered ones, and drops links scrolled away. The behavior exists only in a production build, so judging navigation speed under next dev misleads you. Set prefetch={false} on huge link lists, such as an infinite table.
Without Cache Components, a dynamic route with no loading file is not prefetched at all, so a click waits on the server; adding loading.tsx lets Next.js prefetch the shared layouts and that skeleton. prefetch={true} fetches the full route of either kind. With Cache Components plus Partial Prefetching, a default <Link> prefetches the route's App Shell instead.
In Next.js 16.4 a prefetched static page stays reusable in the browser for 5 minutes by default, while dynamic segments are not cached at all; Next.js 15 lowered that dynamic default from 30 seconds to 0. The experimental staleTimes config changes both values. Shared layouts are never refetched per navigation either way, and back/forward caching is unaffected.
Call router.prefetch(href) from useRouter in next/navigation to warm a route that has no visible <Link>, or to prefetch only after a sign of intent. Since Next.js 15.4 an optional onInvalidate callback fires, at most once, when Next.js suspects the prefetched data is stale, so you can prefetch again. Rolling your own means you own invalidation too.
useRouter from next/router belongs to the Pages Router; inside app it throws a 'NextRouter was not mounted' error. The next/navigation version drops pathname, query and events: read the path with usePathname, the query string with useSearchParams, segments with useParams. While migrating shared components, next/compat/router avoids the throw.
router.push and a default <Link> add a history entry; router.replace and <Link replace> overwrite the current one. Use replace for steps nobody should return to, such as a post-login redirect or a locale switch, and push for real destinations. Both stay client-side, and both can skip scrolling: scroll: false for the router, scroll={false} on a link.
Both hooks need a Client Component, because Server Components cannot read the current URL; a page receives params as a prop instead. usePathname returns the path without its query string, and useParams returns {} when no dynamic segment exists, with catch-all segments as arrays. With Cache Components, a param unknown at build time makes them suspend, so add a Suspense boundary.
On a prerendered route the query string does not exist at build time, so the client tree up to the nearest Suspense boundary renders in the browser instead. With no boundary at all, Next.js 16.4 fails the production build with a missing-Suspense error rather than ship a page that stays blank until JavaScript loads. next dev renders on demand and hides the problem.
Next.js scrolls only when needed: if the new page's first element is already in view, the scroll position stays; otherwise that element is scrolled to the top. Sticky and fixed elements are skipped when choosing the target, so content can land under a sticky header; CSS scroll-padding-top fixes that. scroll={false} or router.push(href, { scroll: false }) turns the behavior off.
useLinkStatus returns { pending } and only works in a component rendered inside a <Link>. It reports nothing when the route was already prefetched, so it mainly helps prefetch={false} links or dynamic routes lacking loading.js. Render a fixed-size hint and fade it in after a short delay, so quick navigations neither flash nor shift the layout.
React's <ViewTransition> gives an element an identity through name; when the old and new pages share a name, the browser morphs it between positions. Route navigations are transitions, so this activates on its own, and the transitionTypes prop on <Link> (Next.js 16.2) tags direction. Put enter and exit wrappers in each page.tsx, because a layout persists and never enters or exits.
N. Errors, not-found and interrupts
React error boundaries exist only on the client, so error.tsx needs 'use client'. Besides error, it receives retry, stable since Next.js 16.3, which re-fetches and re-renders the segment, and reset, which re-renders without re-fetching; prefer retry for transient failures. A Client Component may throw a non-Error value, so narrow error before reading digest.
Within a segment, error.tsx wraps page, loading, not-found and nested layouts, but sits inside that same segment's layout and template. A crash in the layout therefore escapes to the parent segment's boundary, and in the root layout only global-error.tsx can catch it. To guard a particular layout, put an error.tsx one level up.
global-error.tsx lives in the root app folder and replaces the root layout while it renders, so your global stylesheet, fonts and theme class are absent unless you import or apply them inside it. Being a Client Component, it cannot export metadata; use React's <title> element. Since Next.js 15.2 it also shows during development.
notFound() has a never return type, so return notFound() is unnecessary and TypeScript keeps the checked value narrowed afterward. Because it works by throwing, a surrounding try/catch swallows it, and a call inside an un-awaited promise renders nothing. Before streaming starts the response is a 404; after streaming starts it stays 200 with a noindex tag.
Since Next.js 13.3, app/not-found.tsx covers explicit notFound() calls and URLs that match no route, rendering inside the root layout. An app with several root layouts, or with its root layout under a dynamic segment like [country], cannot build one consistent 404 that way; the experimental global-not-found.js (Next.js 15.4, experimental.globalNotFound) renders a full document instead.
Development shows the real error.message, which hides the production behavior. In a production build, an error thrown in a Server Component reaches error.tsx as a generic message plus error.digest, a hash you search for in the server logs. Errors thrown in Client Components keep their message. Do not branch UI logic on server error text.
Validation failures and missing records are normal outcomes, so model them as return values: a Server Function returns { error } and useActionState displays it. Throwing them instead sends the user to an error boundary that replaces the form and its state. Error boundaries also miss errors in event handlers and async code after render, which you catch and store in state yourself.
notFound(), redirect(), permanentRedirect(), forbidden() and unauthorized() all work by throwing special errors, and request-time APIs can throw one when a route must stay static. Call unstable_rethrow(error) as the first line of the catch so those pass through, then handle only your own failures. Put cleanup before that call or in finally.
Both still require experimental.authInterrupts in Next.js 16.4. unauthorized() means not signed in and renders unauthorized.tsx with a 401; forbidden() means signed in but not allowed and renders forbidden.tsx with a 403. Each throws, so execution stops, and neither may be called in the root layout. Once streaming has started, the status stays 200.
O. Metadata, SEO and OG images
A metadata object suits values known without request data, such as a fixed title or description. It works only in Server Component layouts and pages, and one segment cannot export both metadata and generateMetadata. Next.js always adds the charset and viewport tags, so you never declare those, and it writes the resulting tags into the document head for you.
generateMetadata receives the route's params, plus searchParams in a page, as Promises since Next.js 15, so you await them, and a parent argument holding outer segments' resolved metadata. Its fetch calls are memoized with the page's, and React cache dedupes other reads. On a dynamic page the tags can stream after the UI, except for bots that need them in the head.
Metadata resolves from the root layout down to the page, and a later segment replaces an earlier key whole. If a page sets openGraph with only a title, the layout's Open Graph description and images vanish rather than merging in. To share nested fields, export them from a module and spread them into each segment, or extend await parent inside generateMetadata.
Set metadataBase once in the root layout, usually to your production origin, and every URL-based field below it can use a relative path. A relative path with no metadataBase is a build error, and a field already holding an absolute URL ignores it. A leading / in a field still resolves from the end of metadataBase, so a base path survives.
A title.template such as '%s | Acme' applies only to child segments, never to the segment that defines it, so a template in page.tsx does nothing. A layout that sets a template must also set title.default, used wherever a child gives no title. A child's title.absolute ignores every parent template.
Placing opengraph-image.png, icon.png, robots.txt or sitemap.xml in app makes Next.js emit the matching tags with the right type and size, and these files override a conflicting metadata field. A file deeper in the tree wins for its own route. The code versions, like opengraph-image.tsx, are special Route Handlers cached unless they read request data.
ImageResponse from next/og turns JSX with inline styles into a PNG using Satori and Resvg. Only flexbox and a CSS subset work, so display: grid fails, and fonts must be ttf, otf or woff. The whole bundle, fonts and images included, is capped at 500KB. Inside opengraph-image.tsx it renders at build time unless the code reads request data.
Since Next.js 14, viewport, themeColor and colorScheme inside metadata are deprecated. Export a static viewport object, or generateViewport when values depend on data, from a Server Component layout or page, never both in one segment. Unlike metadata, the viewport cannot stream, so one that depends on the request blocks the page until it resolves.
app/sitemap.ts default-exports a function returning entries and app/robots.ts returns rules; Next.js serializes them to XML and plain text. Both are special Route Handlers cached by default, so a database read inside runs at build time unless the file uses a request-time API or dynamic config. A large site can split its sitemap with generateSitemaps.
Render a plain <script type="application/ld+json"> in the page or layout containing JSON.stringify output; next/script is meant for executable code, not data. JSON.stringify does not escape HTML, so a value containing </script> could inject markup — replace < with \u003c first. Check the result with Google's Rich Results Test or the Schema Markup Validator.
With Cache Components on, a generateMetadata that reads cookies(), headers(), params or uncached data defers to request time. If the rest of the page fully prerenders, Next.js raises an error so the choice is deliberate: wrap the metadata in use cache when it needs only external data, or add a dynamic marker to the page when it needs request data.
P. Images, fonts, scripts and assets
width and height give the intrinsic pixel size, used only to derive the aspect ratio; CSS still decides the rendered size. Both are required unless the image is statically imported or uses fill. A fill image stretches to its parent, which must be positioned relative, fixed or absolute, and objectFit chooses between cropping and letterboxing.
import hero from './hero.jpg' hands Next.js the file's width and height and, for a jpg, png, webp or avif that is not animated, a tiny generated blurDataURL. The blur still appears only when you set placeholder="blur". The imported file is also content-hashed, so it is served with a long-lived immutable cache header.
Without sizes, the browser assumes the image spans the full viewport, and a fill image gets a srcset built only from deviceSizes. A card one third of the screen wide then downloads a file sized for the whole screen. Write sizes to match your CSS, such as (max-width: 768px) 100vw, 33vw, and Next.js generates the fuller srcset.
An external src is optimized only if it matches an entry in images.remotePatterns; anything else gets a 400. Spell out protocol, port and pathname as well as the hostname, because any of them left out acts as ** and lets strangers use your optimizer. images.domains has been deprecated since Next.js 14 because it cannot restrict protocol, port or path.
Next.js 16 changed the images.qualities default from any value to only [75]. A quality prop outside the list is not an error: it is coerced to the closest allowed entry, so quality={90} quietly serves 75, with a warning only in development. A direct optimizer request for an unlisted quality gets a 400, so list every value you use.
next/image lazy-loads by default, which delays the Largest Contentful Paint image. Next.js 16 deprecated priority; its replacement preload inserts a <link> into the head, though the docs recommend loading="eager" or fetchPriority="high" in most cases. Avoid preloading several candidates whose LCP role depends on the viewport.
unoptimized serves the source file unchanged, sensible for SVGs, GIFs or images under about 1KB, and images.unoptimized applies it everywhere. A loader prop or images.loaderFile instead builds a CDN URL from src, width and quality. A loader is how next/image still optimizes under output: 'export', and a loader prop needs a Client Component.
An optimized image's max age is whichever is larger: minimumCacheTTL or the source image's own Cache-Control max-age. Next.js 16 raised the default from 60 seconds to 4 hours (14400). There is no invalidation API, so a long TTL can only be beaten by changing the src or deleting <distDir>/cache/images.
next/font/google downloads the CSS and font files during the build and serves them from your own domain with your static assets, so visitors' browsers never contact Google. It also generates an adjusted fallback font (adjustFontFallback) to limit layout shift. A font is preloaded only on routes that use it, so loading it in the root layout preloads it everywhere.
For Google fonts, subsets names the subsets that get a preload link. Omit it while preload is on, the default, and Next.js 16.4 behaves by bundler: webpack fails the build with a missing-subsets error, while Turbopack builds fine and preloads no font file. display defaults to 'swap', which shows fallback text first and swaps when the font arrives; 'optional' lets the browser keep the fallback when the font is slow.
Next.js rewrites each next/font call at compile time, so the result must be assigned to a const at module level with every option written as a literal; a variable, a function call or a loader inside a component fails the build. Load each font once in a shared file and import that object, because every call creates a separately hosted instance.
afterInteractive is the default and loads once some hydration has happened, which suits tag managers. beforeInteractive is injected into the initial HTML head, must sit in a root layout, and does not block hydration. lazyOnload waits for browser idle time, fit for chat widgets. worker is experimental and works only in the Pages Router.
alt should state what the image conveys, as text that could replace it without changing the page's meaning, and should not repeat a nearby caption. For a purely decorative image, alt="" tells screen readers to skip it. Leaving the prop out is a different thing: in TypeScript it is a type error, because ImageProps declares alt as a required string.
Q. Styling
Next.js renames each class in a .module.css file to a unique generated name, and the component reads it as styles.button, so two files can both define .button without clashing. Wrapping a selector in :global(...) deliberately opts it out of renaming, and collisions return for that rule. Use modules for component styles Tailwind utilities do not cover.
You can import a global stylesheet from any layout, page or component in app, but its rules apply to the whole document and are not removed when you navigate away, so styles loaded on one route can leak into the next. Keep global files for truly global rules, like resets and Tailwind's base layer, and import them in the root layout.
The Next.js 16.4 docs set up Tailwind v4: install tailwindcss and @tailwindcss/postcss, register that plugin in postcss.config.mjs, and @import 'tailwindcss' in your global CSS. v4 has no content array; it scans project files except gitignored ones, node_modules, binaries and CSS. Class names assembled at runtime, like bg-${color}-600, are never generated.
Runtime CSS-in-JS libraries such as styled-components produce styles from JavaScript during rendering, and Next.js supports them only in Client Components. Setup takes a style registry that collects rules during a render, useServerInsertedHTML to flush them into the head ahead of streamed content, and a client wrapper around the root layout's children. Emotion was still adding support as of Next.js 16.4.
In production, Next.js merges CSS into chunks ordered by when each file is first imported, so moving one import can flip which equally specific rule wins. Development may order files differently, which is why a conflict can surface only after next build. Keep CSS imports in one entry file where possible, and turn off linters that auto-sort imports.
R. Configuration and environment
next.config.ts is a regular Node module loaded by the build and by the server, and none of it enters the browser bundle. It is not passed through webpack or Babel, so syntax your Node version lacks fails there, and a TypeScript config resolves as CommonJS unless Node's native TypeScript support is on. A function export receives the current phase.
Lookup stops at the first hit: process.env, then .env.$(NODE_ENV).local, .env.local, .env.$(NODE_ENV), and finally .env. A variable already set in the shell therefore beats every file. Under NODE_ENV=test, .env.local is skipped so tests behave the same for everyone. With a src folder, the files still belong at the project root.
next build replaces each literal process.env.NEXT_PUBLIC_X in client code with its value at that moment, so the string ships in your JavaScript; never put a secret behind the prefix. A dynamic lookup such as process.env[name], or reading through an alias of process.env, is not inlined and stays missing in the browser. Unprefixed variables exist only on the server.
A NEXT_PUBLIC_ value is frozen into the bundle by whichever environment ran next build, so promoting one Docker image from staging to production keeps staging's value. Read per-environment settings through plain process.env on the server during a dynamic render, then hand the client what it needs through props or an endpoint.
redirects returns { source, destination, permanent } entries: permanent: true sends a 308, which browsers and search engines cache, and false sends a 307. Both codes keep the request method, unlike 301 and 302. Config redirects run before Proxy and before the filesystem, so a fixed mapping needs no code. rewrites remap a path without changing the URL.
headers() in the config returns { source, headers } pairs applied before the filesystem, so they cover pages and public files alike, which suits HSTS, X-Content-Type-Options and Referrer-Policy. When two rules set the same key, the last one wins. A nonce-based CSP changes per request and needs Proxy instead, and hashed _next/static assets keep their own Cache-Control.
basePath: '/docs' prefixes every route and is applied to <Link> and router calls automatically, but a next/imagesrc must include it by hand. It is inlined at build time, so changing it needs a rebuild. assetPrefix points only /_next/static requests at a CDN host; files in public keep their paths unless you prefix them yourself.
The App Router bundles the dependencies of Server Components and Route Handlers by default. A package relying on Node-specific features, such as a native addon, can break when bundled, so list it in serverExternalPackages to load it with Node's own require. Next.js already exempts a built-in list that includes bcrypt, prisma, puppeteer and sharp.
Next.js does not compile code inside node_modules, so a dependency shipping raw TypeScript or JSX fails until you list it in transpilePackages, by package name only, with no paths or globs. Turbopack already compiles local workspace packages in both routers. A package cannot appear in both transpilePackages and serverExternalPackages; the build throws at startup.
With typedRoutes: true, a stable top-level option in Next.js 16.4, plus TypeScript, Next.js writes route types into .next/types, so a typo in a literal href fails type-checking. In the App Router, push, replace and prefetch are checked too. A computed string needs a cast with as Route, which skips the check for that link.
S. Runtimes, build output and deployment
The Node.js runtime has the full Node API, filesystem included, and renders your app unless told otherwise. The Edge runtime offers only Web APIs such as fetch, Request and TextEncoder, so many npm packages fail there. Edge has been deprecated since Next.js 16.3, Proxy cannot use it, and Cache Components does not support a runtime export at all.
In practice a segment exporting runtime = 'edge' loses both timed and on-demand revalidation of its pages, because ISR runs only on the default Node.js runtime. Edge has been deprecated since Next.js 16.3, so the usual fix is to delete the export rather than work around it; the Node.js runtime supports ISR, and Cache Components requires it.
In Next.js 16.4, export const runtime = 'edge' still opts a segment into Edge, but the build logs a deprecation warning (added in 16.3) and the advice is to delete the export. 'experimental-edge' was deprecated back in Next.js 15. Proxy files may not set runtime at all, and with Cache Components even runtime = 'nodejs' must go.
next build marks each route with a symbol. Without Cache Components the legend is Static (whole route prerendered), SSG (paths from generateStaticParams or getStaticProps) and Dynamic. With Cache Components, App Router pages show Static, Partial Prerender (a static shell plus streamed content) or Dynamic, meaning an empty prerender. An unexpected Dynamic points to a request-time API or missing params.
output: 'standalone' traces the files each route needs and copies them, with only the required node_modules, into .next/standalone alongside a minimal server.js. It does not copy public or .next/static, so add those yourself or serve them from a CDN. Set the PORT and HOSTNAME environment variables to change where server.js listens.
output: 'export' writes plain HTML, JS and CSS with no server, so anything needing a request is gone: Server Actions, Proxy, cookies, ISR, config redirects, rewrites and headers, and default-loader image optimization. Route Handlers survive only as GET handlers rendered to files at build. Dynamic routes need generateStaticParams, and a custom loader restores image optimization.
By default each next start instance keeps ISR pages and fetch results on its own disk and in memory, so replicas drift apart and a revalidateTag on one leaves the others stale. A cacheHandler backed by Redis or S3 shares entries; an in-process Map does not. For use cache, configure cacheHandlers and implement refreshTags to sync invalidations.
Each next build gets a new build ID, and use cache keys include it, so a deploy starts those entries empty, even ones in a remote handler; a configured deploymentId takes the build ID's place in that key. The fetch cache and unstable_cache are the stores documented to persist across deploys. Containers built separately must agree on one ID through generateBuildId.
Next.js tells a CDN what to keep through Cache-Control: a one-year s-maxage for static pages, s-maxage plus stale-while-revalidate for ISR, and private with no-store for dynamic pages. revalidateTag and revalidatePath clear only the Next.js server cache; the CDN serves its copy until s-maxage expires unless you also purge both the HTML and RSC variants.
Next.js 16 requires Node.js 20.9 or newer, and its own package.json declares that in engines, but Next never installs or selects Node. The version that actually runs is whatever your Docker base image, CI setup step or host setting provides, so pin it in those places. npm only warns at install time, but the next command itself exits with an error below 20.9.
T. Security and data protection
A Data Access Layer is a server-only module whose functions check the session and the user's right to each record, then return trimmed objects. Pages and actions call it instead of the database, so a new route cannot forget a check, and only this module reads process.env secrets. A security review then examines one directory instead of every component.
Add import 'server-only' at the top of a module holding secrets or database code. If a Client Component imports it, directly or through a chain of imports, the build fails instead of shipping that code to the browser. Next.js handles the import itself, so installing the npm package only quiets lint rules. client-only guards the opposite direction.
Props passed to a Client Component and values a Server Action returns are serialized into the response the browser receives, so a full user row ships its password hash and email even when the UI shows only a name. Return a small object holding exactly the fields the UI renders. Functions and classes cannot cross that boundary at all.
Enable experimental.taint, then call experimental_taintObjectReference on a whole object or experimental_taintUniqueValue on a string such as an API key; React throws if either reaches a Client Component. Tracking follows the reference only, so a spread copy or a value derived from it is untainted. Treat taint as a backstop behind trimmed DTOs.
Server Actions accept only POST and compare the host in the Origin header with the app's host from x-forwarded-host or host, aborting on a mismatch, in production as well as development. If the Origin header is missing entirely, Next.js lets the request proceed and logs a warning. Behind a proxy that forwards its own host, list the public host in serverActions.allowedOrigins.
A Server Action request body is capped at 1MB by default to limit parsing cost and denial-of-service risk; raise it with serverActions.bodySizeLimit, in bytes or as a string like '3mb'. The cap counts the raw body, so multipart boundaries and part headers use some of it; leave roughly 10 to 20KB of headroom for uploads near the limit.
An action defined inside a component can capture render-time values, and Next.js sends those values to the browser encrypted with a key generated fresh for each build. Replicas built separately cannot decrypt each other's payloads and fail with 'Failed to find Server Action'. Set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, a base64 AES key of 16, 24 or 32 bytes, identically everywhere.
Proxy creates a random nonce per request and puts it in the Content-Security-Policy header; while rendering, Next.js reads it from the 'nonce-...' source and stamps it on framework and page scripts. Static HTML is built with no request, so every page using a nonce must render dynamically, giving up static output and CDN caching, and it is incompatible with Partial Prerendering.
Each attribute stops a different attack. httpOnly keeps the cookie out of document.cookie, so an injected script cannot read it; secure sends it only over HTTPS; sameSite limits which cross-site requests carry it, which blunts CSRF. Add expires or maxAge, and set the cookie on the server through the cookies API, never from client code.
Enabling experimental.sri with sha256, sha384 or sha512 makes the build hash each JavaScript file and add an integrity attribute, so the browser refuses a file whose bytes no longer match. Because the hashes exist at build time, pages can stay static, unlike with nonce-based CSP. It works only in the App Router and cannot cover scripts generated at runtime.
U. Pages Router: the legacy contrast
getStaticProps runs during next build, in the background for ISR, and on every request under next dev, but never in the browser, so it can query a database directly. It cannot see request headers or the query string. Next.js also writes the props to a JSON file for client-side navigation, and they are visible in the page source, so leave secrets out.
With false, unlisted paths 404 and adding one needs a rebuild. With true, the first visitor gets a fallback page with empty props and router.isFallback set while the real page renders in the background; crawlers are treated as blocking. With 'blocking', the first request waits for full HTML, which is then cached. Static export supports neither true nor 'blocking'.
getServerSideProps runs on every request, server-side only, and can read headers, cookies and the query; on client navigation Next.js fetches its result as JSON. That makes it the slowest option, so set a Cache-Control header inside it, or prefer getStaticProps with ISR when data is not per-user. One page cannot export both functions.
With revalidate: 60, requests inside the window get the cached page; the first request after it still receives the stale page while regeneration starts in the background, and later requests see the new one. If regeneration throws, the last good page stays live. The x-nextjs-cache header reports HIT, STALE or MISS, and res.revalidate() forces an update.
Props travel to the browser as JSON, so Next.js checks them in development, and at build time for getStaticProps, throwing a serialization error for a Date, undefined, a class instance or a circular reference. Convert a date with toISOString() and rebuild it on the page, and use null where you meant undefined. Whatever you return is also readable in the page source.
pages/_app receives Component and pageProps and stays mounted across navigations, so shared layout, global CSS and router listeners belong there; giving it getInitialProps disables automatic static optimization. pages/_document renders only on the server, requires Html, Head, Main and NextScript, and cannot use event handlers like onClick.
Any file in pages/api becomes an /api/* endpoint with one default-export handler receiving Node's req and res, plus helpers like req.query, req.cookies and a parsed req.body; you branch on req.method yourself. An App Router route.ts instead exports GET or POST functions taking a Web Request. API routes cannot be used with static export.
next/router returns one object holding pathname, query, asPath, isReady and events. The App Router splits that into usePathname, useSearchParams and useParams, drops isReady because components reading search params render on the client during prerendering, and replaces routeChangeStart listeners with an effect that watches the pathname and search params.
In the Pages Router, next/head adds tags to the head from any component; give repeated meta tags a key to dedupe them, while title and base dedupe automatically. Tags must be direct children, it cannot set html or body attributes, and its tags disappear when the component unmounts. In app, the metadata export and generateMetadata replace it.
A page is prerendered to HTML when it has neither getServerSideProps nor getInitialProps. During that prerender router.query is empty and fills in after hydration, so check router.isReady before using query values. A getInitialProps on a custom _app switches the optimization off for every page that lacks getStaticProps.
app and pages can share one project so you move routes one at a time, but defining the same path in both is an error: a webpack build stops on conflicting files, and Turbopack reports both routers matching. Delete the pages file as you add the app one. Navigating between the two routers is a hard navigation, and <Link> does not prefetch across them.
V. Tooling, testing and diagnostics
Next.js 16 runs next dev and next build on Turbopack with no flag, and --webpack opts back. A custom webpack function in your config, often added by a plugin, makes the default next build fail rather than be silently ignored, so migrate it, pass --webpack, or pass --turbopack to ignore it. Platforms lacking native bindings, like FreeBSD, need webpack.
Turbopack saves compilation work under .next so restarts and rebuilds start warm: on by default for next dev since Next.js 16.1 and for next build since 16.3. It holds compiled modules, not fetched data, so clearing it never revalidates content. A CI or container build benefits only when .next/cache is restored; otherwise set turbopackFileSystemCacheForBuild: false.
Per the Next.js 16.4 guides, Jest and Vitest cannot render async Server Components, though synchronous Server and Client Components unit-test fine. Cover anything that awaits data inside a component with an end-to-end tool such as Playwright or Cypress against a running app. Moving the data logic into plain async functions lets you unit-test that part on its own.
next/dynamic wraps React.lazy and Suspense, so a Client Component's code loads only when it renders, with an optional loading placeholder. ssr: false skips its server render and is allowed only inside a Client Component; in a Server Component it is an error. Dynamically importing a Server Component lazy-loads only its client children, not the component itself.
React compares the first browser render with the server's HTML, and any difference raises a hydration error. Usual sources: new Date() in render, date formatting that depends on the runtime's time zone or locale, typeof window branches, invalid nesting such as a div inside a p, and browser extensions editing the DOM. Move client-only values into useEffect.
Server Components, Route Handlers and actions run in the Node process, so DevTools attached to the page cannot pause them. next dev --inspect passes the flag to Node; open chrome://inspect and choose the app under Remote Target. Use --inspect=0.0.0.0 inside Docker, and set --inspect-brk through NODE_OPTIONS rather than as a CLI flag.
Place instrumentation.ts in the project root, or in src if you use one. register() may be async, and the server waits for it before handling requests, which makes it the spot to start OpenTelemetry or an APM agent. Branch on process.env.NEXT_RUNTIME for runtime-specific setup, and add the optional onRequestError export, added in Next.js 15, to report server errors.
React Compiler inserts memoization at build time, reducing the need for hand-written useMemo and useCallback. Next.js 16 promoted the setting to top-level reactCompiler: true; you also install babel-plugin-react-compiler, and Next applies it only to files with JSX or hooks to limit build cost. compilationMode: 'annotation' restricts it to components marked 'use memo'.
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.