Handbooks / Next.js / Chapter 5
Server Actions and Route Handlers
7 pages · ~5 min✓ Reviewed
Builds on Navigation and the Client Router Cache. Next up: Styling and Client State.
Mutation Toolkit Overview
1. The Three Tools
- Server Action: async function marked
'use server', runs on the server - Called from forms or client code; Next.js exposes a POST endpoint for it
- Route Handler:
route.tsexporting GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS - Builds a public HTTP endpoint on Web
Request/Response - Middleware: runs before the request completes, matched by path
- Jobs: redirect, rewrite, set headers, gate access
typescript
'use server' export async function save(fd: FormData) { const title = fd.get('title') }
Server Action in its own file
2. Which One to Use
| Need | Tool |
|---|---|
| Mutation from your own UI | Server Action |
| Webhook, public API, non-UI client | Route Handler |
| Cross-cutting check before rendering | Middleware |
3. Segment Conflict
One owner per route
A route.ts cannot sit beside a page.tsx at the same route segment. They conflict.
Server Actions and Forms
1. Define and Wire
'use server'inline in a Server Component, or atop a file<form action={fn}>passes FormData:formData.get('name')- Forms work before hydration (progressive enhancement)
ts
'use server' export async function create(prev, fd) { const title = fd.get('title') // validate, auth, write redirect('/posts') }
2. State, Pending, Safety
useActionState(action, init)→[state, formAction, pending]useFormStatusin a child readspending- Actions are public POST endpoints: validate (zod), re-check auth
- Validate
- Auth
- Write DB
- Revalidate
- redirect
Revalidation After Mutations
1. Two Ways to Invalidate
revalidatePath('/posts'): invalidate that route's data and output on the next visit- Second arg
'layout'or'page'scopes it:revalidatePath('/', 'layout') revalidateTag('posts', 'max'): invalidate every entry taggedposts- One-argument
revalidateTag('posts')is deprecated in Next.js 16 updateTag('posts'): read-your-writes inside a Server Action
2. Order of Steps
- 1Validateinput and auth
- 2Write DBmust succeed
- 3Invalidatepath or tag
- 4Redirectoptional
typescript
// read: tag the fetch fetch(url, { next: { tags: ['posts'] } }) // Server Action, after the write revalidateTag('posts', 'max')
same string tags and invalidates
3. Path vs Tag
| Path | Tag | |
|---|---|---|
| Style |
Route Handlers (route.ts)
1. Define the Handler
- File: app/api/items/route.ts (any segment)
- Export named async functions per method:
GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS - Return
Response.json(data, { status: 201 })ornew Response(body, { headers }) NextResponseadds helpers: cookies, redirects
2. Params and Input
typescript
// app/api/items/[id]/route.ts export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) { const { id } = await params const body = await request.json() return Response.json({ id, body }, { status: 201 }) }
params is a Promise in recent versions: await it
| Read | API |
|---|---|
| JSON body | await request.json() |
| Form body | await request.formData() |
| Raw body | await request.text() |
| Query |
Request/Response APIs and Middleware
1. Request Helpers
cookies()andheaders()are async: await themcookies().set/delete: only in Actions and Route Handlers, not render
typescript
const jar = await cookies() const h = await headers()
read both on the server; set only in actions or handlers
| Adds | |
|---|---|
NextRequest | nextUrl, cookies |
NextResponse | json redirect rewrite next |
2. Middleware and proxy
- Next.js 16:
middleware.tsis deprecated, renamedproxy.ts proxy.tsexportsproxy(request); runs on Node.js- Root or
src/;config.matcherlimits paths - Jobs: auth redirect, locale rewrite, headers; keep it light
Cheat Sheet: Which Tool and Key Snippets
1. Which Tool
| Need | Use |
|---|---|
| Own form or button mutation | Server Action |
| Webhook, third-party or mobile client | Route Handler |
| Pre-render redirect, rewrite, header | Middleware |
2. Skeletons
- Action: validate, auth check, write DB, revalidate, redirect
ts
'use server' export async function create(prev, formData) { // validate, auth check, write DB revalidatePath('/x') redirect('/x') }
- Handler: read the body, return a
Responsewith a status
ts
export async function POST(req: Request) { const body = await req.json() return Response.json({ ok: true }, { status: 201 }) }
3. Revalidate and Middleware
Check yourself
Spot the bug: an action runs try { await save(); redirect('/posts') } catch { return { error: 'Failed' } }. What does the user see on success?
- The error message, even though the save worked
redirect()throws internally, so thecatchswallows it- Fix: call
redirectafter the try/catch
An action edits /blog/[slug] and calls revalidatePath('/'). Why might /blog/hello still be stale?
'/'alone refreshes only that page, not nested routes- Use
revalidatePath('/', 'layout'), orrevalidateTagon a tagged fetch
Middleware redirects logged-out users away from /dashboard/:path*. Is deleteAccount on that page safe without its own check?
- No: actions are public POST endpoints, directly callable
- Matchers can miss routes, so re-check auth inside the action