Next.js Cheatsheet - Next.js Framework Reference
For React developers building full-stack apps who must decide between SSR and SSG. The core of Next.js is not a list of APIs but clarifying who renders on the server, what interacts on the client, and when data goes stale — server components cannot attach onClick, and ISR serves cached data that can lag behind. By the end you can build a practical App Router page and layout, draw the client/server component boundary, weigh static generation vs ISR, and handle middleware and data fetching.
Typical Use Case
For engineers taking an App Router project from runs locally to behaves in production. The cost is rarely the markup but the rendering boundary: which data is fetched at build time, per request, or on the client. The sheet covers the file conventions (page, layout, loading, error, not-found), route groups, the fetch cache options, the dynamic and revalidate switches, Route Handlers and middleware, and metadata. Use it as a review checklist when a page slows down on every request.
App Router Basics 5
app/page.tsxapp/layout.tsxapp/loading.tsxapp/error.tsxapp/not-found.tsxData Fetching 6
fetch(url, { cache: "force-cache" })fetch(url, { cache: "no-store" })fetch(url, { next: { revalidate: 60 } })generateStaticParams()"use server"server-onlyRendering Strategies 5
export const dynamic = "force-static"export const dynamic = "force-dynamic"export const revalidate = 3600"use client"export const runtime = "edge"Layout & Templates 5
export const metadata = { title: "Page Title" }export async function generateMetadata()app/@modal/default.tsxapp/(...)/[...slug]/page.tsxtemplate.tsxAPI & Middleware 6
app/api/route.tsexport async function GET(request)middleware.tsNextResponse.redirect(new URL("/login", request.url))NextResponse.next()cookies().get("token")Static Assets & Optimization 5
import Image from "next/image"import { Inter } from "next/font/google"import Script from "next/script"export const metadata = { robots: { index: true, follow: true } }<Link href="/about" prefetch={true}>Deployment & Configuration 5
output: "standalone"output: "export"process.env.NEXT_PUBLIC_API_URLnext buildnext startParameter matrix
| Parameter | Effect | Example |
|---|---|---|
app/page.tsx | The only page file for route /; the file name is the URL, and one route can have only one page.tsx | app/page.tsx |
app/layout.tsx | Layout; a parent layout does not re-render when child routes change; must contain <html> and <body> | app/layout.tsx |
app/loading.tsx | Same-level Suspense fallback UI, shown automatically during route segment changes and data waits | app/loading.tsx |
app/error.tsx | Catches render errors in the same page segment and must export a client component; provides reset() to retry | app/error.tsx |
app/not-found.tsx | The 404 page thrown by notFound(); put the global 404 at the root | app/not-found.tsx |
app/(marketing)/about/page.tsx | Route group, wrapped in parentheses and not present in the URL; used to hold shared layouts without polluting the path | app/(marketing)/about/page.tsx |
app/blog/[slug]/page.tsx | Dynamic segment; [slug] is passed via params; paired with generateStaticParams to prerender all entries at build time | app/blog/[slug]/page.tsx |
app/(...)/[...slug]/page.tsx | Catch-all segment (..) and optional segment (...) ellipsis syntax, used to match multi-level paths outside app | app/(...)/[...slug]/page.tsx |
useRouter() | Get the router in a client component and navigate with router.push / replace; cannot be called in a server component | useRouter() |
<Link href="/about"> | Client-side navigation; prefetches the JS and data of subsequent routes by default; replaces a bare <a> to avoid a full page reload | <Link href="/about"> |
useSearchParams() | Read the query string; it marks the whole subtree as dynamic rendering (CSR bailout) and must be wrapped in Suspense | useSearchParams() |
fetch(url, { cache: "force-cache" }) | Force caching and reuse long-term; equivalent to statically rendering that data | fetch(url, { cache: "force-cache" }) |
fetch(url, { cache: "no-store" }) | Hit the origin on every request; the cache is used only once per request; suitable for personalized and real-time data | fetch(url, { cache: "no-store" }) |
fetch(url, { next: { revalidate: 60 } }) | ISR semantics: the cache expires after 60 seconds and re-fetches in the background on the next request | fetch(url, { next: { revalidate: 60 } }) |
generateStaticParams() | Enumerate parameters for a dynamic route at build time; every returned value is statically prerendered | generateStaticParams() |
"use server" | Marks a function as a Server Action that a client form can call directly; embedded third-party credentials do not enter the bundle | "use server" |
server-only | Module-level import guard that fails the build if referenced by a client component; used to protect secrets and DB connections | server-only |
"use client" | Declares the client boundary; this file and the modules it imports enter the browser bundle; useState/useEffect live on this side | "use client" |
export const dynamic = "force-dynamic" | Force the whole segment to dynamic rendering on every request; suitable for real-time pages; turns off all caching for the segment | export const dynamic = "force-dynamic" |
export const dynamic = "force-static" | Force static rendering; ignores dynamic APIs and freezes the result; suitable for content that never changes | export const dynamic = "force-static" |
export const revalidate = 3600 | Page-level ISR interval (seconds); simpler than setting revalidate per fetch | export const revalidate = 3600 |
export const runtime = "edge" | Switch to the Edge Runtime; lower cold start but no Node APIs; using Node-only libraries fails the build | export const runtime = "edge" |
export async function generateMetadata() | Generate a dynamic title/description from params, producing the <head> and OG tags for each route | export async function generateMetadata() |
app/@modal/default.tsx | Slot directory for Parallel Routes; default.tsx is a required soft fallback | app/@modal/default.tsx |
template.tsx | A template that remounts on every navigation; put enter animations here; the only difference from layout is whether it remounts | template.tsx |
app/api/route.ts | Route Handler entry point; export functions like GET/POST per method, replacing the pages/api style | app/api/route.ts |
export async function GET(request) | GET handler; request is a standard Web Request, and you can read the query string directly with new URL(request.url) | export async function GET(request) |
middleware.ts | Defined at the root (or under src); the matcher determines the interception scope; used for auth and rewrites | middleware.ts |
NextResponse.redirect(new URL("/login", request.url)) | Server-side redirect; the origin must be derived from request.url and must not be an absolute domain (multi-domain deployments would redirect to the wrong host) | NextResponse.redirect(new URL("/login", request.url)) |
NextResponse.next() | Let the request continue; omitting this line also continues by default, but writing it explicitly is clearer | NextResponse.next() |
cookies().get("token") | Read a cookie in a server component / route handler; reading it makes that route dynamic rendering | cookies().get("token") |
import Image from "next/image" | Built-in image component that auto-generates srcset/sizes and uses the optimization pipeline by default; forgetting width/height triggers a missing-dimensions warning | import Image from "next/image" |
import { Inter } from "next/font/google" | Self-host fonts at build time, eliminating external requests and CLS; the build fails when the network is restricted | import { Inter } from "next/font/google" |
output: "standalone" | The build output ships a standalone server directory with a trimmed node_modules; ideal for Docker images | output: "standalone" |
output: "export" | Pure static export; only for fully static sites; any Route Handler, ISR, or dynamic route will fail | output: "export" |
process.env.NEXT_PUBLIC_API_URL | Variables with the NEXT_PUBLIC_ prefix are inlined into the client bundle; changes require a rebuild and are exposed to everyone | process.env.NEXT_PUBLIC_API_URL |
next build / next start | Build and start in production mode; dev and production differ in caching and routing behavior, so diagnosing production issues requires next start | next build |
Common pitfalls
SymptomCalling useRouter / useState in a Server Component throws "useState only works in a Client Component".
CauseApp Router defaults to Server Components; only files with "use client" and their import chain have access to these Hooks.
FixExtract the minimal interactive leaf component into its own file with "use client" at the top; do not add it to the whole layout, or the entire subtree enters the bundle.
SymptomThe first-paint HTML has empty data, and the real content flashes in only after the page loads (hydration mismatch warning).
CauseThe server render used a cached value while the client used a live value, so the two sides disagree; common when reading localStorage or a random number directly.
FixSet the inconsistent part into state inside useEffect, or mark that component "use client" and keep the server and client output identical.
SymptomEven with fetch caching enabled, the API request count still equals the visit count.
Causefetch caching only applies to server rendering and static generation; dynamic APIs like cookies(), headers(), or searchParams in the request turn the whole segment dynamic and invalidate all caching.
FixMove data fetching into a separate static subsegment (no dynamic APIs) and consume it in the page via Suspense + a child component; or verify dynamic is not set to force-dynamic.
SymptomUsing useSearchParams() directly in page.tsx makes the whole page dynamic at build time and throws a missing Suspense boundary error.
CausesearchParams is a dynamic API; reading it opts that route segment out of static rendering, and without a Suspense wrapper Next cannot determine the streaming boundary.
FixMove the searchParams reading logic into a dedicated client child component and wrap it in <Suspense>.
SymptomNextResponse.redirect(new URL("https://example.com/login", request.url)) sends users to the production domain in the staging environment.
CauseThe redirect URL hard-codes an absolute domain and ignores two things: the request origin may differ from the deployment domain (e.g. a staging subdomain).
FixAlways construct it relatively with new URL("/login", request.url) so Next derives the origin from the current request.
SymptomAn API key is written into a variable a client component can read, and the key shows up in the browser Network tab.
CauseVariables with the NEXT_PUBLIC_ prefix, and modules imported into client components without the prefix, are both inlined into the bundle.
FixKeep keys server-only: guard the module with server-only and put the call in a Route Handler or Server Action.
SymptomAfter export const runtime = "edge", the build reports that some Node library is unavailable under the Edge Runtime.
CauseThe Edge Runtime only has Web APIs; it lacks fs, net, and some native npm modules.
FixSwitch back to the default Node runtime; if Edge is truly needed, keep heavy dependencies on the server and call them through a Route Handler.
Symptomnext build passes and next dev works, but the page 404s after next start.
CauseThe file sits at the wrong directory level (e.g. under pages/ instead of app/, or the app directory is neither at the root nor under src); the routing differences between dev and build mask the problem.
FixVerify the directory location and file name; use the route table from next build to confirm the path was registered.
SymptomAfter deploying to a CDN the page keeps serving stale data, and revalidate seems to have no effect.
CauseCDN cache headers take precedence over Next ISR, or with output: "export" there is no ISR at all and revalidate is ignored.
FixCheck the Cache-Control response header and CDN rules; in static-export scenarios switch to build-time data fetching or revert to a standard deployment.
Troubleshooting
1Reproduce production issues with production mode
next build && next startnext dev does not do full optimization and behaves differently in routing; diagnosing caching and build errors requires next start.
2Inspect the routes and render modes in the build output
next buildEach route in the output is labeled Static / Dynamic / ISR along with size and first-load JS.
3Confirm whether a client variable value made it into the bundle
grep -r "NEXT_PUBLIC" .next/staticFinding the plaintext inside .next/static means the variable was inlined into the client output.
4Check whether static export is viable
grep -r "export const dynamic" appAny Route Handler, ISR, or force-dynamic segment will make the output: "export" build fail.
5Check which paths the middleware actually intercepts
grep -n "matcher" middleware.tsAn overly broad matcher makes every request go through the middleware, a common source of performance problems.
6Verify that convention files are recognized by the router
ls -R appConfirm the file names and levels of page.tsx / layout.tsx; typos (like page.ts) do not error, they just produce no route.
Command Examples
A Route Handler serving GET
// app/api/items/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const res = await fetch("https://api.example.com/items", { cache: "no-store" });
return NextResponse.json(await res.json());
}The exported function name is the HTTP method, and the file path supplies the URL prefix. no-store forces a real upstream fetch on every request; without it the result is served from a stale cache. Return NextResponse.json(...) rather than a bare object so you keep control over status and headers.
ISR: revalidate in the background after 60 seconds
// app/products/[id]/page.tsx
export const revalidate = 60;
export async function generateStaticParams() {
const ids = await getIds();
return ids.map((id) => ({ id: String(id) }));
}revalidate = 60 marks the page stale after a minute and regenerates it in the background on the next visit: the first request still serves old content but returns instantly, preserving both SEO and first-paint speed. generateStaticParams decides which dynamic paths are pre-rendered at build time; omit it and those pages are only generated on first hit.
Middleware gating unauthenticated requests
// middleware.ts
import { NextResponse } from "next/server";
export function middleware(request) {
const token = request.cookies.get("token");
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}Middleware runs before a request reaches its route, making it the earliest place to redirect on auth failure. Note that NextResponse.redirect(new URL(..., request.url)) must build on the full request.url to resolve a correct absolute location; a bare "/login" throws.
Client component using navigation hooks
"use client";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
export function Nav() {
const router = useRouter();
const pathname = usePathname();
const params = useSearchParams();
return <button onClick={() => router.push("/about")}>{pathname}</button>;
}The three hooks from next/navigation are client-only; using them in a Server Component is a hard error. useSearchParams pulls the component across the client rendering boundary, so a statically exported page that reads it needs a Suspense wrapper.
Static metadata and dynamic generateMetadata
export const metadata = {
title: "Page title",
description: "Page description",
robots: { index: true, follow: true },
};
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return { title: post.title, description: post.excerpt };
}Static metadata fits fixed pages; dynamic routes need generateMetadata receiving params to fill in title and description, otherwise every detail page shares one title, which is an outright SEO defect. Do not define both in one file: generateMetadata takes precedence over the static export.
Common Pitfalls
- "use client" is file-scoped: every module imported by that file is bundled for the browser. Importing a server-only helper for its database or key handling ships that code and its dependencies to the client.
- Mixing build-time constant lookups on process.env and import.meta.env across files lets the bundler replace those accesses with literals, so client and server disagree and hydration mismatches appear as the page jumping on first paint. Read environment values the conventional Next.js way, on the server.
- Relying on directory conventions instead of the server-only marker means one wrong import path can pull key-handling logic into the client bundle. Put server-only at the top of the module so the bundler fails loudly.
- Guessing at fetch caching is expensive: the default may serve stale data, while no-store sends every request upstream and hits both latency and billing. Declare cache: "no-store" or a revalidate interval explicitly for frequently changing data.
- Heavy work in middleware.ts (database queries, third-party calls) slows every request that passes through, and the Edge Runtime does not support all Node APIs, so failures surface as errors inside someone else library. Keep middleware light and push the real work into a route or a Server Action.
- With output: "export" anything depending on the server runtime either fails the build or comes back empty: Route Handlers, middleware, server-side fetching, and on-demand generation of dynamic routes. Confirm the page really can be produced statically before choosing static export.
Tips
- App Router is the default routing solution in Next.js 13+, replacing Pages Router
- Page components are Server Components by default, add "use client" for client-side interactivity
- Use next/link Link component instead of a tags for automatic prefetching and client-side navigation
- Fetch data directly in Server Components with async/await, no useEffect needed
- Use generateStaticParams for static generation of dynamic routes
FAQ
Where do I put "use client" when a page throws a client-related error?
Under the App Router, app/page.tsx is a Server Component by default; add "use client" only when the component needs browser APIs, event handlers or state, and understand that it converts that file plus its whole import tree into client code. The heuristic: if the component uses onClick, useState or useEffect, mark the smallest possible layer — usually a leaf component — and work upward, rather than slapping the directive on the top-level page. Server Components should not fetch inside useEffect; just async/await fetch directly in the component. The opposite tool is server-only, which guarantees a module never leaks into the client bundle, keeping secrets safe.
How do I choose between static generation, ISR and dynamic rendering?
Three routes for three kinds of data. fetch(url, { cache: "force-cache" }) is the default static path — HTML is produced at build time, right for content that rarely changes. fetch(url, { next: { revalidate: 60 } }) is ISR: the page is revalidated in the background after 60 seconds, balancing speed against freshness. fetch(url, { cache: "no-store" }) is dynamic, refetching on every request for personalised or live data. At the page level you can pin the policy with export const dynamic = "force-static" or "force-dynamic", or set an ISR interval for the whole page with export const revalidate = 3600. Dynamic routes additionally need generateStaticParams() to enumerate the paths to pre-render.
Why prefer a Server Action over a Route Handler for form submissions?
Because a Server Action is a first-class capability inside your component tree: putting "use server" at the top of the function marks it as a server action, and you can pass it straight to a client form's action prop, skipping hand-written fetch calls and state bookkeeping. A Route Handler — an async function GET(request) exported from app/api/route.ts — is for external consumers: other programs calling you over defined HTTP semantics, or endpoints needing custom methods and response shapes. On the security side, server-only keeps secrets server-side, and sessions should be read with cookies().get("token") on the server rather than round-tripping a token through the client.
When does logic belong in middleware.ts rather than in each page?
middleware.ts runs before a request reaches any route, which makes it the right home for cross-cutting rules applied uniformly: auth checks, redirects, header rewrites. NextResponse.redirect(new URL("/login", request.url)) sends unauthenticated users away, and NextResponse.next() lets the request continue. Doing the same check page by page is what invites bugs — every new route is a chance to forget, and the logic ends up scattered. Middleware has the request object, so path and cookie decisions are easy; keep it cheap, though, since it runs on every request. Anything expensive belongs in a page or a Route Handler, not here.
How do static export and standalone output differ in next.config?
Pick based on the deployment target. output: "export" emits pure HTML/CSS/JS that any static host can serve, but the cost is that server-side capabilities — Route Handlers, Server Actions, ISR revalidation — stop working, so pages must be resolvable at build time. When you need a Node runtime and want a self-contained artifact, use output: "standalone", which bundles the required node_modules slice and fits Docker images neatly. Validate production behaviour locally with next build followed by next start serving the .next directory, never next dev. For environment-specific URLs use process.env.NEXT_PUBLIC_API_URL, remembering that NEXT_PUBLIC_-prefixed values get inlined into the client bundle.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us