Route Handlers sind die App-Router-Antwort auf klassische API-Routen: Sie definieren HTTP-Endpunkte in einer route.ts-Datei, indem Sie benannte Funktionen wie GET, POST, PUT, PATCH oder DELETE aus dem Modul exportieren. Jede Funktion erhält ein Request-Objekt und liefert ein Response zurück, basierend auf der Web-Fetch-API und vollständig typisiert über NextRequest und NextResponse. In Next.js 16 sind Route Handlers per Default uncached, und Caching wird über "use cache" oder die Konfiguration explizit aktiviert.
Route Handlers ersetzen die pages/api-Routen aus dem Pages Router und leben unter app/.../route.ts.
Sie exportieren je HTTP-Verb eine Funktion (GET, POST, …); alle anderen Verben antworten automatisch mit 405.
Ab Next.js 16 sind GET-Handler standardmäßig dynamisch. Explizites Caching erfordert "use cache" oder export const dynamic = "force-static".
Streaming funktioniert über native ReadableStream-Objekte, ideal für KI-Token-Streams oder große CSV-Downloads.
Dynamische Segmente wie app/items/[id]/route.ts erhalten Parameter über das zweite Funktionsargument als Promise.
Für serverseitige Mutationen aus Formularen sind Server Actions oft die bessere Wahl als ein selbstgebauter POST-Endpunkt.
Was sind Route Handlers in Next.js?
Ein Route Handler ist eine TypeScript- oder JavaScript-Datei mit dem Dateinamen route.ts (oder route.js), die innerhalb des app/-Verzeichnisses liegt und einen oder mehrere HTTP-Verb-Handler exportiert. Der Pfad zur Datei entspricht direkt der URL: app/api/users/route.ts wird unter /api/users erreichbar. Im Gegensatz zu React-Server-Components rendert ein Route Handler kein JSX. Er liefert ausschließlich Daten zurück, typischerweise JSON, aber auch Binärdaten, Streams oder XML.
Intern basiert das System auf den Web-Standards Request und Response, ergänzt durch Next-spezifische Erweiterungen in NextRequest (nützliche Helpers wie req.nextUrl.searchParams, req.cookies und req.geo) und NextResponse (Helpers wie NextResponse.json() und NextResponse.redirect()). Da diese Objekte direkt aus der Fetch-API stammen, läuft derselbe Code in Node.js, am Edge und in lokalen Tests. Das hat mir bei der Migration einer alten Express-API tatsächlich Tage erspart.
Unterschied zu klassischen API Routes im Pages Router
Der wichtigste Unterschied: pages/api/*.ts exportiert eine einzige Default-Funktion, in der man req.method manuell prüfen muss. Route Handlers exportieren stattdessen benannte Funktionen pro HTTP-Verb. Das eliminiert eine ganze Klasse von Bugs (das berüchtigte „Vergessen, das Method-Switch zu aktualisieren") und liefert für nicht-implementierte Verben automatisch einen 405 Method Not Allowed.
Ein weiterer Bruch: Pages-Router-APIs nutzen req und res im Express-Stil mit res.status(200).json(...). Route Handlers folgen dem Fetch-Standard, Sie geben also ein Response-Objekt zurück. Damit verschwindet auch die res.end()-Fallgrube, in der Pages-Routen hängenbleiben konnten. Die folgende Tabelle fasst die zentralen Unterschiede zusammen:
Aspekt
Pages Router (pages/api)
App Router (route.ts)
Dateikonvention
pages/api/users.ts
app/api/users/route.ts
Methoden-Routing
if (req.method === 'POST')
Exportierte Funktion POST
Request-/Response-API
Node-spezifisch (req, res)
Web-Fetch (Request, Response)
Default-Caching (GET)
Dynamisch
Dynamisch ab Next.js 16
Streaming-Support
Über res.write() + res.end()
Native ReadableStream
Edge Runtime
Über config.runtime
Über export const runtime = 'edge'
Body-Parsing
Automatisch (deaktivierbar)
Manuell via req.json()
Den ersten GET- und POST-Handler schreiben
Also, fangen wir klein an. Die kleinste sinnvolle Implementierung braucht drei Zeilen: Sie legen app/api/health/route.ts an, exportieren GET und geben JSON zurück. Ich empfehle, von Anfang an mit NextResponse.json() zu arbeiten. Der Helper setzt den Content-Type-Header automatisch und akzeptiert ein zweites Argument für Statuscode und zusätzliche Header.
// app/api/health/route.ts
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ status: "ok", time: new Date().toISOString() });
}
Ein POST-Handler liest typischerweise den Request-Body, validiert ihn und persistiert die Daten. Mit Zod als Schema-Validator lässt sich das sauber und typsicher umsetzen. Validierungsfehler werden mit 422 Unprocessable Entity beantwortet:
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { db } from "@/lib/db";
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(120),
});
export async function POST(req: NextRequest) {
const json = await req.json().catch(() => null);
const parsed = CreateUserSchema.safeParse(json);
if (!parsed.success) {
return NextResponse.json(
{ error: "ValidationError", issues: parsed.error.issues },
{ status: 422 }
);
}
const user = await db.user.create({ data: parsed.data });
return NextResponse.json(user, { status: 201 });
}
Dynamische Segmente und Query-Parameter
Dynamische URL-Teile entstehen durch eckige Klammern im Dateipfad: app/api/users/[id]/route.ts matcht /api/users/42. Seit Next.js 15 sind die params ein Promise und müssen mit await ausgelesen werden, als Folge der Async-Request-API, die auch cookies() und headers() in Server Components betrifft. Das Signature sieht so aus:
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
type Ctx = { params: Promise<{ id: string }> };
export async function GET(req: NextRequest, ctx: Ctx) {
const { id } = await ctx.params;
const user = await db.user.findUnique({ where: { id } });
if (!user) {
return NextResponse.json({ error: "NotFound" }, { status: 404 });
}
return NextResponse.json(user);
}
export async function DELETE(req: NextRequest, ctx: Ctx) {
const { id } = await ctx.params;
await db.user.delete({ where: { id } });
return new NextResponse(null, { status: 204 });
}
Query-Parameter werden über req.nextUrl.searchParams gelesen, ein URLSearchParams-Objekt mit Methoden wie get(), getAll() und has(). Für Pagination ist das Standard-Pattern const page = Number(req.nextUrl.searchParams.get("page") ?? "1"). Catch-all-Segmente ([...slug]) und optional-catch-all ([[...slug]]) funktionieren identisch wie in Seiten-Routen und sind nützlich für Proxy-Endpunkte oder Webhook-Routen mit variabler Pfadtiefe.
Caching-Verhalten in Next.js 16
Hier hat sich in Next.js 15 und 16 viel verändert. Bis Version 14 wurden GET-Handler standardmäßig statisch gecached. Seit Version 15 ist das Verhalten umgekehrt: Alle Route Handlers gelten als dynamisch, es sei denn, Sie aktivieren Caching explizit. In Next.js 16 ist das endgültig Standard, und ein häufiger Stolperstein bei der Migration. Bestehende Endpunkte werden plötzlich bei jedem Request neu ausgeführt, was man oft erst an der Rechnung des Datenbank-Providers merkt.
Es gibt drei Wege, GET-Handler wieder zu cachen: die export const dynamic = "force-static"-Direktive (mit optionalem revalidate), die "use cache"-Direktive innerhalb der Funktion, oder das neue Cache-Components-Modell. Wenn Sie das Cache-Components-Modell tiefer verstehen möchten, lesen Sie unseren Leitfaden zu Cache Components und der use cache-Direktive.
// app/api/posts/route.ts — statisch generiert, alle 60s revalidiert
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
export const dynamic = "force-static";
export const revalidate = 60;
export async function GET() {
const posts = await db.post.findMany({ take: 50 });
return NextResponse.json(posts);
}
Streaming-Responses mit ReadableStream
Streaming-Antworten sind eine der mächtigsten, aber am wenigsten genutzten Funktionen von Route Handlers. Sie sind ideal für LLM-Token-Streams (Server-Sent Events von OpenAI, Anthropic etc.), große CSV-Exports oder Echtzeit-Logs. Da Response bereits einen ReadableStream als Body akzeptiert, ist die API erfreulich nah am Web-Standard:
// app/api/events/route.ts
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 5; i++) {
controller.enqueue(encoder.encode(`event: tick
data: ${i}
`));
await new Promise((r) => setTimeout(r, 1000));
}
controller.close();
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
},
});
}
Streaming-Endpunkte funktionieren in beiden Runtimes, profitieren aber besonders am Edge. Dort liefert die Plattform die ersten Bytes oft in unter 50 ms aus, was die wahrgenommene Latenz bei KI-Antworten deutlich verbessert. Achten Sie darauf, Cache-Control: no-cache zu setzen. Vercel, Cloudflare und andere CDNs würden Streams sonst puffern und damit den eigentlichen Zweck zerstören (ich habe genau diesen Fehler beim ersten Production-Deployment eines Chat-Streamings gemacht).
Wie konfiguriert man CORS in Route Handlers?
Route Handlers bekommen keine automatische CORS-Konfiguration. Sie sind selbst verantwortlich für Access-Control-Allow-Origin und Preflight-Requests. Das ist bewusst so, weil restriktive Defaults sicherer sind als permissive. Für eine Public-API mit Browser-Clients implementieren Sie einen OPTIONS-Handler und spiegeln Origin und Methoden in den Response-Headern:
Für Single-Origin-Apps ist Middleware oft sauberer, weil sie CORS-Header projektweit konsistent setzt. Wer Middleware seit der CVE-Migration zu proxy.ts noch nicht angefasst hat, findet im Beitrag zu Next.js Middleware-Sicherheit und proxy.ts einen vollständigen Migrationspfad. Die offizielle MDN-Referenz zu CORS ist nebenbei eine sehr gute Quelle, falls Sie die Header-Semantik tiefer verstehen wollen.
Edge Runtime vs. Node.js Runtime
Jeder Route Handler kann in einer von zwei Laufzeiten ausgeführt werden. Standard ist Node.js, kompatibel mit nativen Modulen, dem gesamten npm-Ökosystem und Prisma/Drizzle-Treibern, die TCP-Verbindungen brauchen. Die Edge Runtime hingegen ist eine V8-Isolate-Umgebung ohne Node-APIs, dafür mit Kaltstarts unter 50 ms und globaler Verteilung. Sie aktivieren sie pro Datei:
Cookies lesen Sie über req.cookies.get("name"), und neue Cookies setzen Sie über die Response: response.cookies.set({ name, value, httpOnly: true, secure: true, sameSite: "lax" }). Für komplexere Auth-Flows nutzen Sie eine Library wie Auth.js v5, die HTTP-Only-Session-Cookies signiert und das JWT-Handling übernimmt. Eine ausführliche Anleitung dazu finden Sie in unserem Artikel zu Auth.js v5 im App Router.
Für einfache API-Token-Authentifizierung reicht oft ein Authorization: Bearer <token>-Header. Das untenstehende Pattern abstrahiert die Prüfung in eine kleine Helper-Funktion, die in jedem geschützten Handler aufgerufen wird und im Fehlerfall früh mit 401 antwortet:
Ehrlich gesagt ist die größte Falle bei Route Handlers, dass uncaught Exceptions als generischer 500 mit leerem Body nach außen erscheinen. Keine hilfreiche Fehlermeldung, kein error_code. Wickeln Sie deshalb jede Handler-Logik in einen try/catch-Block und mappen Sie bekannte Fehlerklassen auf passende Statuscodes. Strukturierte Fehler (RFC 7807, „Problem Details for HTTP APIs") sind dabei das Mittel der Wahl:
Setzen Sie den richtigen Statuscode: 200 für Erfolg, 201 nach erfolgreichem Create, 204 wenn kein Body zurückgegeben wird (Delete), 400 für Client-Fehler in der Syntax, 401 ohne Auth, 403 mit Auth aber ohne Recht, 404 für unbekannte Ressourcen, 409 für Konflikte (etwa doppelte E-Mail), 422 für semantische Validierungsfehler und 429 für Rate Limits. Konsistente Statuscodes machen Ihre API für Clients vorhersagbar und vereinfachen Monitoring-Dashboards. Wer das Statuscode-Universum noch nicht im Kopf hat, dem hilft die MDN-Übersicht zu HTTP-Statuscodes als praktisches Spickzettel.
Wann Route Handlers, wann Server Actions?
Diese Frage taucht häufig auf, weil Server Actions seit Next.js 14 ebenfalls serverseitigen Code aus dem Client aufrufen können. Die Faustregel: Server Actions für interne Mutationen aus eigenen React-Komponenten, Route Handlers für alles Externe. Externe Webhook-Empfänger (Stripe, GitHub, Auth-Provider-Callbacks), Public REST APIs, mobile App-Backends, Endpunkte für Drittanbieter-Integrationen, all das braucht stabile URLs, HTTP-Verben und volle Kontrolle über Response-Header. Genau das liefern Route Handlers.
Für In-App-Formulare ist hingegen eine Server Action meist die bessere Wahl: weniger Boilerplate, automatische Progressive Enhancement, eingebaute Revalidierung und CSRF-Schutz. Wer das Pattern noch nicht kennt, findet eine vollständige Einführung im Beitrag zu Next.js Server Actions, Formularen und Mutationen. Die offizielle Next.js-Dokumentation zu Route Handlers empfiehlt explizit dieselbe Aufteilung.
Werden Route Handlers in Next.js 16 standardmäßig gecached?
Nein. Seit Next.js 15 sind alle Handler, auch GET, standardmäßig dynamisch. Caching muss explizit über export const dynamic = "force-static", "use cache" oder das Cache-Components-Modell aktiviert werden.
Kann ich Route Handlers und Server Actions im selben Projekt nutzen?
Ja, und das ist sogar der empfohlene Ansatz. Nutzen Sie Server Actions für interne Formulare und Mutationen aus eigenen Komponenten, Route Handlers für externe Webhooks, Public APIs und mobile Backends.
Wie streame ich Antworten von OpenAI oder Anthropic an den Browser?
Setzen Sie den Content-Type auf text/event-stream und geben Sie einen ReadableStream als Response-Body zurück, der die SDK-Tokens als SSE-Frames enkodiert. Wichtig: Cache-Control: no-cache setzen, damit CDNs nicht puffern.
Funktionieren Route Handlers mit Prisma am Edge?
Nicht direkt. Der klassische Prisma-Engine braucht TCP-Verbindungen und ist nicht edge-kompatibel. Nutzen Sie entweder den Prisma Accelerate Connection Pool, einen HTTP-basierten Treiber (Neon, PlanetScale, Turso) oder belassen Sie den Handler in der Node.js-Runtime.
Wie testet man Route Handlers lokal?
Da Handler reine Funktionen mit Request-Input und Response-Output sind, können Sie sie direkt in Vitest oder Jest importieren und mit einem new Request(url, { method, body }) aufrufen, ohne Next-Server-Setup. Für vollständige Integrationstests starten Sie next dev und fetchen die Endpunkte mit node --test oder Playwright.
Praxisleitfaden zu Partial Prerendering in Next.js 16: statische Shell plus dynamische Suspense-Holes, cacheComponents-Flag, use-cache-Direktive, updateTag-Invalidierung und Edge-Deployment auf Vercel mit echten Migrationsfallstricken.
Turbopack wird in Next.js 16 zum Build-Default. So migrierst du sauber, behebst die typischen Production-Fehler und steigst per --webpack oder NEXT_DISABLE_TURBOPACK gezielt aus.
Schritt-für-Schritt-Integration von Drizzle ORM mit Next.js App Router und PostgreSQL: von Installation und Schema-Definition über Migrationen bis zu typsicheren CRUD-Operationen via Server Actions und Neon Serverless Pooling – vollständig für 2026.