9.1 Next.js: Route Handlers, die eigene API
Nach dieser Einheit kannst du:
- einen API-Entwurf als Route Handlers umsetzen, für die Sammlung und für ein einzelnes Element.
- den Body einer Anfrage lesen, prüfen und kaputte Eingaben mit
400im vereinbarten Fehlerformat beantworten. - Header einer Anfrage am Server lesen und den Fehlwert
nullbehandeln. - die eigene API mit curl von außen testen, samt aller dokumentierten Fehlerfälle.
Am Buffet hängt die Preisliste schon seit Wochen, nur steht hinter dem Schalter noch niemand. Genau so weit ist der Vertrag aus Kapitel 6: Die Render-Job-API ist auf Papier entworfen, aber keine Anfrage bekommt eine Antwort. In dieser Einheit besetzen wir den Schalter: Jede Anfrage wird angenommen, geprüft und so beantwortet, wie es auf der Preisliste steht.
Next.js macht dafür aus demselben Projekt, das die Seiten ausliefert, auch den API-Server: Route Handlers setzen die Endpunkte um, und der Browser spricht sie genauso über HTTP an wie ein fremdes Programm. Du hast Schnittstellen benutzt (Kapitel 5), entworfen (Kapitel 6) und baust sie jetzt selbst. Die Daten bleiben vorerst eine Liste im Arbeitsspeicher; sauber gespeichert wird in den Kapiteln 10 und 11.
Begriffe vorab
Abschnitt betitelt „Begriffe vorab“- Route Handler: eine Funktion in einer Datei
route.ts, die Anfragen mit einer bestimmten HTTP-Methode an eine Adresse beantwortet. - Request und Response: die Objekte für Anfrage und Antwort; dieselben Klassen, die
fetchim Browser verwendet. - In-Memory-Speicher: Daten, die nur in einer Variable des laufenden Programms liegen und beim Neustart verschwinden.
Route Handlers: Endpunkte im Dateisystem
Abschnitt betitelt „Route Handlers: Endpunkte im Dateisystem“API-Endpunkte folgen derselben Ordnerlogik wie Seiten aus Einheit 8.1. Nur heißt die Datei route.ts statt page.tsx, und statt einer Komponente exportiert sie pro HTTP-Methode eine Funktion mit dem Namen der Methode:
app/└── api/ └── jobs/ ├── route.ts -> GET /api/jobs, POST /api/jobs └── [id]/ └── route.ts -> GET /api/jobs/42, DELETE /api/jobs/42flowchart LR
accTitle: Ordner unter app/api werden zu Endpunkten
api["app/api/jobs/"] --> r1["route.ts"]
r1 --> g1["export GET<br/>GET /api/jobs"]
r1 --> p1["export POST<br/>POST /api/jobs"]
api --> id["[id]/"]
id --> r2["route.ts"]
r2 --> g2["export GET<br/>GET /api/jobs/42"]
r2 --> d2["export DELETE<br/>DELETE /api/jobs/42"]
Abb. 9.1: Der Ordner bestimmt die Adresse, der Name der exportierten Funktion die Methode (eigene Darstellung).
Der vorläufige Speicher ist eine Liste in einem eigenen Modul. Beide route.ts-Dateien importieren es, also arbeiten Sammlung und Element mit denselben Daten:
// lib/jobs.ts: the in-memory store, used by all route handlersimport type { Job, NewJob } from "./types";
let jobs: Job[] = [];let nextId = 1;
export function listJobs(): Job[] { return jobs;}
export function addJob(input: NewJob): Job { const job: Job = { id: nextId++, filename: input.filename, preset: input.preset, state: "queued", progress: 0, createdAt: new Date().toISOString(), }; jobs.push(job); return job;}
// findJob(id) returns one job or undefined, deleteJob(id) removes oneJob und NewJob sind die Interfaces aus Einheit 6.1, in lib/types.ts. Die Felder, die der Server vergibt (id, state, progress, createdAt), setzt addJob; vom Client kommen nur filename und preset. Darauf bauen die Endpunkte der Sammlung auf:
import { connection, NextResponse } from "next/server";import { addJob, listJobs } from "@/lib/jobs";import type { NewJob } from "@/lib/types";
export async function GET() { await connection(); // wait for a real request, explained in unit 9.2 return NextResponse.json(listJobs());}
export async function POST(request: Request) { let body: Partial<NewJob> | null; try { body = await request.json(); } catch { return NextResponse.json( { error: { code: "INVALID_JSON", message: "body is not valid JSON" } }, { status: 400 }); } const filename = typeof body?.filename === "string" ? body.filename.trim() : ""; if (filename === "") { return NextResponse.json( { error: { code: "VALIDATION_FAILED", message: "filename must not be empty" } }, { status: 400 }); } const job = addJob({ filename, preset: body?.preset ?? "web-1080p" }); return NextResponse.json(job, { status: 201 });}Beispielprojekt: 09_nextjs_api_datenfluss_examples/ex01_job_api
Die Bausteine:
- Der Handler erhält das
Request-Objekt und liest den Body mitawait request.json(). - Er antwortet mit
NextResponse.json(daten, { status }). Ohne Angabe ist der Status200. - Alles aus Kapitel 6 wird wörtlich umgesetzt:
201beim Anlegen, das vereinbarte Fehlerformat bei400, die Interfaces aus der gemeinsamen Typdatei.
Drei Zeilen verdienen einen zweiten Blick:
await connection()kennst du aus Einheit 8.2: Die Liste wird bei jeder Anfrage frisch gelesen. Was ohne die Zeile passiert, zeigt Einheit 9.2; bis dahin beginnt jederGET, der veränderliche Daten liefert, mit ihr.try/catchumrequest.json(): Ein kaputter Body ist ein Fehler des Clients (400), kein Absturz des Servers. Ohne den Block antwortet Next.js mit500und leerem Body.typeof: Der Body kann gültiges JSON sein und trotzdemnull, eine Zahl oder ein Objekt ohnefilenameenthalten. TypeScript prüft das nicht, denn Typen gelten nur für deinen eigenen Code, nicht für Daten von außen.
sequenceDiagram
accTitle: Weg einer POST-Anfrage durch den Route Handler
participant c as Client
participant h as POST in route.ts
participant s as lib/jobs.ts
c->>h: POST /api/jobs mit Body
alt Body ist kein JSON
h-->>c: 400 INVALID_JSON
else filename fehlt oder ist leer
h-->>c: 400 VALIDATION_FAILED
else alles in Ordnung
h->>s: addJob
s-->>h: Job mit id
h-->>c: 201 mit dem Job
end
Abb. 9.2: Der Handler prüft zuerst und speichert erst dann; jeder Ausgang hat seinen Statuscode (eigene Darstellung).
Die Endpunkte für ein einzelnes Element lesen die ID aus dem Pfad, genau wie die dynamischen Seiten aus Einheit 8.1:
import { NextResponse } from "next/server";import { deleteJob, findJob } from "@/lib/jobs";
export async function GET( request: Request, { params }: { params: Promise<{ id: string }> },) { const { id } = await params; const job = findJob(Number(id)); if (!job) { return NextResponse.json( { error: { code: "NOT_FOUND", message: `job ${id} does not exist` } }, { status: 404 }); } return NextResponse.json(job);}Beispielprojekt: 09_nextjs_api_datenfluss_examples/ex01_job_api
id ist ein String, darum Number(id) vor dem Vergleich. PATCH und DELETE folgen demselben Muster: finden, prüfen, ändern oder entfernen, passender Status. DELETE antwortet mit 204 und leerem Body, also mit new Response(null, { status: 204 }).
Der Speicher in lib/jobs.ts ist flüchtig: Ein Neustart des Servers leert ihn. Für die Übungen dieses Kapitels reicht das, und es ist zugleich der Grund, warum Kapitel 11 eine Datenbank einführt.
Wo begegnet dir das? Öffne in einer beliebigen Web-App die DevTools und sieh dir im Netzwerk-Tab die Anfragen vom Typ fetch an. Hinter jeder Adresse wie /api/cart oder /api/search steht am Server eine Funktion wie diese: Anfrage lesen, prüfen, antworten.
Auflösung
(a) app/api/jobs/[id]/route.ts, Funktion PATCH. (b) app/api/presets/route.ts, Funktion GET. (c) Next.js antwortet von selbst mit 405 Method Not Allowed: Die Adresse gibt es, die Methode nicht. Dafür musst du nichts schreiben.
Header der Anfrage am Server lesen
Abschnitt betitelt „Header der Anfrage am Server lesen“Der Body ist nicht das Einzige, was eine Anfrage mitbringt. Aus Einheit 5.1 kennst du die Header: Schlüssel-Wert-Paare mit Angaben über die Anfrage. Dasselbe Request-Objekt, dessen Body du mit await request.json() liest, gibt sie über request.headers.get("name") heraus:
export async function GET(request: Request) { const accept = request.headers.get("accept"); // e.g. "application/json" const auth = request.headers.get("authorization"); // null if not set // ...}Zwei Dinge musst du wissen:
- Header-Namen unterscheiden nicht zwischen Groß- und Kleinschreibung:
"Accept"und"accept"liefern dasselbe. getliefertnull, wenn der Header fehlt. Diesen Fall planst du immer ein.
Nützlich sind Header überall, wo eine Anfrage etwas über sich selbst mitteilt, das nicht in den Body gehört. Das typische Beispiel: Wer fragt an? Eine echte Anmeldung kommt erst in Kapitel 12. Bis dahin lässt sich die Rolle über einen selbst gewählten Header nachstellen, etwa x-role. In der Render-Farm darf nur die Rolle operator Jobs löschen:
export async function DELETE( request: Request, { params }: { params: Promise<{ id: string }> },) { const role = request.headers.get("x-role") ?? "anonymous"; if (role !== "operator") { return NextResponse.json( { error: { code: "FORBIDDEN", message: "only the operator may do this" } }, { status: 403 }); } // ... delete the job: 204, or 404 if it does not exist ...}Beispielprojekt: 09_nextjs_api_datenfluss_examples/ex01_job_api
flowchart LR
accTitle: Rollenprüfung über den Header x-role
req["DELETE /api/jobs/1"] --> read["x-role lesen,<br/>fehlt er: anonymous"]
read --> check{"Rolle ist<br/>operator?"}
check -- nein --> f403["403 FORBIDDEN"]
check -- ja --> work["löschen:<br/>204 oder 404"]
Abb. 9.3: Die Rolle wird vor der eigentlichen Arbeit geprüft (eigene Darstellung).
Das vollständige DELETE steht im Beispielprojekt. Ein Client setzt den Header so, wie er Content-Type setzt: im fetch-Aufruf unter headers oder mit curl -H "x-role: operator". Das ist keine Sicherheit, denn jeder kann den Header frei setzen. Es ist eine Attrappe, mit der du die Rollenlogik schon jetzt bauen kannst; die fälschungssichere Fassung folgt in Kapitel 12.
Auflösung
(a) 403: get liefert null, der Fehlwert "anonymous" ist nicht "operator". (b) 204: Beim Namen des Headers spielt Groß- und Kleinschreibung keine Rolle. (c) 403: Beim Wert spielt sie sehr wohl eine Rolle, "Operator" ist ein anderer String als "operator".
Sofort testen
Abschnitt betitelt „Sofort testen“Die eigene API behandelst du wie eine fremde: zuerst von außen ansprechen, erst dann Code dagegen schreiben. Das Werkzeug kennst du aus Einheit 5.1:
curl -i http://localhost:3000/api/jobscurl -i -X POST http://localhost:3000/api/jobs \ -H "Content-Type: application/json" \ -d '{"filename": "intro.mp4"}' # expect 201curl -i -X POST http://localhost:3000/api/jobs \ -H "Content-Type: application/json" \ -d '{"filename": ""}' # expect 400curl -i http://localhost:3000/api/jobs/999 # expect 404curl -i -X DELETE http://localhost:3000/api/jobs/1 # expect 403curl -i -X DELETE http://localhost:3000/api/jobs/1 \ -H "x-role: operator" # expect 204Zum Handwerk gehört, auch die Fehlerfälle von Hand auszulösen: leerer Dateiname, kaputtes JSON, unbekannte ID, fehlende Rolle. Jede Antwort, die der Vertrag dokumentiert, siehst du einmal mit eigenen Augen. Automatische Tests für die API folgen in Kapitel 13; diese Handgriffe sind ihre Vorstufe.
Stolperfallen
Abschnitt betitelt „Stolperfallen“- Datei heißt nicht
route.ts:app/api/jobs/jobs.tserzeugt keinen Endpunkt, und die Funktionen müssen genau wie die Methode heißen, in Großbuchstaben (GET, nichtget). GETohneconnection(): Mitnpm run devstimmt alles, nachnpm run buildliefert der Endpunkt für immer die Antwort vom Build (Einheit 9.2).request.json()ohnetry/catch: Ein kaputter Body wird zu500mit leerem Body statt zu400im Fehlerformat.- Status vergessen:
NextResponse.json({ error: ... })ohne{ status: 400 }antwortet mit200; der Client hält den Fehler für einen Erfolg. idals Zahl behandelt: Werte aus dem Pfad sind Strings;j.id === idist immer falsch, erstNumber(id)vergleicht richtig.- Fehlender Header:
request.headers.get("x-role")liefertnull; ohne Fehlwert vergleicht der Code mitnullund lässt je nach Bedingung alle oder niemanden durch. - Daten weg nach dem Neustart: Der In-Memory-Speicher lebt nur, solange der Server läuft.
Zusatzmaterialien
Abschnitt betitelt „Zusatzmaterialien“- Next.js: Route Handlers - Konvention und unterstützte Methoden.
- Next.js: route.js - alle Einzelheiten zu Parametern,
paramsund Antworten. - Next.js: connection - die Funktion, die auf eine echte Anfrage wartet.
- MDN: Request und Headers - die Objekte, die Browser und Server teilen (englisch).
Zum Anhören
Abschnitt betitelt „Zum Anhören“Du kannst dir diese Einheit kostenlos mit der Vorlesefunktion deines Browsers oder Betriebssystems vorlesen lassen (etwa „Laut vorlesen” in Edge), ohne Konto und ohne Upload; die Codebeispiele und Diagramme liest du dabei am Bildschirm mit. Freiwillig kannst du mit Google NotebookLM (Google-Konto nötig) aus diesem öffentlichen Text eine gesprochene Zusammenfassung erzeugen, auch auf Deutsch.
Lernergebnisse
Abschnitt betitelt „Lernergebnisse“Du kannst jetzt:
- einen API-Entwurf als Route Handlers umsetzen, für die Sammlung und für ein einzelnes Element.
- den Body einer Anfrage lesen, prüfen und kaputte Eingaben mit
400im vereinbarten Fehlerformat beantworten. - Header einer Anfrage am Server lesen und den Fehlwert
nullbehandeln. - die eigene API mit curl von außen testen, samt aller dokumentierten Fehlerfälle.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 09-1a - Die eigene API
Quellen
Abschnitt betitelt „Quellen“- Route Handlers, unterstützte Methoden,
405bei nicht exportierter Methode → Next.js: Route Handlers, route.js NextResponse.json→ Next.js: NextResponse;connection()→ Next.js: connection500mit leerem Body ohnetry/catch,405beiPUT, alle curl-Aufrufe samt Statuscodes → eigener Test mit Next.js 16.4.0 und Cache Components (Oktober 2026)Request,request.json(),Headers.getund Groß-/Kleinschreibung der Header-Namen → MDN: Request, Headers.get- Vorsilbe
X-für eigene Header als überholt eingestuft → IETF: RFC 6648 (Juni 2012) - Postman und Bruno → postman.com, usebruno.com
- Abb. 9.1 (Ordner und Endpunkte), Abb. 9.2 (Weg einer POST-Anfrage), Abb. 9.3 (Rollenprüfung) → eigene Darstellung, alle als Mermaid-Diagramm
Autoren
Abschnitt betitelt „Autoren“- Oliver Maurhart (MAO)
- Michael Gabriel (GAM)
CC-BY-SA