TS
Thomas Schmitz

Freiberuflicher IT-Berater, Softwareentwickler & DevOps. Praxisnahe Artikel zu API-Design, Architektur und Cloud-Workflows.

Serie: API-Design · Teil 8 von 22

API-Design Teil 8: Fehlerbehandlung & Validierung

Ein konsistentes Fehlerformat, maschinenlesbare Codes und strukturierte Feldfehler – damit Clients Probleme verstehen und lösen können.

Praxisnah Checkliste

Eine API, die nur generische Fehler zurückgibt, ist nutzlos. Eine API, die Stack Traces an Clients schickt, ist gefährlich. Gute Fehlerbehandlung liegt dazwischen: genug Information für Debugging, keine Information für Angreifer.

Dieser Artikel hilft dir, ein konsistentes Fehlerformat zu etablieren, das maschinenlesbar und menschenverständlich ist. Am Ende hast du einen Error-Katalog und klare Regeln für Validierung und Fehlermeldungen.

Zielbild

Nach diesem Artikel kannst du:

  • Ein einheitliches Fehlerformat definieren, das für alle Endpoints gilt
  • Maschinenlesbare Error Codes einführen, die Clients programmatisch verarbeiten
  • Feldfehler strukturiert zurückgeben, damit UIs gezielt Feedback geben können
  • Request-IDs und Trace-IDs für Debugging nutzen
  • Sensitive Informationen aus Fehlermeldungen fernhalten
  • Graceful Degradation bei Downstream-Ausfällen umsetzen

Kernfragen

Bevor du weiterliest, versuche diese Fragen für dein Projekt zu beantworten:

  1. Welches Fehlerformat nutzt ihr? JSON? Problem Details (RFC 9457)?
  2. Welche Infos dürfen in Fehlern stehen? Welche nicht?
  3. Wie modelliert ihr Feldfehler? Flach oder strukturiert?
  4. Wie korreliert ihr Fehler mit Logs? Request-ID? Trace-ID?
  5. Was passiert bei Downstream-Ausfällen? 500? 502? 503?

Das Fehlerformat

Ein gutes Fehlerformat ist konsistent, maschinenlesbar und informativ – ohne zu viel preiszugeben.

Problem Details (RFC 9457)

RFC 9457 definiert ein standardisiertes Format für HTTP-Fehler:

{
  "type": "https://api.example.com/errors/validation-failed",
  "title": "Validation Failed",
  "status": 400,
  "detail": "The request body contains invalid fields.",
  "instance": "/orders/ord_abc123"
}
Feld Bedeutung Pflicht (RFC)
type URI, die den Fehlertyp identifiziert Nein (Default about:blank)
title Kurze, menschenlesbare Beschreibung Nein (empfohlen)
status HTTP-Statuscode Nein (empfohlen)
detail Detaillierte Beschreibung des konkreten Fehlers Nein
instance URI der betroffenen Ressource Nein

Hinweis: RFC 9457 macht diese Felder optional. Für konsistente APIs ist es best practice, type, title und status immer zu setzen.

Vorteile von RFC 9457:

  • Standardisiert und dokumentiert
  • Erweiterbar für eigene Felder
  • Content-Type application/problem+json signalisiert Fehlerformat

Praktisches Fehlerformat

In der Praxis erweitert man RFC 9457 um zusätzliche Felder:

{
  "type": "https://api.example.com/errors/validation-failed",
  "title": "Validation Failed",
  "status": 400,
  "detail": "The request body contains invalid fields.",
  "instance": "/v1/orders",
  "error_code": "VALIDATION_FAILED",
  "request_id": "req_abc123xyz",
  "errors": [
    {
      "field": "email",
      "code": "INVALID_FORMAT",
      "message": "Must be a valid email address."
    },
    {
      "field": "quantity",
      "code": "OUT_OF_RANGE",
      "message": "Must be between 1 and 100."
    }
  ]
}

Warum error_code?

Die type-URI ist für Dokumentation, aber Clients brauchen einen stabilen String zum Vergleichen:

// So nicht – URL kann sich ändern
if (error.type === 'https://api.example.com/errors/validation-failed') { 
    // ...
}

// Besser – stabiler Code
if (error.error_code === 'VALIDATION_FAILED') { 
    // ...
}

