Back to Knowledge
Updated Sep 2026

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."

, API routes, caching, and a build pipeline you never have to configure. This page teaches Next.js 16 conventions, which is what your AI assistant should be writing in 2026.

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."

is a React

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."

framework made by Vercel

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 draws the UI; Next.js adds everything around it that a real product needs: routing, server rendering, backend endpoints, caching, images, fonts, and a production build. If React is the engine, Next.js is the whole car.

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.

TopicOld habitNext.js 16
Request interceptionmiddleware.tsproxy.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."

with export function proxy()
params, cookies, headersRead synchronouslyAlways await them
Bundlernext dev --turbopackTurbopack

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."

is the default for dev and build
CachingImplicit fetch caching, unstable_cacheCache 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."

: dynamic by default, opt in with 'use cache'
Lintingnext lintRemoved. Run ESLint (or Biome) directly
Stylingtailwind.config.tsTailwind v4 is CSS-first, no config file needed
Runtimeruntime = '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:

TypeScript
ESLint
Tailwind CSS
App Router
Turbopack
@/* import alias
AGENTS.md

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."

lives in 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-post

Dynamic 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.tsx

The UI for a route. No page.tsx, no URL.

layout.tsx

Shared shell (navbar, footer). Persists across navigation and doesn't re-render.

loading.tsx

Instant loading UI while the route streams in. React Suspense under the hood.

error.tsx

Error boundary for a route segment, so one crash doesn't take down the app. Must be a Client Component.

not-found.tsx

Custom 404, also shown when you call notFound().

route.ts

A backend endpoint (GET, POST, etc.) instead of a page.

proxy.ts

Runs before matching requests: redirects, rewrites, quick auth checks. Replaces middleware.ts.

next.config.ts

Framework 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 <Counter /> just fine. The page stays on the server; only the button's code goes to the browser. See Client Component

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."

in the glossary for more.

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 Actions

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)

proxy.ts sits at the project root (next to app/) and runs before matching requests. It's the renamed middleware

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."

: same job, clearer name, and it always runs on the Node.js runtime.

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."

v4 is CSS-first. There's no 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 postcss

postcss.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.json

11Upgrading 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 await to every params, searchParams, cookies(), and headers().
  • Remove any export const runtime = 'edge'.
  • Bump Node to 24 locally and on Vercel.
Official Next.js 16 upgrade guide

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.