Cheatsheets / Next.js

Next.js Cheatsheet

Complete Next.js reference. Hit Ctrl+P to print.

CLI

npx create-next-app@latest my-appCreate a new Next.js app
npx create-next-app@latest my-app --typescript --tailwind --appCreate with TypeScript, Tailwind, App Router
npm run devStart development server with HMR
npm run buildBuild for production
npm run startStart production server after build
npm run lintRun ESLint checks
ANALYZE=true npm run buildBuild with bundle analyzer (requires plugin)

App Router - File Conventions

app/page.tsxRoute UI - renders at "/"
app/blog/page.tsxRoute UI - renders at "/blog"
app/blog/[slug]/page.tsxDynamic route - "/blog/:slug"
app/blog/[...slug]/page.tsxCatch-all route - "/blog/a/b/c"
app/blog/[[...slug]]/page.tsxOptional catch-all - also matches "/blog"
app/layout.tsxShared layout wrapping child pages
app/loading.tsxLoading UI shown during Suspense
app/error.tsxError boundary UI for the segment
app/not-found.tsxShown when notFound() is called
app/route.tsAPI route handler at the path
app/(group)/page.tsxRoute group - "(group)" not in URL
app/_components/Button.tsxPrivate folder - excluded from routing
middleware.tsMiddleware runs before every request

Server & Client Components

"use client"Directive - marks file as Client Component (top of file)
"use server"Directive - marks file/function as Server Action
export default async function Page() { const data = await fetch(...); }Server Component - async by default, can await directly
export default function Counter() { const [n, setN] = useState(0); }Client Component - can use hooks and browser APIs
import "use client"; // at top of fileMake entire file a Client Component
<ClientComp serverProp={await getData()} />Pass server data as props to Client Component
childrenPass Server Components as children to Client Components
import { cache } from "react"; export const getData = cache(async () => { ... })Deduplicate requests across components

Data Fetching

const data = await fetch("https://api.example.com/data")Fetch in Server Component (cached by default)
fetch(url, { cache: "no-store" })Always fresh - opt out of caching
fetch(url, { next: { revalidate: 60 } })Revalidate cached data every 60 seconds (ISR)
fetch(url, { next: { tags: ["posts"] } })Tag fetch for on-demand revalidation
revalidateTag("posts")On-demand revalidate by tag (in Server Action or Route Handler)
revalidatePath("/blog")On-demand revalidate a specific path
import { unstable_cache } from "next/cache"Cache arbitrary async functions (like DB queries)
export const dynamic = "force-dynamic"Opt page out of static generation
export const revalidate = 3600Route segment revalidation in seconds (ISR)
export async function generateStaticParams() { return posts.map(p => ({ slug: p.slug })); }Pre-generate dynamic routes at build time

Route Handlers (API Routes)

export async function GET(request: Request) { return Response.json({ ok: true }); }GET route handler
export async function POST(request: Request) { const body = await request.json(); }POST handler with JSON body
export async function GET(req, { params }) { const { id } = await params; }Dynamic route params in handler
new Response(JSON.stringify(data), { status: 201, headers: { "Content-Type": "application/json" } })Custom status and headers
import { NextResponse } from "next/server"; NextResponse.json(data, { status: 201 })Next.js helper response
NextResponse.redirect(new URL("/login", request.url))Redirect in route handler or middleware
const url = new URL(request.url); url.searchParams.get("q")Read query string params
request.headers.get("authorization")Read request header
const cookie = request.cookies.get("token")Read cookie in handler

Server Actions

"use server"; export async function createPost(formData) { ... }Define a Server Action in a separate file
async function createPost(formData) { "use server"; ... }Inline Server Action inside Server Component
<form action={createPost}><input name="title" /><button>Save</button></form>Progressive enhancement form with Server Action
const [state, action] = useActionState(createPost, initialState)Client hook for action state and pending status
import { redirect } from "next/navigation"; redirect("/posts")Redirect inside Server Action
import { revalidatePath } from "next/cache"; revalidatePath("/posts")Revalidate cache after mutation
import { cookies } from "next/headers"; (await cookies()).set("token", value)Set cookie in Server Action

Routing & Navigation

