Zum Inhalt springen

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.

2 Min. Lesezeit
API-Architekturdiagramm, das den Anfragefluss durch die Route Handlers von Next.js zeigt

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 Request und Response
  • 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:

tstypescript
// 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.

tstypescript
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:

tstypescript
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:

tstypescript
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

  1. An den Grenzen validieren — Zod-Schemas für alle externen Eingaben nutzen
  2. Konsistente Error-Responses — Standardisiere dein Error-Format frühzeitig
  3. Rate Limiting für alles — Schütze deine APIs vor Missbrauch
  4. Alles typisieren — TypeScript findet Bugs, bevor sie in Produktion landen
  5. 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.

Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX