Skalierbare APIs mit Next.js Route Handlers bauen
Umfassender Leitfaden für skalierbare, produktionsreife APIs mit Next.js Route Handlers, TypeScript, Validierung und Error-Handling-Mustern.

Einführung
APIs zu bauen, die skalieren, bedeutet nicht nur, mehr Anfragen zu verarbeiten — es geht darum, Systeme zu entwerfen, die wartbar, testbar und zuverlässig bleiben, während deine Anwendung wächst. In diesem Post zeige ich die Patterns, die ich beim Bau von Produktions-APIs mit Next.js Route Handlers einsetze.
Warum Next.js Route Handlers?
Der App Router von Next.js hat Route Handlers als First-Class-Möglichkeit zum Bauen von API-Endpunkten eingeführt. Sie bieten mehrere Vorteile gegenüber den alten API Routes:
- Colocation — API-Routen liegen direkt neben deinen Seiten im App-Verzeichnis
- Web-Standard-APIs — Aufgebaut auf den Objekten
RequestundResponse - Edge-Runtime-Support — Deployment näher an deinen Nutzern
- Statisch & dynamisch — Wähle die passende Rendering-Strategie pro Route
Eine skalierbare Struktur aufsetzen
So organisiere ich API-Routen in großen Projekten:
// src/app/api/v1/posts/route.ts
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
const createPostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
tags: z.array(z.string()).max(10).optional(),
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validated = createPostSchema.parse(body);
// Process the validated data
const post = await createPost(validated);
return NextResponse.json(post, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ errors: error.flatten().fieldErrors },
{ status: 400 },
);
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}Validierung an den Grenzen
Ein Prinzip, das ich konsequent befolge: An den Systemgrenzen validieren, intern vertrauen. Jede externe Eingabe — Request-Bodies, Query-Parameter, Header — wird mit Zod-Schemas validiert, bevor sie die Business-Logik berührt.
const querySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sort: z.enum(["date", "title", "popularity"]).default("date"),
});
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const query = querySchema.parse(Object.fromEntries(searchParams));
const posts = await getPosts(query);
return NextResponse.json(posts);
}Error-Handling-Patterns
Eine konsistente Error-Handling-Strategie macht Debugging und Client-Integration deutlich reibungsloser:
class AppError extends Error {
constructor(
message: string,
public statusCode: number,
public code: string,
) {
super(message);
}
}
function handleError(error: unknown): NextResponse {
if (error instanceof AppError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.statusCode },
);
}
console.error("Unhandled error:", error);
return NextResponse.json(
{ error: "Internal server error", code: "INTERNAL_ERROR" },
{ status: 500 },
);
}Rate Limiting
Für Produktions-APIs ist Rate Limiting unverzichtbar. Hier ein einfacher In-Memory-Ansatz, der für Serverless geeignet ist:
const rateLimitMap = new Map<string, { count: number; resetAt: number }>();
function rateLimit(ip: string, maxRequests = 60, windowMs = 60000): boolean {
const now = Date.now();
const entry = rateLimitMap.get(ip);
if (!entry || now > entry.resetAt) {
rateLimitMap.set(ip, { count: 1, resetAt: now + windowMs });
return true;
}
if (entry.count >= maxRequests) return false;
entry.count++;
return true;
}Wichtige Erkenntnisse
- An den Grenzen validieren — Zod-Schemas für alle externen Eingaben nutzen
- Konsistente Error-Responses — Standardisiere dein Error-Format frühzeitig
- Rate Limiting für alles — Schütze deine APIs vor Missbrauch
- Alles typisieren — TypeScript findet Bugs, bevor sie in Produktion landen
- Handler schlank halten — Business-Logik in separate Module auslagern
Skalierbare APIs zu bauen ist ein iterativer Prozess. Starte mit diesen Patterns, miss deine tatsächliche Nutzung und optimiere dort, wo dich die Daten hinführen.


