Validierung an API-Grenzen durchsetzen: Trust Boundary Pattern
AI generated
<T>
type
TypeScript · API-Grenzen · Trust Boundary · Middleware
Validierung an API-Grenzen durchsetzen
Das Trust-Boundary-Pattern in TypeScript-Backends

Jede Request-Payload, die eine API entgegennimmt, ist aus Sicht von TypeScript zunächst unknown, egal wie vertrauenswürdig der Client erscheint. Dieser Artikel zeigt, wie das Trust-Boundary-Pattern Validierung konsequent an der Systemgrenze verankert, mit Middleware, die vor jedem Handler validiert, typisierten Request-Objekten im restlichen Code und einer strukturierten Fehlerausbreitung, die verhindert, dass ungeprüfte Daten tiefer in die Anwendung vordringen.

13 Min. Lesezeit Trust Boundary · Middleware · Zod Express · Fastify · REST-APIs

1. Das Trust-Boundary-Konzept: Wo Vertrauen endet

Eine Trust Boundary ist die Stelle in einer Architektur, an der Daten von einem Bereich mit geringerem Vertrauen in einen Bereich mit höherem Vertrauen wechseln, etwa vom öffentlichen Internet in die Kernlogik eines Backends. Alles, was jenseits dieser Grenze liegt, muss als potenziell fehlerhaft, manipuliert oder schlicht falsch strukturiert behandelt werden, unabhängig davon, wie vertrauenswürdig ein Client im Normalfall erscheint.

Das Trust-Boundary-Pattern für TypeScript-APIs übersetzt dieses Sicherheitskonzept in eine konkrete Architekturregel: Validierung geschieht ausschließlich an der Grenze, direkt bevor Daten ins Innere der Anwendung eintreten, nie verstreut über mehrere Schichten hinweg. Sobald ein Wert diese Grenze einmal validiert passiert hat, kann sich der restliche Code auf den geprüften TypeScript-Typ verlassen, ohne an jeder weiteren Stelle erneut zu misstrauen.

2. Warum ein Request-Body niemals als typisiert gelten darf

In vielen Express- oder Fastify-Projekten wird ein Request-Body mit einem generischen Typparameter wie Request<{}, {}, CreateOrderDto> versehen, obwohl req.body zur Laufzeit tatsächlich any ist, ein reines Framework-Versprechen ohne jede Prüfung. Diese Typannotation beschreibt lediglich, was der Entwickler erwartet, nicht was tatsächlich ankommt, ein Unterschied, der bei einem fehlerhaften Client, einem alten API-Consumer oder einem manipulierten Request sofort sichtbar wird.

Der korrekte Ausgangspunkt an jeder API-Grenze ist deshalb, den Request-Body explizit als unknown zu behandeln, bis eine echte Laufzeit-Validierung stattgefunden hat. Dieser eine gedankliche Schritt, Rohdaten grundsätzlich als unbekannt zu betrachten statt als bereits typisiert, ist die Grundvoraussetzung für ein funktionierendes Trust-Boundary-Pattern und verhindert die verbreitete Praxis, Typsicherheit durch bloße Behauptung statt durch Prüfung herzustellen.

3. Eine Validierungs-Middleware für alle Routen bauen

Statt Validierung in jedem einzelnen Route-Handler zu wiederholen, bündelt eine generische Middleware die Prüfung an einer zentralen Stelle, durch die jede Route läuft, bevor die eigentliche Handler-Logik erreicht wird. Diese Middleware nimmt ein Zod-Schema als Parameter entgegen, validiert req.body dagegen und beendet die Anfrage bei einem Fehler bereits an der Grenze, mit einer strukturierten 400er-Antwort statt eines unklaren Fehlers tief in der Geschäftslogik.

Dieses Muster macht Validierung zu einer deklarativen Eigenschaft jeder Route, sichtbar direkt in der Routendefinition, statt versteckt irgendwo im Handler-Code. Ein neuer Endpunkt bekommt Validierung quasi automatisch, sobald er dieselbe Middleware mit seinem eigenen Schema registriert, ein konsistentes Muster über die gesamte API hinweg.


import { z, type ZodType } from "zod";
import type { Request, Response, NextFunction, RequestHandler } from "express";

