Next.js

Complete Next.js cheat sheet covering App Router, Server Components, data fetching, caching, Server Actions, middleware, and rendering.

13 sections27 cards

Next.js is a React framework that adds server-side capabilities — routing, server rendering, API routes, caching, and build optimization — on top of React. You write React components, Next.js decides where and how they run.

Two routers exist. Pages Router (old, stable, still widely used) lives in /pages. App Router (new, current standard) lives in /app. Both can coexist during migration. This sheet focuses on App Router (Next.js 13+) since that's what interviews and new projects use.

The biggest mental shift from plain React: components run on the server by default. Not all your code runs in the browser. This changes how you think about data fetching, state, and what APIs are available where.

Routes are defined by folders inside app/. Each folder segment maps to a URL segment. Special files inside folders define what renders at that route.

Special files (all are React components by default):

page.tsx — the UI for a route. Makes the route publicly accessible.

layout.tsx — wraps the page and all nested routes. Persists across navigation — doesn't remount. Root layout (app/layout.tsx) is required and must include <html> and <body>.

loading.tsx — shown while the page is loading. Automatically wraps the page in a Suspense boundary.

error.tsx — shown when an error is thrown. Must be a Client Component ("use client"). Receives error and reset props.

not-found.tsx — rendered when notFound() is called or no route matches.

template.tsx — like layout but remounts on every navigation. Rare use case.

route.ts — API endpoint (no UI). Replaces /pages/api.

middleware.ts — runs before every request at the edge. Lives at project root, not inside app/.

route segments

app/dashboard/page.tsx/dashboard

app/blog/[slug]/page.tsx/blog/anything — dynamic segment. Access via params.slug.

app/shop/[...slug]/page.tsx/shop/a/b/c — catch-all. params.slug is an array.

app/shop/[[...slug]]/page.tsx → optional catch-all. Also matches /shop with no segments.

app/(marketing)/about/page.tsx/about — route group. Parentheses are ignored in URL. Used to organize routes without affecting the path, or to apply a layout to a subset of routes.

app/_components/ — private folder. Underscore prefix opts out of routing entirely. Safe to put shared components here.

app/@modal/ — parallel route slot. Renders multiple pages simultaneously in the same layout.

navigation

<Link href="/about"> — client-side navigation. Prefetches the linked route in the background. Use instead of <a> for internal links.

useRouter() — programmatic navigation from Client Components. router.push("/path"), router.replace(), router.back(), router.refresh().

redirect("/path") — server-side redirect from Server Components and Server Actions. Throws internally, so don't put in try/catch.

permanentRedirect("/path") — 308 redirect.

usePathname() — current pathname. Client Component only.

useSearchParams() — read query params. Client Component only.

useParams() — read dynamic route params on client.

This is the most important concept in the App Router. Get this wrong and nothing makes sense.

Server Components (default) — run only on the server. Never sent to the browser as JS. Can use async/await directly. Can access databases, file system, secrets. Cannot use hooks, event handlers, browser APIs, or state.

Client Components — "use client" directive at top of file. Run on the server first (for initial HTML), then hydrate in the browser. Can use hooks, state, event handlers, browser APIs. Cannot use server-only APIs (fs, db directly).

