Route Handlers (API endpoints)

Next.js · Practical · 11 min

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 with await request.formData(), query params with request.nextUrl.searchParams.
  • Dynamic segments work the same as for pages: the second argument has params as a Promise. Type it with the global RouteContext<'/api/todos/[id]'> helper.
  • Use cookies() and headers() from next/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.ts exports GET/POST/... handlers using Web Request and Response
  • params arrive as a Promise in the second argument; type with RouteContext
  • 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.

Check yourself

  1. Your Server Component needs product data. A Route Handler at /api/products already returns it. What is the best approach?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: nextjs.org/docs/app/getting-started/route-handlers