// Generic middleware: validates req.body against any Zod schema
function validateBody<T>(schema: ZodType<T>): RequestHandler {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);

    if (!result.success) {
      res.status(400).json({
        error: "ValidationError",
        issues: result.error.flatten().fieldErrors,
      });
      return;
    }

    // Attach the validated, typed data instead of trusting req.body directly
    (req as Request & { validated: T }).validated = result.data;
    next();
  };
}

const createOrderSchema = z.object({
  customerId: z.string().uuid(),
  items: z.array(
    z.object({ sku: z.string(), quantity: z.number().int().positive() })
  ),
});

// Registered per route: validation is visible right in the route definition
app.post("/orders", validateBody(createOrderSchema), createOrderHandler);

4. Typisierte Handler-Signaturen nach der Validierung

Sobald die Middleware eine Anfrage erfolgreich validiert hat, sollte der eigentliche Handler auf die geprüften, typisierten Daten zugreifen, statt erneut auf req.body. Ein generischer Helfer-Typ, der die Handler-Signatur um den bereits validierten Typ ergänzt, macht diese Garantie im Typsystem sichtbar und verhindert, dass ein Handler versehentlich auf den ungeprüften Rohwert zugreift.

Dieser Schritt schließt den Kreis des Trust-Boundary-Patterns: Die Middleware ist die einzige Stelle, die unknown-Daten sieht, der Handler arbeitet ausschließlich mit einem bereits geprüften, spezifischen Typ. Für den restlichen Code der Anwendung, Service-Klassen, Repository-Aufrufe, Business-Logik, existiert das Problem unvalidierter Eingaben an dieser Stelle schlicht nicht mehr.


import { z } from "zod";
import type { Request, Response } from "express";

const createOrderSchema = z.object({
  customerId: z.string().uuid(),
  items: z.array(
    z.object({ sku: z.string(), quantity: z.number().int().positive() })
  ),
});

type CreateOrderInput = z.infer<typeof createOrderSchema>;

// Typed request: "validated" is guaranteed to exist and match the schema
type ValidatedRequest<T> = Request & { validated: T };

function createOrderHandler(
  req: ValidatedRequest<CreateOrderInput>,
  res: Response
): void {
  // No further validation needed: req.validated is a trusted CreateOrderInput
  const { customerId, items } = req.validated;
  const total = items.reduce((sum, item) => sum + item.quantity, 0);
  res.status(201).json({ customerId, itemCount: items.length, total });
}

5. Query-Parameter, Path-Parameter und Header konsequent einbeziehen

Das Trust-Boundary-Pattern beschränkt sich nicht auf den Request-Body. Query-Parameter kommen immer als Strings an, selbst wenn sie logisch eine Zahl oder ein Boolean repräsentieren, Path-Parameter ebenso, und Header können komplett fehlen oder mehrfach mit unterschiedlichen Werten gesendet werden. Jede dieser Quellen ist eine eigene, unabhängige Grenze zur Außenwelt und verdient ein eigenes Schema, statt stillschweigend als korrekt vorausgesetzt zu werden.

Eine erweiterte Middleware validiert deshalb req.query, req.params und relevante Header jeweils gegen ein eigenes Zod-Schema, mit expliziter Coercion für Zahlen und Booleans über z.coerce.number() beziehungsweise z.coerce.boolean(). Diese Konsequenz verhindert die häufige Lücke, bei der ein Body sorgfältig validiert wird, während eine kritische ID im Pfad ungeprüft direkt an eine Datenbankabfrage weitergereicht wird.


import { z } from "zod";

// Query params always arrive as strings, even for numeric or boolean values
const listOrdersQuerySchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  pageSize: z.coerce.number().int().min(1).max(100).default(20),
  includeArchived: z.coerce.boolean().default(false),
});

// Path params equally need explicit validation, not just a type annotation
const orderParamsSchema = z.object({
  orderId: z.string().uuid(),
});

type ListOrdersQuery = z.infer<typeof listOrdersQuerySchema>;
type OrderParams = z.infer<typeof orderParamsSchema>;

function validateListOrdersRequest(rawQuery: unknown, rawParams: unknown) {
  const query: ListOrdersQuery = listOrdersQuerySchema.parse(rawQuery);
  const params: OrderParams = orderParamsSchema.parse(rawParams);
  return { query, params };
}

6. Strukturierte Fehlerausbreitung statt generischer 500er

Ein häufiger Architekturfehler ist, Validierungsfehler an der Grenze einfach durchzureichen, bis sie irgendwo als generischer 500-Fehler oder als unbehandelte Exception enden. Das Trust-Boundary-Pattern verlangt stattdessen, dass jede Validierungsfehler-Klasse an der Grenze selbst in eine passende HTTP-Antwort übersetzt wird, üblicherweise 400 für strukturell ungültige Eingaben, mit einer konsistenten, maschinenlesbaren Fehlerstruktur.

Diese strukturierte Antwort sollte pro Feld benennen, was genau ungültig war, damit ein aufrufender Client oder ein Frontend-Formular die Fehlermeldung direkt nutzen kann, statt einen kryptischen Stacktrace zu erhalten. Zods error.flatten().fieldErrors liefert genau diese Struktur direkt aus der Box, ohne dass die Anwendung ein eigenes Fehlerformat von Grund auf entwickeln müsste.

7. Interne Service-Grenzen genauso behandeln wie externe

Ein oft übersehener Aspekt des Trust-Boundary-Patterns ist, dass nicht nur die öffentliche API-Grenze zum Client relevant ist, sondern auch interne Grenzen zwischen Microservices, zwischen einem Backend und einer Message-Queue, oder zwischen zwei Modulen mit unterschiedlichen Deploy-Zyklen innerhalb desselben Monolithen. Auch hier gilt: Ein anderer Service kann eine andere Version des Vertrags implementieren, ein Feld weglassen oder unerwartet null statt eines Strings senden.

Wer Validierung nur an der äußersten, öffentlichen Grenze durchsetzt, aber internen Service-zu-Service-Aufrufen blind vertraut, verschiebt das Risiko lediglich, statt es zu beseitigen. Response-Schemas für interne API-Aufrufe, validiert mit demselben Trust-Boundary-Prinzip wie eingehende Requests, decken Versionsinkonsistenzen zwischen Services auf, bevor sie als schwer nachvollziehbarer Laufzeitfehler in der Produktion auffallen.

8. Häufige Fehler bei der Umsetzung von Trust Boundaries

Der häufigste Fehler ist eine Typannotation ohne echte Prüfung, ein Request<{}, {}, CreateOrderDto>, dem niemand eine safeParse()- oder parse()-Aufruf folgen lässt. Der zweite häufige Fehler ist doppelte, aber inkonsistente Validierung, bei der ein Frontend-Formular eigene Regeln prüft und das Backend eigene, nicht identische Regeln, sodass ein Datensatz das Frontend passiert, aber am Backend abgelehnt wird, oder umgekehrt.

Ein dritter, subtilerer Fehler ist, Validierung nur für den Body durchzuführen, aber Query-Parameter, Path-Parameter und Header unvalidiert zu lassen, in der Annahme, sie seien weniger kritisch. Gerade Path-Parameter wie eine orderId landen aber häufig direkt und ungeprüft in einer Datenbankabfrage, was sie zu einem besonders sensiblen Teil der Trust Boundary macht, nicht zu einem nachrangigen.

9. Validierung an der Grenze vs. verstreute Prüfungen im Vergleich

Die folgende Tabelle stellt konsequente Grenzvalidierung verstreuten, ad hoc eingefügten Prüfungen gegenüber, wie sie in gewachsenen Codebasen ohne bewusstes Trust-Boundary-Pattern häufig entstehen.

Aspekt Verstreute Prüfungen Trust-Boundary-Pattern
Ort der Validierung Verteilt über Handler, Service, Repository Zentral in Middleware an der Grenze
Konsistenz der Regeln Leicht widersprüchlich zwischen Schichten Ein Schema pro Grenze, eine Quelle der Wahrheit
Fehlerantworten Uneinheitlich, teils 500 statt 400 Strukturiert, konsistent, mit fieldErrors
Query/Path-Parameter Oft übersehen, direkt an DB weitergereicht Eigenes Schema, explizite Coercion
Interne Service-Grenzen Meist ungeprüft, blind vertraut Gleiche Prinzipien wie öffentliche Grenze

Ein konsequentes Trust-Boundary-Pattern verwandelt Validierung von einer Ansammlung individueller Entscheidungen in eine architektonische Konstante, die für jede neue Route automatisch gilt.

Mironsoft