Regel: error_code ist ein stabiler, versionierter Identifier. Ändert sich nie ohne Major-Version.

Error Codes: Der Katalog

Error Codes sind das Herzstück maschinenlesbarer Fehler. Sie müssen stabil, dokumentiert und sinnvoll gruppiert sein.

Namenskonvention

CATEGORY_SPECIFIC_ERROR

Beispiele:

Code Bedeutung
VALIDATION_FAILED Allgemeiner Validierungsfehler
VALIDATION_FIELD_REQUIRED Pflichtfeld fehlt
VALIDATION_FIELD_INVALID Feldwert ungültig
AUTH_TOKEN_EXPIRED Token abgelaufen
AUTH_TOKEN_INVALID Token ungültig
AUTH_INSUFFICIENT_SCOPE Fehlende Berechtigung
RESOURCE_NOT_FOUND Ressource existiert nicht
RESOURCE_ALREADY_EXISTS Duplikat
RESOURCE_CONFLICT Versionskonflikt
RATE_LIMIT_EXCEEDED Rate Limit erreicht
QUOTA_EXCEEDED Quota erschöpft
DOWNSTREAM_UNAVAILABLE Abhängiger Service nicht erreichbar
INTERNAL_ERROR Unerwarteter Serverfehler

Stabilität und Versionierung

Error Codes sind Teil des API-Vertrags:

  • Niemals einen Code umbenennen (Breaking Change)
  • Niemals die Bedeutung eines Codes ändern
  • Neue Codes hinzufügen ist OK (Non-Breaking)
  • Codes deprecaten mit Übergangszeit

HTTP-Status und Error Code

Jeder Error Code hat einen zugehörigen HTTP-Status:

HTTP-Status Typische Error Codes
400 VALIDATION_*, REQUEST_*
401 AUTH_TOKEN_*, AUTH_CREDENTIALS_*
403 AUTH_INSUFFICIENT_*, AUTH_FORBIDDEN
404 RESOURCE_NOT_FOUND
409 RESOURCE_CONFLICT, RESOURCE_ALREADY_EXISTS
422 BUSINESS_RULE_*
429 RATE_LIMIT_*, QUOTA_*
500 INTERNAL_ERROR
502 DOWNSTREAM_*
503 SERVICE_UNAVAILABLE

Wichtig: Ein Error Code hat immer denselben HTTP-Status. Nie VALIDATION_FAILED mit 500 zurückgeben.

Feldfehler strukturiert

Für Validierungsfehler reicht ein einzelner Error Code nicht. Clients brauchen Details pro Feld, um UIs zu aktualisieren.

Struktur für Feldfehler

{
  "type": "https://api.example.com/errors/validation-failed",
  "title": "Validation Failed",
  "status": 400,
  "error_code": "VALIDATION_FAILED",
  "request_id": "req_abc123",
  "errors": [
    {
      "field": "email",
      "code": "INVALID_FORMAT",
      "message": "Must be a valid email address."
    },
    {
      "field": "password",
      "code": "TOO_SHORT",
      "message": "Must be at least 8 characters."
    },
    {
      "field": "items[0].quantity",
      "code": "OUT_OF_RANGE",
      "message": "Must be between 1 and 100."
    }
  ]
}

Feldpfad-Notation

Für verschachtelte Objekte und Arrays:

Struktur Pfad
{ "email": "..." } email
{ "address": { "city": "..." } } address.city
{ "items": [{ "quantity": ... }] } items[0].quantity

Regel: Der Feldpfad entspricht dem JSON-Pfad im Request Body.

Feld-Error-Codes

Spezifische Codes für Feldvalidierung:

Code Bedeutung
REQUIRED Pflichtfeld fehlt
INVALID_FORMAT Format falsch (E-Mail, URL, ...)
INVALID_TYPE Falscher Datentyp
TOO_SHORT String zu kurz
TOO_LONG String zu lang
OUT_OF_RANGE Zahl außerhalb des Bereichs
INVALID_ENUM Wert nicht in erlaubter Liste
DUPLICATE Wert bereits vergeben
IMMUTABLE Feld darf nicht geändert werden

Request-ID und Trace-ID

Ohne Korrelation zwischen Client-Fehler und Server-Logs ist Debugging ein Ratespiel.

Request-ID

