route-handler-design

Installation
SKILL.md

Route Handler Design

Concept of the skill

A Next.js Route Handler is a file named route.ts (or route.js) under the app/ directory that exports one async function per HTTP method (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS). Each export receives a standard Web Request and returns a standard Web Response (or NextResponse which extends it). The file shape IS the contract — the filesystem path defines the URL; the export name defines the method; the function body defines the handler. No Node-style req/res; no middleware chain; no per-method routing config. Unhandled methods auto-return 405. A route.ts and a page.tsx cannot share a path; one URL has one purpose. Every Route Handler is a PUBLIC HTTP surface — "only our frontend calls this" is not a security boundary; authenticate, authorize, and validate inside the handler. The five body-reading methods (request.json(), request.formData(), request.text(), request.blob(), request.arrayBuffer()) are mutually exclusive — a body can be read only once; if you need both raw and parsed (for webhook HMAC verification), read text() and parse yourself.

Replaces the Pages Router's Node-style /api/* endpoints with Web-standard handlers that compose into the App Router's file-system convention. Solves the problem that Pages Router endpoints were tied to Node's req/res interface (not portable to Edge, Cloudflare Workers, or Deno) and required separate routing-layer registration. The deeper purpose emerges from the App Router introducing Server Actions as a competing mutation surface: Route Handlers become purely the public HTTP endpoint surface, used when the caller is not your own typed UI — mobile apps, third-party integrations, webhooks, server-to-server calls, cross-origin client-side fetches, binary downloads, server-sent events, anything that benefits from explicit HTTP semantics. The single most common Route Handler mistake in App Router code is using one for an internal mutation that a Server Action would serve better — duplicate type contracts, manual fetch wiring, no progressive enhancement, no built-in revalidation.

Distinct from server-actions-design, which owns the internal mutation surface — a function the bundler turns into a network call from your own UI; this skill owns the public-endpoint surface for callers who aren't the Next.js bundler's typed call sites. Use Server Actions when the only caller is this app's UI; use Route Handlers when the caller is anything else or when you need fine-grained HTTP control. Distinct from a Server Component reading data on render — that should call the data source (DB/service) directly, never self-fetch its own Route Handler. Distinct from middleware-patterns, which runs once before route resolution and applies to many routes via a matcher (the file is middleware.ts, renamed to proxy.ts in Next 16 and Node-only there) — Route Handlers run for one route and one method after that pass. Distinct from api-design, which owns the broader REST/contract/versioning discipline — this skill owns the Next.js implementation surface that hosts the contract. Distinct from http-semantics, which owns the abstract method/status/header semantics — this skill owns honoring them in App Router. Distinct from webhook-integration, which owns the full webhook reliability story (HMAC details, idempotency keys, retry semantics, dead-letter queues) — this skill covers the endpoint surface only. Distinct from streaming-architecture, which owns the cross-cutting streaming model — this skill covers Route Handler streaming specifically. Distinct from client-server-boundary (which owns the bundler's component split, a different boundary). A Route Handler is to a Next.js app what a service window at a government office is to its workflow — different windows handle different services (GET /posts, POST /comments); each window has a posted sign saying which forms it accepts and what stamps it returns; you do not walk into the back office (Server Action) unless you work there. The window is the contract: filesystem path = window number, export name = service offered, function body = the clerk's actual work. The wrong mental model is that Route Handlers are "Next.js's API routes" and that every HTTP endpoint in a Next.js app belongs there. They are, but with a critical refinement after App Router: internal mutations from your own UI belong in Server Actions, not Route Handlers. Using a Route Handler for an internal mutation duplicates the type contract (request shape + response shape + manual fetch wiring + manual revalidation), loses progressive enhancement (form submission without JS), and loses built-in revalidation. Adjacent misconceptions: that request.body can be read multiple times (it cannot — pick one of json/formData/text/blob/arrayBuffer; if you need both raw and parsed, read text() and parse yourself); that GET responses are statically cached by default (they were in Next 14; since Next 15 Route Handlers are NOT cached by default — you opt IN with export const dynamic = 'force-static', so the stale-data risk has flipped to an accidental-uncached-cost risk, not a leaked-cached-response risk — though force-static on a per-user GET still leaks one user's data to all); that params/cookies()/headers() can be read synchronously (they cannot since Next 16 — await them); that request.geo/request.ip exist on NextRequest (removed in Next 15 — they were middleware-only even before that; use geolocation()/ipAddress() from @vercel/functions); that a Server Component should fetch its own Route Handler to read data (it should not — that adds an HTTP round-trip and can fail during prerender/build; call the data function directly); that "only our frontend calls this endpoint" is a security boundary (it is not — any client can send the same request; authenticate/authorize/validate inside the handler); that calling request.json() on a webhook is fine (it is not — the parse can mutate whitespace and break HMAC verification; read raw bytes via text(), verify, then parse); that webhooks should do heavy work inline (they should not — vendors retry slow ACKs, producing duplicate processing; ACK fast with 200 and durably enqueue the work); that Access-Control-Allow-Origin: '*' works with credentialed requests (it does not — browsers refuse credentials with wildcard origin; allowlist explicit origins); that Edge runtime is always faster (it is not — Vercel folded standalone Edge Functions onto Vercel Functions and recommends Node, though runtime = 'edge' stays selectable; Fluid Compute + bytecode caching closed most of the cold-start gap, while Edge keeps a tight capability surface and many vendor SDKs require Node's crypto).

Coverage

The discipline of designing Next.js App Router route.ts / route.js handlers: the file-and-export convention (one async function per HTTP method, one URL per filesystem path), the Web-standard Request/Response interface that replaces the Node req/res pair, the body-parsing primitives (request.json / formData / text / blob / arrayBuffer) and one-shot body consumption, the off-by-default GET caching behavior (Next 15+) and the opt-in mechanisms (dynamic = 'force-static', the Cache Components use cache model), async request APIs (params, cookies(), headers()await required since Next 16) and RouteContext typed params, dynamic segments and search-param access, the rule that every Route Handler is a public surface that must authenticate/authorize/validate inside the handler, manual CORS, the Edge-vs-Node runtime choice (Node now default and recommended) and deployment knobs (maxDuration, preferredRegion, static-export limits), streaming responses via ReadableStream with pull() backpressure, status-code and header discipline, error response shaping, the canonical webhook pattern (verify signature against the raw body before parsing, ACK fast, after() for short post-response work), a version-drift map across Next 13→16, and the central design rule that determines when a Route Handler is the right surface at all: the caller is not your own typed UI or render tree.

Philosophy of the skill

The App Router collapsed three things that used to be distinct in the Pages Router:

  • Pages: rendered routes — moved to Server Components and page.tsx.
  • API routes: HTTP endpoints under /api/* — moved to Route Handlers in route.ts.
  • Custom server handlers: middleware, edge functions — partly absorbed by middleware.ts (renamed proxy.ts in Next 16), partly by per-route runtime selection.
Installs
2
First Seen
May 18, 2026
route-handler-design — jacob-balslev/skills