Trust-Boundary-Middleware, API-Absicherung und Request-Validierung für eure Backends

Validierung konsequent an eure API-Grenzen bringen?

Wir bauen eine zentrale Validierungs-Middleware für Body, Query und Path-Parameter, richten typisierte Handler ein und schließen Lücken an internen Service-Grenzen.

Grenz-Audit

Alle Endpunkte auf ungeprüfte Eingaben und Lücken durchleuchten

Middleware-Aufbau

Zentrale, wiederverwendbare Validierung für Body, Query und Params

Interne Verträge

Service-zu-Service-Grenzen mit demselben Prinzip absichern

10. Zusammenfassung

Das Trust-Boundary-Pattern löst ein strukturelles Problem, das eine reine Typannotation niemals lösen kann: Ein Request-Body, ein Query-Parameter oder ein Path-Parameter ist zur Laufzeit immer unknown, unabhängig davon, was der Compiler behauptet. Validierung an API-Grenzen konsequent durchzusetzen bedeutet, diese Prüfung an einer zentralen Middleware zu bündeln, bevor Daten überhaupt in die eigentliche Geschäftslogik eintreten.

Typisierte Handler-Signaturen nach erfolgreicher Validierung machen die Garantie im Typsystem sichtbar, strukturierte Fehlerantworten mit feldbezogenen Details ersetzen generische 500er, und dasselbe Prinzip sollte konsequent auch auf interne Service-zu-Service-Grenzen angewendet werden, nicht nur auf die öffentliche API. Wer diese Grenze einmal sauber zieht, muss dem restlichen Code nie wieder misstrauen.

Validierung an API-Grenzen durchsetzen, das Wichtigste auf einen Blick

Trust Boundary definieren

Alles jenseits der Grenze ist unknown, unabhängig von der Typannotation.

Zentrale Middleware

Body, Query und Path-Parameter über eine generische Validierungs-Middleware prüfen.

Typisierte Handler

Nach der Grenze gilt der geprüfte Typ, keine erneute Prüfung im Handler nötig.

Interne Grenzen einbeziehen

Service-zu-Service-Aufrufe verdienen dieselbe Sorgfalt wie öffentliche Endpunkte.

11. FAQ: Validierung an API-Grenzen durchsetzen

1Was bedeutet Trust Boundary konkret?
Die Stelle, an der Daten aus einem weniger vertrauenswürdigen Bereich in den Kern der Anwendung wechseln. Alles davor gilt als unknown bis geprüft.
2Warum reicht eine Typannotation nicht aus?
Sie beschreibt nur die Erwartung, req.body bleibt zur Laufzeit any. Ohne echte Prüfung ist die Annotation reine Behauptung.
3Wie baut man eine wiederverwendbare Middleware?
Eine generische Funktion nimmt ein Schema entgegen, prüft req.body, antwortet mit 400 bei Fehlern oder hängt das validierte Ergebnis an.
4Wie unterscheidet sich ein typisierter Handler?
Er erwartet ein bereits validiertes, spezifisches Feld statt rohen body-Zugriff und greift nie direkt auf req.body zu.
5Müssen Query-Parameter genauso validiert werden?
Ja, sie kommen immer als Strings an. z.coerce.number() und z.coerce.boolean() übernehmen Umwandlung mit Prüfung.
6Wie sollten Validierungsfehler beantwortet werden?
Mit strukturiertem 400er und feldbezogenen Details, etwa über error.flatten().fieldErrors, statt generischem 500er.
7Gilt das Pattern auch für interne Service-Aufrufe?
Ja, andere Services können andere Vertragsversionen implementieren. Response-Schemas verdienen dieselbe Validierung wie öffentliche Requests.
8Was ist der häufigste Umsetzungsfehler?
Typannotation ohne echte Prüfung, inkonsistente Doppelvalidierung zwischen Frontend und Backend, ungeprüfte Path-Parameter an der Datenbank.
9Verlangsamt zentrale Validierung die API spürbar?
Kaum, die Prüfzeit liegt im einstelligen Millisekundenbereich, vernachlässigbar gegenüber Netzwerklatenz und Datenbankzugriffen.
10Ersetzt Grenzvalidierung Autorisierung?
Nein, Validierung prüft Datenstruktur, Autorisierung prüft Berechtigung. Beide laufen typischerweise nacheinander in derselben Kette.