Skip to content
Next.js6 min read

Route Handlers: Building APIs Inside Next.js

Route Handlers give your Next.js app real HTTP endpoints using web-standard Request and Response. When to use them over Server Actions, and the details that matter in production.

Not everything is a page. Webhooks arrive, mobile apps need JSON, third parties want an endpoint to call. Route Handlers are the App Router's answer: files that export functions named after HTTP methods, built on the standard Request and Response objects rather than Express-style wrappers. If you know the Fetch API, you already know most of the surface.

The basics

ts
// app/api/subscribers/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  const subscribers = await db.subscriber.findMany();
  return NextResponse.json(subscribers);
}

export async function POST(request: Request) {
  const body = await request.json();
  const created = await db.subscriber.create({ data: body });
  return NextResponse.json(created, { status: 201 });
}

Dynamic segments work exactly as they do for pages — a route.ts inside app/api/subscribers/[id]/ receives the id via params. A route segment can contain a page or a route handler, never both. And because the Request object is the web-standard one, testing a handler is just calling a function with a Request and asserting on the Response — no server spin-up required.

Route Handlers or Server Actions?

Since Server Actions arrived, the honest answer is that your own frontend rarely needs a Route Handler for mutations. The decision rule we use:

  • Server Actions: forms and mutations triggered by your own React components
  • Route Handlers: webhooks, OAuth callbacks, and anything called by a non-React client
  • Route Handlers: streaming responses, file downloads, and endpoints needing custom headers or status codes
  • Route Handlers: a public or versioned API that outlives any single frontend

In short: actions for your app talking to itself, handlers for the outside world talking to your app.

Caching: know the default

Since Next.js 15, GET handlers are dynamic by default — every request executes the function. You can opt a handler into static behaviour with a route config export if it serves genuinely static data, but most API endpoints want the dynamic default. The old Next.js 14 behaviour of caching GET handlers unless told otherwise caught out nearly everyone; if you are upgrading, this is one of the changes to re-verify.

Beyond JSON

Because handlers speak the platform Response type, they are not limited to JSON. Return a ReadableStream and you have server-sent events for progress updates or token-by-token AI output; set a content-disposition header and you have CSV exports; proxy a storage bucket and you have authenticated file downloads. These jobs have no Server Action equivalent, and they are where handlers earn their keep.

Choose the runtime per route as well: the Node runtime is the default and supports every library, so declare the edge runtime only for handlers that genuinely benefit and use nothing Node-specific.

Production details that matter

Validate every body with a schema before touching it, and return errors in one consistent shape so clients can handle failures uniformly. For webhooks, verify the signature before parsing anything else, and return quickly — do slow work after responding or hand it to a queue. Authenticate handlers explicitly: they are not covered by whatever protects your pages unless you write that check.

ts
const result = schema.safeParse(await request.json());
if (!result.success) {
  return NextResponse.json(
    { error: 'Invalid payload', issues: result.error.issues },
    { status: 422 }
  );
}

If browsers on other origins will call your API, handle CORS explicitly — preflight OPTIONS requests included — because nothing does it for you. And rate-limit anything unauthenticated; a public endpoint without a limiter is an invitation.

Route Handlers will not replace a dedicated backend for a large service surface, and they do not need to. For the API a typical product actually requires — a handful of well-built endpoints living beside the frontend that consumes them — they are exactly enough. Keep the surface small and well-tested, and promote it to a dedicated service only when traffic or team structure demands it.

Start your project

Have an idea? Let's ship it together.

Tell us what you're building — we'll reply within one business day with an honest take and a clear next step.