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 | 路由 / 的唯一页面文件;文件名即 URL,一个路由只能有一个 page.tsx | app/page.tsx |
app/layout.tsx | 布局,父级布局在切换子路由时不重渲染;必须包含 <html> 与 <body> | app/layout.tsx |
app/loading.tsx | 同级 Suspense 回退界面,路由段切换与数据等待期间自动显示 | app/loading.tsx |
app/error.tsx | 捕获同级页面段的渲染错误并要求导出 client component,可拿到 reset() 重试 | app/error.tsx |
app/not-found.tsx | notFound() 抛出的 404 页面,全局 404 放根目录即可 | app/not-found.tsx |
app/(marketing)/about/page.tsx | 路由组,用括号包住且不出现在 URL 里,用来放共享布局而不污染路径 | app/(marketing)/about/page.tsx |
app/blog/[slug]/page.tsx | 动态段,[slug] 由 params 传入;配合 generateStaticParams 在构建期预渲染全部条目 | app/blog/[slug]/page.tsx |
app/(...)/[...slug]/page.tsx | 捕获所有段 (..)、可选段 (...) 省略号写法,用来匹配 app 之外的多层路径 | app/(...)/[...slug]/page.tsx |
useRouter() | 客户端组件里拿到 router,用 router.push / replace 做导航,不能在服务端组件调用 | useRouter() |
<Link href="/about"> | 客户端路由跳转,默认预取后续路由的 JS 与数据,替代裸 <a> 避免整页刷新 | <Link href="/about"> |
useSearchParams() | 读取查询串;它会把整棵子树标为动态渲染(CSR bailout),需用 Suspense 包住 | useSearchParams() |
fetch(url, { cache: "force-cache" }) | 强制缓存并长期复用,等价于静态渲染该处数据 | fetch(url, { cache: "force-cache" }) |
fetch(url, { cache: "no-store" }) | 每次请求都回源,一个请求只用一次缓存;适合个性化与实时数据 | fetch(url, { cache: "no-store" }) |
fetch(url, { next: { revalidate: 60 } }) | ISR 语义,缓存 60 秒后过期并在下次请求后台重取 | fetch(url, { next: { revalidate: 60 } }) |
generateStaticParams() | 构建期为动态路由枚举参数,返回的每个值都会被静态预渲染 | generateStaticParams() |
"use server" | 把函数标记为 Server Action,可被客户端表单直接调用;内含的第三方凭据不会进包 | "use server" |
server-only | 模块级导入守卫,一旦被客户端组件引用即构建报错,用于保护密钥与 DB 连接 | server-only |
"use client" | 声明客户端边界,此文件及其 import 的模块进入浏览器 bundle;useState/useEffect 就属于这一侧 | "use client" |
export const dynamic = "force-dynamic" | 整段强制动态渲染,每次请求都跑,适合实时页面;会关闭该段所有缓存 | export const dynamic = "force-dynamic" |
export const dynamic = "force-static" | 强制静态化,忽略动态 API 并把结果固化,适合内容不变的说明页 | export const dynamic = "force-static" |
export const revalidate = 3600 | 页面级 ISR 间隔(秒),比逐个 fetch 设置 revalidate 更省事 | export const revalidate = 3600 |
export const runtime = "edge" | 切到 Edge Runtime,冷启动更低但无 Node API;用 Node 专属库会构建报错 | export const runtime = "edge" |
export async function generateMetadata() | 按 params 生成动态 title/description,产出各路由的 <head> 与 OG 标签 | export async function generateMetadata() |
app/@modal/default.tsx | 平行路由(Parallel Routes)的插槽目录,default.tsx 是必填的软回退 | app/@modal/default.tsx |
template.tsx | 每次导航都重新挂载的模板,放进入动画;与 layout 的差别就在「会不会重挂载」 | template.tsx |
app/api/route.ts | Route Handler 入口,按方法导出 GET/POST 等函数,取代 pages/api 写法 | app/api/route.ts |
export async function GET(request) | GET 处理函数,request 是标准 Web Request,可直接用 new URL(request.url) 取查询串 | export async function GET(request) |
middleware.ts | 在根目录(或 src)定义,匹配器决定拦截范围,用于鉴权与重写 | middleware.ts |
NextResponse.redirect(new URL("/login", request.url)) | 服务端重定向,origin 必须由 request.url 派生,不能写绝对域名(多域名部署会错跳) | NextResponse.redirect(new URL("/login", request.url)) |
NextResponse.next() | 放行继续执行,不写这一行 middleware 默认也是继续但显式写出更清楚 | NextResponse.next() |
cookies().get("token") | 在服务端组件/路由处理器读 Cookie;一旦读取即让该路由变为动态渲染 | cookies().get("token") |
import Image from "next/image" | 内建图片组件,自动生成 srcset/尺寸并默认走优化管线;忘记给宽高会报尺寸缺失警告 | import Image from "next/image" |
import { Inter } from "next/font/google" | 构建期自托管字体,消除外部请求与 CLS;网络受限时构建会失败 | import { Inter } from "next/font/google" |
output: "standalone" | 构建产物自带精简 node_modules 的独立服务目录,适合打进 Docker 镜像 | output: "standalone" |
output: "export" | 纯静态导出,只能用于完全静态的站点;任何 Route Handler、ISR、动态路由都会失败 | output: "export" |
process.env.NEXT_PUBLIC_API_URL | NEXT_PUBLIC_ 前缀的变量被打进客户端 bundle;改动必须重新构建且会暴露给所有人 | process.env.NEXT_PUBLIC_API_URL |
next build / next start | 构建并以生产模式启动;开发模式与生产模式的缓存与路由行为并不一致,判断线上问题必须用 next start | next build |
Common pitfalls
Symptom服务端组件里调用 useRouter / useState,报 "useState only works in a Client Component"。
CauseApp Router 默认为服务端组件,只有加了 "use client" 的文件及其 import 链才有这些 Hook。
Fix把需要交互的最小叶子组件单独抽成一个文件并顶部写 "use client",不要在整个 layout 上加,否则整棵子树都会进 bundle。
Symptom页面首屏 HTML 里数据是空的,页面加载后才闪现出真实内容(hydration mismatch 警告)。
Cause服务端渲染时取了缓存值、客户端又取了实时值,两侧输出不一致;常见于直接读 localStorage 或随机数。
Fix把不一致的部分放进 useEffect 里设置状态,或对该组件加 "use client" 并让服务端与客户端输出一致。
Symptom明明用了 fetch 缓存,接口请求量还是等于访问量。
Causefetch 缓存只作用于服务端渲染与静态生成;请求里出现 cookies()、headers()、searchParams 等动态 API 会让整段变成动态渲染,缓存全部失效。
Fix把数据获取放进单独的静态子段(不带动态 API),在页面层用 Suspense + 子组件消费;或确认 dynamic 未被设为 force-dynamic。
Symptom在 page.tsx 里直接用 useSearchParams(),构建时整页都变动态并抛 Suspense 边界缺失错误。
CausesearchParams 是动态 API,读取它会让该路由段 opt out 静态渲染;没有 Suspense 包裹时 Next 无法确定流式边界。
Fix把读取 searchParams 的逻辑单独放进一个客户端子组件,外层用 <Suspense> 包住。
SymptomNextResponse.redirect(new URL("https://example.com/login", request.url)) 在预发环境把用户跳到生产域名。
Cause重定向 URL 写死了绝对域名,忽略了两层:请求的 origin 与部署域名可能不同(如预发子域)。
Fix一律用 new URL("/login", request.url) 相对构造,让 Next 从当前请求派生 origin。
Symptom把 API 密钥写进客户端组件能读到的变量里,密钥出现在浏览器 Network 里。
CauseNEXT_PUBLIC_ 前缀的变量与未加前缀但在客户端组件中 import 的模块都会被内联进 bundle。
Fix密钥只放服务端:用 server-only 保护模块,把调用放进 Route Handler 或 Server Action。
Symptomexport const runtime = "edge" 后构建报某个 Node 库在 Edge Runtime 下不可用。
CauseEdge Runtime 只有 Web API,没有 fs、net、部分 npm 原生模块。
Fix改回默认 Node runtime;确需 Edge 时把重依赖留在服务端并通过 Route Handler 调用。
Symptomnext build 通过、next dev 正常,但 next start 后页面 404。
Cause文件写在错误的目录层级(例如放在 pages/ 而非 app/,或 app 目录未位于根或 src 下),dev 与 build 的路由解析差异让问题被掩盖。
Fix核对目录位置与文件名;用 next build 的路由表确认路径是否被收录。
Symptom部署到 CDN 后页面一直拿到旧数据,revalidate 似乎没生效。
CauseCDN 层的缓存头优先于 Next 的 ISR,或 output: "export" 下完全没有 ISR,revalidate 被忽略。
Fix检查响应头 Cache-Control 与 CDN 规则;静态导出场景改用构建期取数或改回标准部署。
Troubleshooting
1用生产模式复现线上问题
next build && next startnext dev 不做完整优化且路由行为不同,判断缓存与构建错误必须用 next start。
2查看构建产物的路由与渲染模式
next build输出里每个路由会标注 Static / Dynamic / ISR 以及大小与首次加载 JS。
3确认客户端变量的取值是否进了 bundle
grep -r "NEXT_PUBLIC" .next/static在 .next/static 里搜到明文即说明该变量被内联进客户端产物。
4排查静态导出是否可行
grep -r "export const dynamic" app任何 Route Handler、ISR 或 force-dynamic 段都会让 output: "export" 构建失败。
5检查中间件实际拦截了哪些路径
grep -n "matcher" middleware.tsmatcher 配置过宽会让每个请求都过一遍中间件,是性能问题的常见来源。
6核对约定式文件是否被路由识别
ls -R app确认 page.tsx / layout.tsx 的文件名与层级,拼写错误(如 page.ts)不会报错只是没有路由。
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: "页面标题",
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