Route Handlers are how you build API endpoints in Next.js. You create a route.ts file, export functions named after HTTP methods (GET, POST, PUT, DELETE), and Next.js maps them to URLs based on the file's location in app/. Under the hood, they use the Web Platform's Request and Response APIs — the same ones that power Service Workers and Cloudflare Workers — so everything you learn transfers outside Next.js.
The mental model is straightforward: route.ts is to APIs what page.tsx is to UI. They cannot coexist in the same directory (pick one per folder), and they run exclusively on the server. This means you can safely access databases, environment variables, and secrets without anything leaking to the browser.
Where things get interesting is the caching behavior. A GET handler with no dynamic input gets statically evaluated at build time — effectively becoming a JSON file on the CDN. The moment you read cookies, headers, or search params, Next.js switches to dynamic rendering. POST, PUT, and DELETE are always dynamic. Understanding this split is the key to building fast APIs that stay fresh when they need to.
Master next.js route handlers
Take the Next.js Full-Stack course with hands-on lessons and challenges.
Two files handle the full CRUD lifecycle. The collection route (app/api/posts/route.ts) handles listing with pagination and creation. The item route (app/api/posts/[id]/route.ts) handles fetching, updating, and deleting a single resource. Note that params are a Promise that must be awaited — this changed in Next.js 15.
Always validate both URL params and request body. Params are strings by default — a bare [id] could be anything. Zod's safeParse avoids uncaught exceptions and gives you structured errors to return. The refine on UpdateUserSchema prevents empty PATCH requests that would hit the database for nothing.
Webhook endpoints must verify signatures to prevent forged requests. Read the body as text (not JSON) because signature verification needs the raw payload. The idempotency check prevents duplicate processing when Stripe retries delivery. Always return 200 quickly — if processing takes too long, Stripe assumes the webhook failed and retries.
ReadableStream lets you send data incrementally. The SSE format requires 'data: ...' lines separated by double newlines. The 'event:' field lets clients listen for specific event types. Set Cache-Control to no-cache — you never want a CDN caching a live event stream. Mark the route as force-dynamic so Next.js does not try to statically render it.
Use request.formData() to parse multipart uploads. Always validate file type and size before processing — never trust the client. The file is a Web API File object, so call arrayBuffer() to get the raw bytes. In production, upload to cloud storage (S3, R2, Vercel Blob) rather than writing to the local filesystem, which does not persist on serverless deployments.
Extract the JWT verification logic into a shared utility so every protected route stays clean. Return 401 for missing or invalid tokens and 403 for insufficient permissions — they mean different things and clients handle them differently. Use jose (Edge-compatible) over jsonwebtoken (Node-only) if your Route Handler might run on the Edge runtime.
Placing `route.ts` and `page.tsx` in the same directory — Next.js throws a build error because it cannot serve both a page and an API from the same path
Put API routes in a separate folder. Use `app/api/users/route.ts` for the API and `app/users/page.tsx` for the UI, or nest the API under a different segment like `app/dashboard/api/route.ts`.
Exporting lowercase method names like `get()` or `post()` — they are silently ignored and the route returns 405 Method Not Allowed
Method exports must be uppercase: `export async function GET()`, `export async function POST()`. This matches the HTTP specification and is how Next.js identifies which methods the route supports.
Assuming GET handlers are always dynamic — a GET handler that does not read cookies, headers, or search params gets statically evaluated at build time and cached indefinitely
If your GET handler returns data that changes (e.g., database queries), export `const dynamic = 'force-dynamic'` or `const revalidate = 60` to control caching. Otherwise the response is frozen at build time.
Reading `request.json()` twice in the same handler — the body is a stream that can only be consumed once. The second call returns an error or empty result
Store the parsed body in a variable: `const body = await request.json()`. Then reference `body` wherever you need the data. If you need both text and JSON, read as text first and then `JSON.parse()` it.
Route Handlers are Next.js's mechanism for building server-side API endpoints using standard Web APIs. Create a `route.ts` file anywhere in `app/`, export uppercase HTTP method functions, and the folder path becomes your URL. GET handlers are cached by default unless they read dynamic inputs like cookies or search params — POST, PUT, and DELETE are always dynamic. Use them for external API consumers, webhooks, and file uploads. For internal form mutations in your own React components, prefer Server Actions instead. Validate inputs with Zod, return proper status codes, set cache headers on public endpoints, and always verify webhook signatures.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.