---
title: "Next.js: Route Handlers"
slug: "nextjs-route-handlers"
category: "frontend"
tags: ["frontend", "nextjs", "route-handlers", "api", "http"]
status: "stable"
last_updated: 2026-08-14
summary: "Route handlers in Next.js 16: route.ts files, HTTP method exports, Request and Response, dynamic params, streaming, and when to prefer Server Actions."
related: ["[[frontend/nextjs]]", "[[frontend/nextjs-app-router]]", "[[frontend/nextjs-server-actions]]", "[[frontend/nextjs-middleware]]", "[[frontend/nextjs-caching]]", "[[backend/fastapi]]", "[[ops/vercel]]"]
---

## Overview

A route handler is a `route.ts` (or `route.js`) file inside `app/` that exports one function per HTTP method. The framework wires it as a Web-standard `Request`/`Response` handler. Route handlers replace `pages/api/` and they exist for one job: true HTTP endpoints. For mutations triggered from a React component, prefer [[frontend/nextjs-server-actions]].

## Export named methods from `route.ts`

A handler module exports `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, or `OPTIONS`. The file path determines the URL: `app/api/posts/route.ts` serves `/api/posts`.

```ts
// app/api/posts/route.ts
import { NextResponse } from "next/server";

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

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

A method not exported returns `405 Method Not Allowed`. Do not mix `route.ts` and `page.tsx` in the same folder; the framework rejects it.

## Read dynamic params from the second argument

For `app/api/posts/[id]/route.ts`, the framework passes `params` as the second argument. Params arrive as a Promise; await them, or the build rejects the synchronous access as of Next.js 16.

```ts
// app/api/posts/[id]/route.ts
export async function GET(
  _req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const post = await db.post.findUnique({ where: { id } });
  if (!post) return new Response("Not Found", { status: 404 });
  return Response.json(post);
}
```

Catch-all and optional catch-all segments (`[...slug]`, `[[...slug]]`) follow the same convention; the param type widens to `string[]`.

## Validate inputs and return real status codes

A handler is a public endpoint; validate everything. Return `400` for malformed input, `401` for missing auth, `403` for forbidden, `404` for missing, `409` for conflict, `422` for semantic validation failure.

```ts
import { z } from "zod";

const Input = z.object({ title: z.string().min(1).max(200) });

export async function POST(req: Request) {
  const parsed = Input.safeParse(await req.json());
  if (!parsed.success) {
    return Response.json({ errors: parsed.error.flatten() }, { status: 422 });
  }
  const post = await db.post.create({ data: parsed.data });
  return Response.json(post, { status: 201, headers: { Location: `/api/posts/${post.id}` } });
}
```

A handler is the right place to set custom headers (`Location`, `Cache-Control`, `Content-Disposition`). Server Actions cannot do this; if the response shape needs control, use a handler.

## Stream responses with `ReadableStream`

A handler can return a streaming `Response`. Use it for AI completions, large CSV exports, or server-sent events.

```ts
export async function GET() {
  const stream = new ReadableStream({
    async start(controller) {
      for await (const chunk of model.stream(prompt)) {
        controller.enqueue(new TextEncoder().encode(chunk));
      }
      controller.close();
    },
  });
  return new Response(stream, {
    headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" },
  });
}
```

For server-sent events, set `Content-Type: text/event-stream` and format chunks as `data: ...\n\n`. The client uses `EventSource` to consume them.

## Cache GETs explicitly; mutations are dynamic

`GET` handlers are dynamic by default (on a project without `cacheComponents` enabled, this matches the Next.js 15 behavior). Opt in to static caching with `export const dynamic = "force-static"` for handlers whose response never changes per request, or wrap the cached work in `"use cache"` on a Cache Components project; see [[frontend/nextjs-caching]].

```ts
export const dynamic = "force-static";
export const revalidate = 3600;

export async function GET() {
  const config = await loadPublicConfig();
  return Response.json(config);
}
```

For per-request caching, set `Cache-Control` headers on the response. Mutation methods (`POST`, `PUT`, `PATCH`, `DELETE`) are always dynamic; the runtime does not cache them. See [[frontend/nextjs-caching]] for the larger cache model.

## Reach for handlers when actions cannot

A handler is the right tool when:

- A non-React client calls the endpoint (mobile app, third-party webhook, cron, CLI).
- The response needs custom headers, status codes, or a non-JSON body.
- The client streams the response (SSE, chunked downloads).
- The path participates in a public API contract with stable URLs and methods.

For a mutation a React form triggers, prefer a server action: better type safety, integrated cache invalidation, progressive enhancement, no manual JSON parsing. See [[frontend/nextjs-server-actions]].

## Pick the runtime per handler

A handler can run on the Edge runtime for low-latency global execution, or on the Node runtime for native modules and longer compute. Set it per file:

```ts
export const runtime = "edge"; // or "nodejs"
```

Edge handlers cannot use `pg`, native `node:*` modules, or the filesystem. Use them for auth checks, redirects, geolocation, A/B routing, and small JSON responses. Use Node for Prisma, image processing, PDF generation, and anything Node-only. The trade-offs match the runtime guidance in [[ops/vercel]].

## Related

- [[frontend/nextjs]]
- [[frontend/nextjs-app-router]]
- [[frontend/nextjs-server-actions]]
- [[frontend/nextjs-middleware]]
- [[frontend/nextjs-caching]]
- [[backend/fastapi]]
- [[ops/vercel]]