Jeder Request bekommt eine eindeutige ID, die in der Response zurückkommt:

HTTP/1.1 400 Bad Request
X-Request-Id: req_abc123xyz
Content-Type: application/problem+json

{
  "error_code": "VALIDATION_FAILED",
  "request_id": "req_abc123xyz"
}

Regel: Request-ID immer in Response Header und Body zurückgeben. Client kann sie für Support-Anfragen nutzen.

Trace-ID (W3C Trace Context)

Für verteilte Systeme reicht eine Request-ID nicht. Trace-IDs verbinden alle Logs über Service-Grenzen hinweg:

X-Request-Id: req_abc123xyz
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

Empfehlung:

  • Request-ID: einfache UUID, vom API-Gateway generiert
  • Trace-ID: W3C Trace Context (traceparent), OpenTelemetry nutzt diesen Standard

Logging

Server-Logs müssen die Request-ID enthalten:

{
  "timestamp": "2026-01-09T10:15:30Z",
  "level": "error",
  "request_id": "req_abc123xyz",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "error_code": "VALIDATION_FAILED",
  "path": "/v1/orders",
  "message": "Validation failed: email invalid"
}

Was nicht in Fehlern stehen darf

Fehlermeldungen sind öffentlich. Alles, was drinsteht, kann ein Angreifer sehen.

Verboten

Kategorie Beispiel Risiko
Stack Traces at OrderService.create(OrderService.java:42) Code-Struktur offengelegt
SQL-Queries SELECT * FROM users WHERE id = '...' SQL Injection erleichtert
Interne IDs Database connection pool_7 exhausted Infrastruktur offengelegt
Dateipfade /var/www/app/config/secrets.yml Pfadstruktur offengelegt
Credentials API key sk_live_... is invalid Key-Format offengelegt
Interne Fehler NullPointerException in UserMapper Implementierung offengelegt

Erlaubt

Kategorie Beispiel
Feldname "field": "email"
Erwartetes Format "message": "Must be a valid email address"
Bereich "message": "Must be between 1 and 100"
Erlaubte Werte "message": "Must be one of: pending, active, cancelled"
Request-ID "request_id": "req_abc123"

Produktions- vs. Development-Modus

Im Development-Modus sind mehr Details hilfreich. Aber niemals in Produktion:

// RICHTIG: Unterschiedliche Details je nach Umgebung
const errorResponse = {
    error_code: 'INTERNAL_ERROR',
    message: 'An unexpected error occurred.',
    request_id: requestId,
    // Nur in Development:
    ...(isDevelopment && {debug: {stack: error.stack}})
};

Validierung: Schema und Business Rules

Validierung passiert auf zwei Ebenen, die unterschiedliche Fehler produzieren.

Schema-Validierung

Technische Prüfung des Request-Formats:

  • Pflichtfelder vorhanden?
  • Datentypen korrekt?
  • Formate gültig (E-Mail, URL, Datum)?
  • Längen im Rahmen?

HTTP-Status: 400 Bad Request

{
  "error_code": "VALIDATION_FAILED",
  "errors": [
    {
      "field": "email",
      "code": "INVALID_FORMAT",
      "message": "Must be a valid email address."
    }
  ]
}

Business-Rule-Validierung

Fachliche Prüfung nach Schema-Validierung:

  • Existiert die referenzierte Ressource?
  • Hat der User genug Guthaben?
  • Ist die Aktion im aktuellen Status erlaubt?

HTTP-Status: 422 Unprocessable Entity

{
  "error_code": "INSUFFICIENT_BALANCE",
  "detail": "Account balance is insufficient for this order.",
  "request_id": "req_abc123"
}

Validierungsreihenfolge

  1. Schema-Validierung zuerst (schnell, ohne DB-Zugriff)
  2. Business-Rules danach (kann DB-Queries erfordern)

Warum? Schema-Fehler sind billig zu prüfen. Business-Rules können teuer sein. Bei ungültigem Schema lohnt sich die Business-Prüfung nicht.

Alle Fehler auf einmal

Gib alle Validierungsfehler in einer Response zurück, nicht nur den ersten:

{
  "error_code": "VALIDATION_FAILED",
  "errors": [
    {
      "field": "email",
      "code": "INVALID_FORMAT",
      "message": "Must be a valid email address."
    }
  ]
}