import Link from "next/link"; <Link href="/about">About</Link>Client-side navigation link
<Link href="/blog/[slug]" as={`/blog/${slug}`}>Link with dynamic segment
import { useRouter } from "next/navigation"; const router = useRouter()Programmatic navigation (Client Component)
router.push("/dashboard")Navigate to route
router.replace("/login")Navigate without adding to history
router.back()Navigate to previous page
router.refresh()Refresh current route (re-fetch server data)
import { usePathname } from "next/navigation"; const pathname = usePathname()Get current pathname
import { useSearchParams } from "next/navigation"; const q = useSearchParams().get("q")Read query string (Client Component)
import { useParams } from "next/navigation"; const { slug } = useParams()Read dynamic params (Client Component)
import { redirect, notFound } from "next/navigation"Server-side redirect and 404

Metadata & SEO

export const metadata = { title: "My Page", description: "..." }Static metadata export from page/layout
export async function generateMetadata({ params }) { return { title: post.title }; }Dynamic metadata function
title: { template: "%s | My Site", default: "My Site" }Title template in root layout
openGraph: { title: "...", description: "...", images: ["/og.png"] }Open Graph metadata
twitter: { card: "summary_large_image", title: "..." }Twitter card metadata
app/opengraph-image.tsxDynamic OG image via file convention
import { ImageResponse } from "next/og"Generate OG images with JSX
app/robots.ts → export default function robots() { return { rules: [...] }; }robots.txt via file convention
app/sitemap.ts → export default function sitemap() { return [...]; }sitemap.xml via file convention

Image & Font Optimization

import Image from "next/image"Import optimized Image component
<Image src="/photo.jpg" alt="Photo" width={800} height={600} />Local image - width and height required
<Image src={img} alt="Photo" fill className="object-cover" />Fill parent container
<Image src="https://..." alt="..." width={400} height={300} />Remote image - add hostname to next.config
priorityEagerly load - use on LCP image
sizes="(max-width: 768px) 100vw, 50vw"Responsive size hints for srcset
import { Inter } from "next/font/google"; const inter = Inter({ subsets: ["latin"] })Import Google Font
<html className={inter.className}>Apply font to entire app
import localFont from "next/font/local"; const myFont = localFont({ src: "./font.woff2" })Load local font file

Middleware

export function middleware(request: NextRequest) { ... }Default middleware export in middleware.ts
export const config = { matcher: ["/dashboard/:path*", "/api/:path*"] }Scope middleware to specific paths
NextResponse.next()Continue to next handler
NextResponse.redirect(new URL("/login", request.url))Redirect from middleware
NextResponse.rewrite(new URL("/404", request.url))Rewrite URL (user sees original path)
const response = NextResponse.next(); response.headers.set("X-Custom", "value"); return response;Modify response headers
request.cookies.get("token")?.valueRead cookie in middleware
request.nextUrl.pathnameGet current pathname in middleware

Environment Variables

.env.localLocal overrides - never committed to git
.env.development / .env.productionEnvironment-specific defaults
.envShared defaults - safe to commit (no secrets)
NEXT_PUBLIC_API_URL=https://api.example.comNEXT_PUBLIC_ prefix exposes variable to the browser
DB_URL=postgres://...No prefix - server-only, never sent to client
process.env.NEXT_PUBLIC_API_URLRead public env var in client or server code
process.env.DB_URLRead server-only env var (undefined in client components)
import { env } from "process"Access env from Node process module in Server Components
NEXT_PUBLIC_ vars are inlined at build timeChanging them requires a rebuild - not dynamic
export const dynamic = "force-dynamic"Required if reading server env vars that change at runtime

next.config.js

/** @type {import("next").NextConfig} */JSDoc type annotation for IntelliSense
output: "standalone"Self-contained build for Docker deployment
output: "export"Fully static export - no Node.js server needed
images: { remotePatterns: [{ hostname: "cdn.example.com" }] }Allow next/image to load from remote host
images: { formats: ["image/avif", "image/webp"] }Enable AVIF and WebP image optimization
redirects: async () => [{ source: "/old", destination: "/new", permanent: true }]Permanent (308) redirect
rewrites: async () => [{ source: "/api/:path*", destination: "https://api.example.com/:path*" }]Proxy requests to another URL
headers: async () => [{ source: "/(.*)", headers: [{ key: "X-Frame-Options", value: "DENY" }] }]Add custom response headers
env: { CUSTOM_KEY: process.env.CUSTOM_KEY }Expose server env vars to the app (use NEXT_PUBLIC_ instead for client)
basePath: "/docs"Serve app under a sub-path
trailingSlash: trueRedirect /about to /about/
poweredByHeader: falseRemove X-Powered-By: Next.js response header
compress: falseDisable built-in gzip compression (use reverse proxy instead)
transpilePackages: ["some-esm-package"]Transpile packages in node_modules with SWC
experimental: { typedRoutes: true }Enable statically typed Links and routes