Next.js Playbook
React with superpowers: file-based routing, server components Server Component A React component that runs ONLY on the server. Can directly access databases, fetch data, and keeps secrets safe. The default in Next.js App Router. "Like the kitchen in a restaurant. Customers never see it, but that's where the magic happens."
1What Next.js Is (and What Changed in 16)
Next.js Next.js A React framework that adds routing, server rendering, API endpoints, and caching. The go-to for modern web apps. Current releases (Next.js 16) default to Turbopack, use proxy.ts instead of middleware.ts, and make caching explicit with Cache Components. "Like React with a jetpack. Everything you need to go from idea to production." React A JavaScript library (by Meta) for building user interfaces out of components. It's the foundation under Next.js and React Native, and the most popular frontend tool. "Like LEGO for websites. Build small pieces (components), snap them together." Vercel A cloud platform built by the team behind Next.js. Push to Git and every branch gets a live Preview Deployment; production is one merge away. Functions run on Fluid Compute by default, and Vercel adds storage, AI Gateway, queues, sandboxes, and more. "Like magic website publishing. Push to GitHub, and boom — it's live."
React alone
- • Just the UI layer
- • No routing built in
- • Runs in the browser by default
- • You wire up everything else
Next.js
- • Full-stack framework
- • Folders become URLs
- • Server + client rendering
- • Sensible defaults, zero config
The Next.js 16 changes that bite AI-written code
Most tutorials (and a lot of model training data) describe Next.js 13 to 15. If Claude writes any of the "old" column, push back.
| Topic | Old habit | Next.js 16 |
|---|---|---|
| Request interception | middleware.ts | proxy.ts proxy.ts The Next.js 16 name for what used to be middleware.ts: a file at the project root whose `export function proxy(request)` runs before a request reaches your pages, for redirects, rewrites, and auth checks. Upgrade with `npx @next/codemod@canary middleware-to-proxy .`; with Auth.js v5 it can be as short as `export { auth as proxy } from "@/auth"`. "Like the host stand at a restaurant entrance. Everyone passes it first, and it decides where you're seated or if you get in at all." export function proxy() |
| params, cookies, headers | Read synchronously | Always await them |
| Bundler | next dev --turbopack | Turbopack Turbopack The Rust-based bundler built into Next.js. Since Next.js 16 it's the default for both `next dev` and `next build`, no flag needed; you can opt out with `next build --webpack` if a plugin still needs webpack. "Like swapping a bicycle courier for a motorbike. Same packages delivered, just a lot faster." |
| Caching | Implicit fetch caching, unstable_cache | Cache Components Cache Components Next.js 16's explicit caching model. Turn it on with `cacheComponents: true` in next.config.ts, then mark functions or components with the `'use cache'` directive and control freshness with `cacheLife`, `cacheTag`, and `updateTag` from `next/cache`. Anything not marked stays dynamic. "Like labeling containers in the fridge with a use-by date. You decide what gets saved and exactly when it gets tossed." 'use cache' |
| Linting | next lint | Removed. Run ESLint (or Biome) directly |
| Styling | tailwind.config.ts | Tailwind v4 is CSS-first, no config file needed |
| Runtime | runtime = 'edge' | Not supported as of 16.3. Routes run on Node.js |
Why vibe coders love it
Conventions are strict and predictable, so AI assistants can navigate your project without a map. New projects even ship with an AGENTS.md file with guidance aimed at coding agents.
2Quick Start
You need Node.js Node.js A runtime that lets you run JavaScript outside of a web browser. It's the engine that powers most modern dev tools, including Claude Code. "Like installing a game console. You need the console (Node) before you can play any games (run tools)."20.9 or newer. Node 18 and 20 are end-of-life, so install Node 24 (the current Active LTS) and stop worrying about it.
Terminal
node --version # want v24.x (anything >= 20.9.0 works)
npx create-next-app@latest my-app --yes
cd my-app
npm run dev # http://localhost:3000--yes accepts the recommended defaults, which are exactly what you want:
Prefer Biome over ESLint? Run without --yes and pick it at the linter prompt.
3The App Router: Folders Are URLs
The App Router App Router Next.js's routing system based on the /app directory (the default since Next.js 13). Folders become routes, layouts nest, and components are Server Components by default. In Next.js 16, `params`, `searchParams`, `cookies()`, and `headers()` must all be awaited. "Like a GPS that automatically knows every street in your app. Create a folder = create a route."app/. Make a folder, drop a page.tsx in it, and that folder's path is now a URL. The old pages/ router still works, but start every new project on the App Router.
Folder → URL
app/
page.tsx → yoursite.com/
about/
page.tsx → yoursite.com/about
blog/
page.tsx → yoursite.com/blog
[slug]/
page.tsx → yoursite.com/blog/any-postDynamic routes: params is a Promise now
[slug] captures part of the URL. In Next.js 16 you must await params. Synchronous access was removed, so old snippets that read params.slug directly will break.
app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>Post: {slug}</h1>;
}4Special Files You'll See Everywhere
page.tsxThe UI for a route. No page.tsx, no URL.
layout.tsxShared shell (navbar, footer). Persists across navigation and doesn't re-render.
loading.tsxInstant loading UI while the route streams in. React Suspense under the hood.
error.tsxError boundary for a route segment, so one crash doesn't take down the app. Must be a Client Component.
not-found.tsxCustom 404, also shown when you call notFound().
route.tsA backend endpoint (GET, POST, etc.) instead of a page.
proxy.tsRuns before matching requests: redirects, rewrites, quick auth checks. Replaces middleware.ts.
next.config.tsFramework settings, e.g. turning on Cache Components.
app/layout.tsx
import "./globals.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Navbar />
{children}
<Footer />
</body>
</html>
);
}5Server vs Client Components
The #1 confusion point
Get this model in your head and half of all Next.js errors disappear.
Server Components
The default. Run on the server, send finished HTML. Their code never ships to the browser.
✓ Fetch data / query the database directly
✓ Use secrets safely
✓ Smaller JavaScript bundle
✗ No useState / useEffect
✗ No onClick / onChange
✗ No window or localStorage
Client Components
Add "use client" at the top. Also run in the browser, with full React interactivity.
✓ State and effects
✓ Event handlers
✓ Browser APIs, animation libraries
✗ Can't touch the database directly
✗ Adds to bundle size
✗ Anything you import here is public
Rule of thumb: start with Server Components. Add "use client" only to the smallest leaf that needs interactivity.
components/Counter.tsx
"use client"; // must be the very first line
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}A Server Component page can render Client Component A React component that runs in the browser. Required for interactivity (useState, onClick, useEffect). Add "use client" at the top of the file. "Like the dining room. The customer sees it, interacts with it, and clicks the buttons."<Counter /> just fine. The page stays on the server; only the button's code goes to the browser. See Client Component
6Fetching Data (and the Async Request APIs)
Server Components can be async, so you just await your data. No useEffect, no loading-state juggling, and the HTML arrives already filled in.
app/users/page.tsx
import { db } from "@/lib/db";
import { users } from "@/lib/schema";
export default async function UsersPage() {
const allUsers = await db.select().from(users);
return (
<ul>
{allUsers.map((u) => <li key={u.id}>{u.name}</li>)}
</ul>
);
}Request data is async too
cookies(), headers(), draftMode(), params, and searchParams all return Promises in Next.js 16. Await every one.
app/search/page.tsx
import { cookies } from "next/headers";
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
const { q } = await searchParams;
const theme = (await cookies()).get("theme")?.value ?? "dark";
return <p>Searching for {q} in {theme} mode</p>;
}7Caching: Dynamic by Default, Cached on Purpose
Older Next.js cached things behind your back, and "why is my page showing stale data?" was a rite of passage. With Cache Components the deal is simpler: everything runs fresh on every request unless you say otherwise.
Think of it like a restaurant. By default every dish is cooked to order. 'use cache' is you telling the kitchen "batch-cook this one, it keeps."
next.config.ts (turn it on)
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;lib/products.ts (cache one function)
import { cacheLife, cacheTag } from "next/cache";
export async function getProducts() {
"use cache";
cacheLife("hours"); // profiles: seconds, minutes, hours, days, weeks, max
cacheTag("products"); // a label you can invalidate later
return db.select().from(products);
}app/actions.ts (bust the cache after a write)
"use server";
import { updateTag } from "next/cache";
export async function addProduct(formData: FormData) {
// ...check auth, validate, insert...
updateTag("products"); // next read gets fresh data
}Good candidates for 'use cache'
- • Product catalogs, blog posts, docs
- • Expensive queries that change rarely
- • Third-party API calls with rate limits
Leave these dynamic
- • Anything per-user (dashboards, carts)
- • Anything reading cookies or headers
- • Data that must be right this second
The old unstable_cache and unstable_-prefixed cache helpers are gone; if Claude suggests them, ask for the 'use cache' version.
8Route Handlers & Server Actions
Route Handlers (route.ts)
Your backend API, living next to your pages. Use these for webhooks, mobile clients, and anything outside your own UI calling in.
app/api/users/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const allUsers = await db.select().from(users);
return NextResponse.json(allUsers);
}
export async function POST(request: Request) {
const body = await request.json();
// validate with Zod, then insert...
return NextResponse.json({ ok: true }, { status: 201 });
} Server ActionsServer Actions
Next.js feature that lets you run server-side code directly from React components. Mark a function with 'use server' and call it from forms or buttons.
"Like a direct line to the kitchen from your table. No waiter needed — press a button and the order goes straight to the chef."
Server Actions
Next.js feature that lets you run server-side code directly from React components. Mark a function with 'use server' and call it from forms or buttons.
"Like a direct line to the kitchen from your table. No waiter needed — press a button and the order goes straight to the chef."
Server functions your forms and buttons call directly. No API route to write.
app/actions.ts
"use server";
import { auth } from "@/auth";
import { updateTag } from "next/cache";
export async function createNote(formData: FormData) {
const session = await auth();
if (!session?.user) throw new Error("Not signed in");
const text = String(formData.get("text") ?? "");
await db.insert(notes).values({ text, userId: session.user.id });
updateTag("notes");
}In any component
<form action={createNote}>
<input name="text" />
<button type="submit">Save</button>
</form>Every Server Action is a public endpoint. Anyone can POST to it. Check the session and validate input inside the action itself, every time. See Protected Routes.
9proxy.ts (Formerly middleware.ts)
Middleware Code that runs BETWEEN receiving a request and sending a response, used for logging, auth checks, redirects, and validation. Express apps chain middleware functions. In Next.js 16 the old middleware.ts file is now called proxy.ts, with an exported `proxy` function. "Like airport security. Every passenger (request) passes through before reaching the gate."proxy.ts sits at the project root (next to app/) and runs before matching requests. It's the renamed middleware
proxy.ts
import { NextResponse, type NextRequest } from "next/server";
export function proxy(request: NextRequest) {
return NextResponse.redirect(new URL("/home", request.url));
}
export const config = {
matcher: "/about/:path*",
};Terminal (migrating an older app)
npx @next/codemod@canary middleware-to-proxy .Use it for redirects, rewrites, and quick "is there a session cookie?" checks. Don't treat it as your security layer. That's the whole point of the Protected Routes guide.
10Styling with Tailwind v4 + Project Structure
Tailwind CSS Tailwind CSS A utility-first CSS framework: instead of writing custom CSS, you use small classes like 'bg-blue-500' or 'p-4' directly in your markup. Tailwind v4 is CSS-first, so setup is just `@import "tailwindcss";` in your CSS plus the PostCSS plugin, with no config file required. "Like a box of pre-labeled LEGO pieces. You build by combining small, predictable utilities."tailwind.config.ts to maintain: you import Tailwind in your CSS and customize with CSS. create-next-app sets this up for you; here's what it does if you add it by hand.
Terminal
npm install tailwindcss @tailwindcss/postcss postcsspostcss.config.mjs
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;app/globals.css
@import "tailwindcss";A structure Claude can navigate
my-app/
app/ # routes & pages
layout.tsx # root layout
page.tsx # homepage
globals.css # @import "tailwindcss";
dashboard/page.tsx
api/users/route.ts
components/ # reusable UI
lib/ # db.ts, utils.ts, server-only helpers
public/ # static files (logo.png)
auth.ts # if you use Auth.js
proxy.ts # optional request interception
.env.local # secrets (gitignored)
next.config.ts
postcss.config.mjs
AGENTS.md # guidance for coding agents
package.json11Upgrading an Older Project
Coming from Next.js 14 or 15? Upgrade on a branch, run the codemods, then let the type checker and build tell you what's left. A good prompt: "Upgrade this app to Next.js 16 following the official upgrade guide. Show me the plan first." (That's Plan Mode territory.)
Terminal
npm install next@latest react@latest react-dom@latest
npx @next/codemod@canary middleware-to-proxy .
npx @next/codemod@canary next-lint-to-eslint-cli .
npm run build # Turbopack builds by default; opt out with: next build --webpack- Add
awaitto everyparams,searchParams,cookies(), andheaders(). - Remove any
export const runtime = 'edge'. - Bump Node to 24 locally and on Vercel.
12Common Traps
"useState only works in Client Components"
You used a hook in a Server Component.
Fix: Put "use client" at the top of that component's file (ideally a small child component, not the whole page).
"params.slug is undefined" or a sync-access error
Old code reading params, searchParams, cookies(), or headers() without awaiting.
Fix: Type them as Promises and await them. Next.js 16 removed synchronous access.
My middleware.ts stopped running
Next.js 16 looks for proxy.ts and a function named proxy (or a default export).
Fix: Run npx @next/codemod@canary middleware-to-proxy .
Hydration mismatch
Server HTML didn't match the first client render. Usually window, localStorage, Date.now(), or random values used during render.
Fix: Move browser-only reads into useEffect, or render them only after mount.
Importing a Server Component into a Client Component
Client files can't import server-only code.
Fix: Pass the Server Component in as children or a prop from a Server Component parent.
Environment variable is undefined in the browser
Only variables prefixed NEXT_PUBLIC_ are bundled into client code.
Fix: Read secrets only on the server. Use NEXT_PUBLIC_ just for values that are safe to publish. Full rundown in Environment Variables.
"Why is this page stale?" / "Why is this page slow?"
With Cache Components nothing is cached unless you ask, and whatever you did cache stays until its cacheLife runs out or a tag is updated.
Fix: Add 'use cache' + cacheTag to slow, shared reads; call updateTag after writes.
Ready to build?
Spin up your first app, then add login and protect it properly.