Warum? Ein Roundtrip pro Fehler ist frustrierend für Clients.

Graceful Degradation

Wenn Downstream-Services ausfallen, sollte die API nicht einfach 500 zurückgeben.

HTTP-Status für Downstream-Fehler

Situation Status Error Code
Downstream antwortet nicht 502 Bad Gateway DOWNSTREAM_UNAVAILABLE
Downstream ist überlastet 503 Service Unavailable SERVICE_UNAVAILABLE
Timeout bei Downstream 504 Gateway Timeout DOWNSTREAM_TIMEOUT

Retry-After Header

Bei temporären Fehlern hilft Retry-After:

HTTP/1.1 503 Service Unavailable
Retry-After: 30
Content-Type: application/problem+json

{
  "error_code": "SERVICE_UNAVAILABLE",
  "detail": "The service is temporarily unavailable. Please retry later.",
  "request_id": "req_abc123"
}

Partial Success

Manchmal ist ein Teil der Operation erfolgreich. Das ist komplexer zu modellieren:

{
  "status": "partial",
  "succeeded": [
    {
      "id": "item_001",
      "status": "created"
    }
  ],
  "failed": [
    {
      "id": "item_002",
      "error_code": "VALIDATION_FAILED",
      "message": "Quantity out of range"
    }
  ]
}

HTTP-Status: 207 Multi-Status (bei Batch-Operationen)

Empfehlung: Vermeide Partial Success wenn möglich. Transaktionale Operationen (alles oder nichts) sind einfacher zu verstehen.

Regeln & Anti-Patterns

Do

  • Einheitliches Fehlerformat für alle Endpoints
  • Maschinenlesbare Error Codes mit stabiler Namenskonvention
  • Feldfehler strukturiert mit Pfad, Code und Message
  • Request-ID in jedem Fehler
  • Schema-Validierung vor Business-Rules
  • Alle Validierungsfehler in einer Response
  • Retry-After bei temporären Fehlern

Don't

  • Stack Traces in Produktion zurückgeben
  • SQL-Queries oder interne Pfade in Fehlern
  • Unterschiedliche Fehlerformate pro Endpoint
  • Error Codes ohne Dokumentation
  • Nur den ersten Validierungsfehler zurückgeben
  • 500 für alle Fehler (egal ob Client- oder Serverfehler)
  • Sensitive Daten in Fehlermeldungen

Artefakt: Error-Katalog

# Error-Katalog

## Format

Alle Fehler folgen RFC 9457 (Problem Details) mit Erweiterungen:

```json
{
  "type": "https://api.example.com/errors/{error_code}",
  "title": "Human-readable title",
  "status": 400,
  "detail": "Specific details about this error instance.",
  "instance": "/v1/resource/id",
  "error_code": "ERROR_CODE",
  "request_id": "req_xxx",
  "errors": []
}
```

## Error Codes

### Validierung (400)

| Code                 | Title                | Detail                                |
|----------------------|----------------------|---------------------------------------|
| VALIDATION_FAILED    | Validation Failed    | Request body contains invalid fields. |
| REQUEST_BODY_MISSING | Request Body Missing | Request body is required.             |
| REQUEST_BODY_INVALID | Invalid Request Body | Request body is not valid JSON.       |

### Feld-Validierung (in errors[])

| Code           | Bedeutung                        |
|----------------|----------------------------------|
| REQUIRED       | Pflichtfeld fehlt                |
| INVALID_FORMAT | Format ungültig                  |
| INVALID_TYPE   | Falscher Datentyp                |
| TOO_SHORT      | Zu kurz                          |
| TOO_LONG       | Zu lang                          |
| OUT_OF_RANGE   | Außerhalb des erlaubten Bereichs |
| INVALID_ENUM   | Nicht in erlaubter Werteliste    |

### Authentifizierung (401)

| Code               | Title                   | Detail                            |
|--------------------|-------------------------|-----------------------------------|
| AUTH_TOKEN_MISSING | Authentication Required | No authentication token provided. |
| AUTH_TOKEN_INVALID | Invalid Token           | The provided token is invalid.    |
| AUTH_TOKEN_EXPIRED | Token Expired           | The provided token has expired.   |

### Autorisierung (403)