The boundary: "use client" marks where the server-client boundary starts. All imports below that file become client-side too (they're part of the client bundle). Children passed as props to a Client Component can still be Server Components — props cross the boundary as serialized data.

server component rules

Can: async/await at component level, access DB/FS/secrets, import server-only packages, pass data down as props.

Cannot: use useState, useEffect, event handlers like onClick, browser APIs like window/document.

import "server-only" — package that throws if imported in a client bundle. Guards sensitive server code.

Props must be serializable — no functions, no class instances, no Date objects (convert to string/number first).

Server Components can import Client Components. The reverse is also allowed — pass Server Components as children props.

client component rules

"use client" at very top of file — before imports.

Can: use all React hooks, event handlers, browser APIs, third-party client libraries.

Still renders on server for initial HTML (SSR). Hydrates on the client.

Push "use client" as deep/far down the tree as possible. Keep most of your app as Server Components — smaller JS bundle, better performance.

Pattern: Server Component fetches data → passes to Client Component for interactive UI.

Context, state management libraries, animation libraries — all need "use client".

server component fetching

Fetch directly in async Server Components. No useEffect, no loading state, no libraries needed for the basics.

const data = await fetch("https://api.example.com/data") — Next.js extends the native fetch with caching options.

DB query directly: const users = await db.user.findMany() — completely fine in a Server Component. No API route needed.

Fetch in parallel: const [a, b] = await Promise.all([fetchA(), fetchB()]) — don't await sequentially unless they depend on each other.

Pass data to children as props. Or fetch in each component that needs it — Next.js deduplicates identical fetch calls within a request automatically.

fetch caching options

fetch(url, { cache: "force-cache" }) — cache indefinitely. Like SSG. Default in Next.js 13/14 (changed in 15).

fetch(url, { cache: "no-store" }) — never cache. Fresh on every request. Like SSR.

fetch(url, { next: { revalidate: 60 } }) — cache for 60 seconds, then revalidate. Like ISR.

fetch(url, { next: { tags: ["posts"] } }) — tag the cache. Invalidate with revalidateTag("posts").

In Next.js 15, fetch is no longer cached by default — you must opt in. Breaking change from 13/14.

route segment config

Export these from page.tsx or layout.tsx to control the whole route's behavior:

export const dynamic = "force-dynamic" — always SSR, never cache. Same as no-store on all fetches.

export const dynamic = "force-static" — force static generation even if dynamic APIs are used.

export const revalidate = 60 — ISR: regenerate page every 60 seconds.

export const runtime = "edge" — run on edge runtime instead of Node.js. Faster cold starts, limited APIs.

export const generateStaticParams = async () => [...] — for dynamic routes, pre-generate static pages at build time. Returns array of param objects.

revalidation

revalidatePath("/blog") — purge cached data for a path. Call from Server Actions or Route Handlers.

revalidateTag("posts") — purge all fetches tagged with "posts".

Both functions imported from "next/cache".

Use after mutations — when you create/update/delete data, revalidate the affected paths so users see fresh data.

router.refresh() — on the client, re-fetches Server Component data for the current route without a full page reload. Preserves client state.

Server Actions are async functions that run on the server, callable from Client Components and forms. They replace the need to create API routes for mutations.

"use server" directive — at the top of a function (inside a Client Component) or top of a file (to make all exports Server Actions).

In a Server Component file, mark individual async functions with "use server" inside the function body. In a separate actions.ts file, put "use server" at the top.

From a form: <form action={myServerAction}> — works without JS (progressive enhancement). formData is passed automatically.

From a button: <button onClick={() => myServerAction(data)}> in a Client Component.

After mutation in a Server Action: call revalidatePath() or revalidateTag() to refresh cached data, then redirect() if needed.

useFormStatus() — hook from react-dom. Returns { pending } — true while a parent form's action is running. Use to show loading state on submit button.

useActionState(action, initialState) — hook that wraps a Server Action. Returns [state, formAction, isPending]. Handles error/success state returned from the action.

app/api/users/route.ts — creates an API endpoint at /api/users. Export named functions for each HTTP method: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS.

Each handler receives a Request object (standard Web API) and returns a Response.

NextRequest — extends Request with Next.js extras: nextUrl (parsed URL with searchParams), cookies.

NextResponse.json(data, { status: 201 }) — convenience method. Or use Response.json(data) (standard).

Read body: const body = await request.json() or await request.formData().

Read query params: request.nextUrl.searchParams.get("id").

Read dynamic segment: handler receives { params } as second argument — params.id from [id]/route.ts.

When to use Route Handlers vs Server Actions: Route Handlers for external API consumers (mobile apps, third-party services, webhooks). Server Actions for mutations triggered from your own UI — simpler, no fetch needed.

static metadata

Export a metadata object from page.tsx or layout.tsx:

export const metadata = { title: "My Page", description: "About us", openGraph: { title: "...", images: ["/og.png"] } }

Root layout metadata acts as defaults. Page metadata merges and overrides.

title: { template: "%s | My App", default: "My App" } — in layout, child pages replace %s automatically.

Handles: title, description, keywords, openGraph, twitter, robots, canonical, icons, viewport, themeColor.

dynamic metadata

Export generateMetadata async function when metadata depends on data:

export async function generateMetadata({ params }) { const post = await getPost(params.slug); return { title: post.title } }

Runs on server only. Next.js deduplicates the data fetch — if you fetch the same thing in the page component, it's cached and reused.

notFound() inside generateMetadata — triggers the not-found page.

SSG — static generation

Page rendered at build time. Fastest — served as a static file from CDN. No server needed per request.

Default for pages with no dynamic data. Use generateStaticParams for dynamic routes you want pre-built.

revalidate export makes it ISR (Incremental Static Regeneration) — static but refreshed on a schedule.

Best for: marketing pages, blogs, docs — content that changes rarely.

SSR — server-side rendering

Page rendered on server on each request. Fresh data every time. Slower than SSG — a server must respond.

Triggered by: using cookies(), headers(), searchParams prop, cache: "no-store", or dynamic = "force-dynamic".

Best for: dashboards, user-specific pages, real-time data, pages behind auth.

ISR — incremental static regeneration

Static page that revalidates in the background after a time interval or on-demand.

Stale-while-revalidate: user gets cached (fast) response immediately, Next.js regenerates in background, next user gets fresh page.

export const revalidate = 60 — revalidate every 60 seconds.

On-demand: call revalidatePath() or revalidateTag() from a webhook or Server Action when data changes.

Best for: e-commerce product pages, news sites, any content that changes but doesn't need to be real-time.

streaming &amp; suspense

Next.js supports streaming HTML — send parts of the page as they're ready instead of waiting for everything.

Wrap slow components in <Suspense fallback={<Skeleton />}>. The rest of the page streams immediately, the suspended part streams when ready.

loading.tsx — automatically wraps the page in Suspense. Shows while the page (and its async data) loads.

Granular: wrap individual slow components in Suspense inside the page. Faster perceived performance — fast parts appear immediately.

middleware.ts at project root. Runs before every matched request — at the edge (before the server). Extremely fast, no cold start.

Export a middleware function receiving a NextRequest. Return NextResponse.next() to continue, NextResponse.redirect() to redirect, NextResponse.rewrite() to rewrite URL internally.

Export a config object with a matcher to control which routes middleware runs on: export const config = { matcher: ["/dashboard/:path*", "/api/:path*"] }

Common uses: auth protection (check cookie, redirect to login if missing), A/B testing (rewrite to different page), geolocation redirects, rate limiting, adding headers.

Read cookies: request.cookies.get("token"). Set cookies on response: response.cookies.set("key", "value").

Middleware runs on the edge runtime — no Node.js APIs. Only Web APIs and a subset of Node APIs are available. Can't use Mongoose or bcrypt here.

Image

import Image from "next/image"

Automatically: lazy loads, prevents layout shift (requires width+height or fill), converts to WebP/AVIF, serves correctly sized image for device.

fill prop — image fills parent container. Parent must have position: relative and defined dimensions.

priority prop — preloads image. Use for above-the-fold images (hero, LCP element).

placeholder="blur" — shows blurred version while loading. Needs blurDataURL.

Remote images need domain allowed in next.config.js under images.remotePatterns.

Font

import { Inter } from "next/font/google"

const inter = Inter({ subsets: ["latin"] })

Apply: <body className={inter.className}>

Zero layout shift — font is loaded at build time and self-hosted. No external request to Google Fonts at runtime.

Local font: import localFont from "next/font/local" then point to font file.

CSS variable: Inter({ subsets: ["latin"], variable: "--font-inter" }) then use in Tailwind or CSS.

Script

import Script from "next/script"

Manages loading strategy for third-party scripts.

strategy="beforeInteractive" — before page becomes interactive. For critical scripts.

strategy="afterInteractive" (default) — after hydration. For analytics, tag managers.

strategy="lazyOnload" — during idle time. For low-priority scripts.

strategy="worker" — off main thread via Partytown. Experimental.

onLoad, onReady, onError callbacks available.

images.remotePatterns — allow external image domains for next/image.

redirects() — async function returning array of redirect rules. Server-side, permanent or temporary.

rewrites() — proxy requests internally. URL stays the same in browser. Use to mask API URLs or proxy to a different backend.

headers() — add custom headers to responses (security headers, CORS).

env — expose environment variables to the browser. Or prefix with NEXT_PUBLIC_ to auto-expose.

experimental.serverActions: true — was required in early versions. Not needed in Next.js 14+.

output: "standalone" — bundle for Docker deployment. Copies only necessary files.

typescript.ignoreBuildErrors: true — skip TS errors in build. Useful in CI but bad practice for production.

NextAuth.js (Auth.js v5) — the standard auth library. Supports OAuth providers (Google, GitHub), credentials, magic links. Handles sessions, JWTs, callbacks.

Setup: app/api/auth/[...nextauth]/route.ts — catch-all route handler. Export GET and POST from the NextAuth handler.

Session on server: const session = await auth() — in Server Components, Server Actions, Route Handlers.

Session on client: useSession() hook — in Client Components. Wrap app with <SessionProvider>.

Protecting routes with middleware — most efficient approach. Check session token in middleware, redirect to login if missing. Runs at edge before any rendering.

Protecting Server Components — call auth() at top of component, redirect if no session.

Protecting Route Handlers — call auth() inside the handler, return 401 if no session.

common traps

Server Components can't use hooks or event handlers — if you need them, add "use client".

"use client" doesn't mean client-only — it still SSRs for initial HTML. It means "this can run on the client too."

cookies() and headers() make a route dynamic. Calling them opts out of caching.

params and searchParams in page components are passed as props, not via hooks. In Server Components at least — useSearchParams() is for Client Components.

redirect() throws internally — don't call it inside try/catch without re-throwing.

Environment variables without NEXT_PUBLIC_ prefix are server-only. Accessing them in client code returns undefined.

Images from external domains need remotePatterns config or Next.js will reject them.

things to know cold

Pages Router vs App Router — know the difference. Pages Router uses getServerSideProps, getStaticProps, getStaticPaths. App Router uses async components, generateStaticParams, fetch options.

Hydration — server renders HTML, React takes over on client and attaches event listeners. Hydration mismatch (server/client output differs) causes errors.

The four rendering modes: Static (build time), SSR (per request), ISR (periodic), Client-side (after hydration). Know when to use each.

Server Actions are POST requests under the hood. Next.js handles serialization automatically.

Layouts don't re-render on navigation within their subtree — state inside a layout persists across page changes. This is intentional and very useful.

Next.js 15 changes: fetch not cached by default, params/searchParams are Promises (must await), React 19 support.