Webhooks, public APIs, mobile backends and file downloads need real HTTP endpoints alongside your pages.
A Route Handler is a route.ts file that exports functions named after HTTP methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. It lives in app/ like a page, for example app/api/todos/route.ts serves /api/todos. A folder can contain page.tsx or route.ts, not both.
Handlers use the standard Web APIs: they receive a Request (or NextRequest, which adds nextUrl and cookie helpers) and return a Response. Response.json(data, { status: 201 }) is the common shortcut.
- Read JSON bodies with
await request.json(), forms withawait request.formData(), query params withrequest.nextUrl.searchParams. - Dynamic segments work the same as for pages: the second argument has
paramsas a Promise. Type it with the globalRouteContext<'/api/todos/[id]'>helper. - Use
cookies()andheaders()fromnext/headers, or read them from the request.
When should you use them? For your own UI, prefer Server Components for reads and Server Actions for mutations. Use Route Handlers for things that must be HTTP endpoints: webhooks from Stripe or GitHub, APIs consumed by mobile apps or third parties, OAuth callbacks, generated files, and streaming responses.
Like actions, they are public: authenticate and validate in every handler.
Example
// app/api/todos/[id]/route.ts
import { NextRequest } from 'next/server'
const todos = new Map([['1', { id: '1', title: 'Write docs', done: false }]])
export async function GET(_req: NextRequest, ctx: RouteContext<'/api/todos/[id]'>) {
const { id } = await ctx.params
const todo = todos.get(id)
if (!todo) return Response.json({ error: 'Not found' }, { status: 404 })
return Response.json(todo) // 200 {"id":"1",...}
}
export async function PATCH(request: NextRequest, ctx: RouteContext<'/api/todos/[id]'>) {
const { id } = await ctx.params
const body = await request.json() // e.g. { "done": true }
const todo = todos.get(id)
if (!todo) return Response.json({ error: 'Not found' }, { status: 404 })
if (typeof body.done !== 'boolean') {
return Response.json({ error: 'done must be boolean' }, { status: 400 })
}
todo.done = body.done
return Response.json(todo)
}Remember
route.tsexports GET/POST/... handlers using Web Request and Responseparamsarrive as a Promise in the second argument; type withRouteContext- Use them for webhooks and external APIs; prefer Server Components and Actions for your own UI
Watch out for
- Fetching your own Route Handler from a Server Component; call the data function directly instead of making an HTTP round trip.