| Code                    | Title                    | Detail                                |
|-------------------------|--------------------------|---------------------------------------|
| AUTH_INSUFFICIENT_SCOPE | Insufficient Permissions | Token lacks required scope.           |
| AUTH_FORBIDDEN          | Access Denied            | Access to this resource is forbidden. |

### Ressourcen (404, 409)

| Code                    | Status | Title                   |
|-------------------------|--------|-------------------------|
| RESOURCE_NOT_FOUND      | 404    | Resource Not Found      |
| RESOURCE_ALREADY_EXISTS | 409    | Resource Already Exists |
| RESOURCE_CONFLICT       | 409    | Resource Conflict       |

### Business Rules (422)

| Code                 | Title                | Detail                                  |
|----------------------|----------------------|-----------------------------------------|
| INSUFFICIENT_BALANCE | Insufficient Balance | Account balance is too low.             |
| INVALID_STATE        | Invalid State        | Operation not allowed in current state. |
| LIMIT_EXCEEDED       | Limit Exceeded       | Operation exceeds allowed limit.        |

### Rate Limiting (429)

| Code                | Title             | Headers                    |
|---------------------|-------------------|----------------------------|
| RATE_LIMIT_EXCEEDED | Too Many Requests | Retry-After, RateLimit-Policy, RateLimit |
| QUOTA_EXCEEDED      | Quota Exceeded    | Retry-After                |

### Server (500, 502, 503, 504)

| Code                   | Status | Title                 |
|------------------------|--------|-----------------------|
| INTERNAL_ERROR         | 500    | Internal Server Error |
| DOWNSTREAM_UNAVAILABLE | 502    | Service Unavailable   |
| SERVICE_UNAVAILABLE    | 503    | Service Unavailable   |
| DOWNSTREAM_TIMEOUT     | 504    | Gateway Timeout       |

## Beispiel-Responses

### Validierungsfehler (400)

```json
{
  "type": "https://api.example.com/errors/validation-failed",
  "title": "Validation Failed",
  "status": 400,
  "error_code": "VALIDATION_FAILED",
  "request_id": "req_abc123",
  "errors": [
    {
      "field": "email",
      "code": "INVALID_FORMAT",
      "message": "Must be a valid email address."
    },
    {
      "field": "quantity",
      "code": "OUT_OF_RANGE",
      "message": "Must be between 1 and 100."
    }
  ]
}
```

### Authentifizierungsfehler (401)

```json
{
  "type": "https://api.example.com/errors/auth-token-expired",
  "title": "Token Expired",
  "status": 401,
  "error_code": "AUTH_TOKEN_EXPIRED",
  "detail": "The provided token has expired. Please refresh your token.",
  "request_id": "req_def456"
}
```

### Rate Limit (429)

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
RateLimit-Policy: "user";q=100;w=60
RateLimit: "user";r=0;t=60

{
  "type": "https://api.example.com/errors/rate-limit-exceeded",
  "title": "Too Many Requests",
  "status": 429,
  "error_code": "RATE_LIMIT_EXCEEDED",
  "detail": "Rate limit exceeded. Please wait before retrying.",
  "request_id": "req_ghi789"
}
```

Checkliste

Bevor du zum nächsten Artikel gehst, prüfe:

  • [ ] Fehlerformat ist definiert (RFC 9457 oder Custom)
  • [ ] Content-Type für Fehler ist festgelegt
  • [ ] Error Codes sind dokumentiert und stabil
  • [ ] Feldfehler-Struktur ist definiert (field, code, message)
  • [ ] Request-ID wird in allen Fehlern zurückgegeben
  • [ ] Keine sensitiven Informationen in Fehlermeldungen
  • [ ] Schema-Validierung passiert vor Business-Rules
  • [ ] Alle Validierungsfehler werden auf einmal zurückgegeben
  • [ ] Downstream-Fehler haben eigene Status (502, 503, 504)
  • [ ] Retry-After wird bei temporären Fehlern gesetzt
  • [ ] Error-Katalog ist dokumentiert

Wie es weitergeht

Im nächsten Teil geht es um Authentifizierung: OAuth2 vs. API Keys, Token Lifetimes und warum JWT nicht immer die richtige Wahl ist.

Alle Teile der Serie: Serie: API-Design

Mehr Beiträge aus dem Blog.