TS
Thomas Schmitz

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

Serie: API-Design · Teil 5 von 22

API-Design Teil 5: HTTP-Semantik & Statuscodes

Wann POST vs. PUT vs. PATCH, und wie du mit Async, Konflikten und Rate Limits umgehst.

Praxisnah Checkliste

HTTP ist mehr als ein Transportprotokoll – es ist ein semantisches Framework. Methoden haben Bedeutung, Statuscodes kommunizieren Ergebnisse, und Header steuern Verhalten. Wer diese Semantik ignoriert, baut APIs, die Clients verwirren, Caching brechen und Debugging erschweren.

Dieser Artikel hilft dir, HTTP-Methoden korrekt einzusetzen und Statuscodes konsistent zu verwenden. Am Ende hast du eine Methoden/Statuscode-Matrix, die als Referenz für dein Team dient.

Zielbild

Nach diesem Artikel kannst du:

  • HTTP-Methoden entsprechend ihrer Semantik einsetzen
  • Den Unterschied zwischen POST, PUT und PATCH klar begründen
  • Statuscodes korrekt und konsistent verwenden
  • Async-Operationen, Konflikte und Rate Limits richtig kommunizieren
  • Edge Cases wie Partial Success und Conditional Requests handhaben

Kernfragen

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

  1. Wann POST, wann PUT, wann PATCH? Kannst du den Unterschied erklären?
  2. Welche Statuscodes nutzt ihr? Nur 200, 400, 500 – oder differenzierter?
  3. Wie kommuniziert ihr Async-Operationen? Polling, Webhooks, 202?
  4. Wie geht ihr mit Konflikten um? Optimistic Locking, Last-Write-Wins?
  5. Wie signalisiert ihr Rate Limits? 429 mit Retry-After?

HTTP-Methoden korrekt einsetzen

GET – Lesen ohne Seiteneffekte

GET ruft Daten ab, ohne den Serverzustand zu ändern. GET-Requests sind:

  • Safe: Keine Seiteneffekte, nur Lesen
  • Idempotent: Mehrfaches Aufrufen hat keine zusätzlichen Seiteneffekte
  • Cacheable: Responses können gecacht werden
GET /orders/order_abc123

Regel: GET darf niemals Daten verändern. Auch nicht nur ein kleines Logging oder Zähler hochsetzen.

Anti-Pattern: GET /orders/order_abc123/markAsRead – das ist ein Seiteneffekt und gehört zu POST oder PATCH.

POST – Erstellen oder Aktionen ausführen

POST erstellt neue Ressourcen oder führt Aktionen aus. POST-Requests sind:

  • Nicht safe: Ändert Serverzustand
  • Nicht idempotent: Zweimal ausführen kann zwei Ressourcen erstellen
  • Nicht cacheable (default): Responses werden nur mit expliziten Cache-Headern gecacht
POST /orders
Content-Type: application/json

{ "customer_id": "user_xyz", "items": [] }

Response:

HTTP/1.1 201 Created
Location: /orders/order_abc123

{ "id": "order_abc123" }

Regel: POST für alles, was eine neue Ressource erstellt oder eine Aktion ausführt, die nicht idempotent ist.

PUT – Vollständig ersetzen

PUT ersetzt eine Ressource vollständig. Der Client sendet die komplette Repräsentation, der Server überschreibt alles.

  • Nicht safe: Ändert Serverzustand
  • Idempotent: Zweimal dasselbe senden = dasselbe Ergebnis
  • Semantik: Hier ist die neue Version, ersetze die alte komplett
PUT /users/user_abc123
Content-Type: application/json

{
  "name": "Max Mustermann",
  "email": "max@example.com",
  "role": "admin"
}

Regel: Bei PUT muss der Client alle Felder senden. Fehlende Felder gelten als entfernt bzw. überschrieben – nicht ignoriert.

Anti-Pattern: PUT, das nur gesendete Felder aktualisiert. Das ist PATCH-Semantik.

PATCH – Teilweise aktualisieren

PATCH aktualisiert eine Ressource partiell. Nur die gesendeten Felder ändern sich, der Rest bleibt unverändert.

  • Nicht safe: Ändert Serverzustand
  • Nicht immer idempotent: Kommt auf die Operation an
  • Semantik: Ändere nur diese Felder
PATCH /users/user_abc123
Content-Type: application/json

{ "email": "newemail@example.com" }

Regel: PATCH ist der Normalfall für Updates. PUT ist selten nötig, außer du willst explizit alles ersetzen.

DELETE – Entfernen

DELETE entfernt eine Ressource.

  • Nicht safe: Ändert Serverzustand
  • Idempotent: Zweimal löschen = Ressource ist weg (zweiter Request liefert 204 oder 404)
  • Response: 200 mit Body, 204 ohne Body, oder 202 für Async
DELETE /orders/order_abc123

Regel: DELETE löscht nur die adressierte Ressource. Keine Seiteneffekte auf andere Ressourcen.

Frage: Was passiert bei DELETE auf nicht-existente Ressource? Zwei valide Ansätze:

  • 404 Not Found: Streng korrekt, Ressource existiert nicht
  • 204 No Content: Pragmatisch, Endzustand ist erreicht (Ressource weg)

Wähle einen Ansatz und bleib konsistent.

Methodenübersicht

Methode Safe Idempotent Cacheable (default) Typischer Use Case
GET Ja Ja Ja Ressource lesen
POST Nein Nein Nein Ressource erstellen, Aktion ausführen
PUT Nein Ja Nein Ressource vollständig ersetzen
PATCH Nein Nein* Nein Ressource teilweise aktualisieren
DELETE Nein Ja Nein Ressource löschen

*PATCH kann idempotent sein, ist es aber nicht garantiert.

Statuscodes richtig verwenden

Statuscodes sind keine Dekoration – sie sind maschinenlesbare Semantik. Clients und Middleware (Proxies, Caches, Monitoring) verlassen sich darauf.

2xx – Erfolg

Code Name Wann verwenden
200 OK Standard-Erfolg mit Response-Body
201 Created Ressource wurde erstellt (POST), Location-Header setzen
202 Accepted Request akzeptiert, Verarbeitung läuft async
204 No Content Erfolg ohne Response-Body (DELETE, manche PATCHs)

Regel: 201 immer mit Location-Header, der auf die neue Ressource zeigt.

HTTP/1.1 201 Created
Location: /orders/order_abc123

3xx – Redirection

Code Name Wann verwenden
301 Moved Permanently Ressource dauerhaft umgezogen
302 Found Temporäre Umleitung (vermeiden, 307 bevorzugen)
304 Not Modified Conditional Request, Ressource unverändert
307 Temporary Redirect Temporäre Umleitung, Methode beibehalten
308 Permanent Redirect Permanente Umleitung, Methode beibehalten

Wichtig: 301/302 können die HTTP-Methode ändern (POST → GET). Für APIs besser 307/308 verwenden.

4xx – Client-Fehler

Code Name Wann verwenden
400 Bad Request Syntaktisch invalider Request (z.B. JSON kaputt, Parsing-Fehler)
401 Unauthorized Nicht authentifiziert (kein Token, Token abgelaufen)
403 Forbidden Authentifiziert, aber nicht autorisiert
404 Not Found Ressource existiert nicht
405 Method Not Allowed Methode für diese URL nicht erlaubt
409 Conflict Konflikt mit aktuellem Zustand (z.B. Duplikat, Zustandskonflikt)
410 Gone Ressource existierte, ist aber dauerhaft entfernt
415 Unsupported Media Type Content-Type nicht unterstützt
422 Unprocessable Entity Syntaktisch korrekt, aber Validierung schlägt fehl
429 Too Many Requests Rate Limit überschritten

400 vs. 422: Klare Linie:

  • 400: Request ist syntaktisch kaputt (kein valides JSON)
  • 422: Request ist syntaktisch OK, aber Validierung schlägt fehl (z.B. Pflichtfeld fehlt)

401 vs. 403:

  • 401: Wer bist du? – Authentifizierung fehlt oder ungültig
  • 403: Ich weiß, wer du bist, aber du darfst das nicht

5xx – Server-Fehler

Code Name Wann verwenden
500 Internal Server Error Unerwarteter Fehler, Bug, Crash
502 Bad Gateway Upstream-Service antwortet nicht korrekt
503 Service Unavailable Server überlastet oder in Wartung
504 Gateway Timeout Upstream-Service antwortet zu langsam

Regel: 5xx bedeutet unser Problem. 4xx bedeutet dein Problem. Verwechsle das nie.

Anti-Pattern: 500 für Validierungsfehler. Das ist ein Client-Fehler (4xx).

Spezialfälle behandeln

Async-Operationen (202 Accepted)

Wenn eine Operation länger dauert, blockiere nicht – akzeptiere den Request und verarbeite im Hintergrund.

POST /exports
Content-Type: application/json

{ "format": "csv", "filters": {...} }

Response:

HTTP/1.1 202 Accepted
Location: /exports/export_abc123

{
  "id": "export_abc123",
  "status": "pending",
  "status_url": "/exports/export_abc123"
}

Client pollt status_url oder wartet auf Webhook:

GET /exports/export_abc123

{
  "id": "export_abc123",
  "status": "completed",
  "download_url": "/exports/export_abc123/download"
}

Regel: 202 heißt: Ich hab's verstanden und arbeite dran – nicht fertig.

Preconditions (412 Precondition Failed)

Konflikte entstehen bei konkurrierenden Änderungen. Optimistic Locking mit ETags oder Versionsnummern hilft.

Request mit If-Match (Optimistic Locking):

PATCH /orders/order_abc123
If-Match: "v3"
Content-Type: application/json

{ "status": "shipped" }

Wenn jemand anderes schneller war:

HTTP/1.1 412 Precondition Failed

{
  "error": {
    "code": "PRECONDITION_FAILED",
    "message": "Resource was modified by another request",
    "current_version": "v4"
  }
}

Regel: Bei 412 sollte der Client die aktuelle Version abrufen und die Änderung erneut versuchen.

Rate Limits (429 Too Many Requests)

Wenn Clients zu viele Requests senden, antworte mit 429 und hilfreichen Headern.

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

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests, please retry after 60 seconds"
  }
}

Wichtige Header:

  • Retry-After: Sekunden bis zum nächsten erlaubten Request
  • RateLimit-Policy: Policy-Definition (z.B. q=Quota, w=Window)
  • RateLimit: Aktueller Status (z.B. r=Remaining, t=Reset)

Hinweis: RateLimit und RateLimit-Policy folgen dem aktuellen IETF-Entwurf draft-ietf-httpapi-ratelimit-headers-10.

Conditional Requests (304 Not Modified)

Clients können fragen, ob sich etwas geändert hat, und Traffic sparen.

Request mit If-None-Match:

GET /orders/order_abc123
If-None-Match: "abc123hash"

Wenn unverändert:

HTTP/1.1 304 Not Modified
ETag: "abc123hash"

Wenn geändert: Normale 200-Response mit neuem ETag.

Partial Success bei Bulk-Operationen

Bei Bulk-Operationen können manche Items erfolgreich sein, andere nicht. Zwei Strategien:

Option 1: All-or-nothing (empfohlen für kritische Operationen)

HTTP/1.1 400 Bad Request

{
  "error": {
    "code": "BULK_VALIDATION_FAILED",
    "message": "Validation failed for some items",
    "details": [
      { "index": 2, "field": "email", "reason": "Invalid format" }
    ]
  }
}

Option 2: Multi-Status (207)

HTTP/1.1 207 Multi-Status

{
  "results": [
    { "index": 0, "status": 201, "id": "user_001" },
    { "index": 1, "status": 201, "id": "user_002" },
    { "index": 2, "status": 400, "error": { "code": "VALIDATION_ERROR" } }
  ]
}

Regel: Dokumentiere klar, welche Strategie gilt.

Regeln & Anti-Patterns

Do

  • GET nur für Lesen ohne Seiteneffekte
  • POST für Erstellen und nicht-idempotente Aktionen
  • PATCH für partielle Updates (der Normalfall)
  • PUT nur, wenn die Intention vollständiges Ersetzen ist
  • 201 mit Location-Header bei erfolgreicher Erstellung
  • 4xx für Client-Fehler, 5xx für Server-Fehler
  • Retry-After bei 429 und 503

Don't

  • GET mit Seiteneffekten (GET /markAsRead)
  • PUT für partielle Updates
  • 200 für alles (auch Fehler)
  • 500 für Validierungsfehler
  • 404 wenn du eigentlich 403 meinst (Security through obscurity ist kein Schutz)
  • Statuscodes ignorieren und Fehler nur im Body kommunizieren

Artefakt: Methoden/Statuscode-Matrix

Dokumentiere, welche Kombinationen in deiner API vorkommen:

# Methoden/Statuscode-Matrix

## Standard-Responses nach Methode

| Methode | Erfolg   | Client-Fehler                | Server-Fehler |
|---------|----------|------------------------------|---------------|
| GET     | 200, 304 | 400, 401, 403, 404           | 500, 503      |
| POST    | 201, 202 | 400, 401, 403, 409, 422      | 500, 503      |
| PUT     | 200, 204 | 400, 401, 403, 404, 409, 422 | 500, 503      |
| PATCH   | 200, 204 | 400, 401, 403, 404, 409, 422 | 500, 503      |
| DELETE  | 200, 204 | 401, 403, 404                | 500, 503      |

## Spezialfälle

| Szenario                      | Statuscode   | Anmerkung                       |
|-------------------------------|--------------|---------------------------------|
| Ressource erstellt            | 201          | Location-Header erforderlich    |
| Async-Operation gestartet     | 202          | Status-URL im Body              |
| Keine Änderung (Conditional)  | 304          | Kein Body                       |
| Validierungsfehler            | 400 oder 422 | Fehlerdetails im Body           |
| Nicht authentifiziert         | 401          | WWW-Authenticate-Header         |
| Nicht autorisiert             | 403          | —                               |
| Konflikt (Optimistic Locking) | 409          | Aktuelle Version im Body        |
| Rate Limit                    | 429          | Retry-After-Header erforderlich |
| Server überlastet             | 503          | Retry-After-Header empfohlen    |

## Header-Konventionen

| Header       | Wann            | Beispiel               |
|--------------|-----------------|------------------------|
| Location     | 201 Created     | `/orders/order_abc123` |
| ETag         | GET, PUT, PATCH | `"v3"`                 |
| Retry-After  | 429, 503        | `60` (Sekunden)        |
| X-Request-Id | Alle Responses  | `req_abc123`           |

Checkliste

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

  • [ ] HTTP-Methoden werden gemäß ihrer Semantik verwendet
  • [ ] GET hat keine Seiteneffekte
  • [ ] POST vs. PUT vs. PATCH ist klar abgegrenzt
  • [ ] 201 inkludiert Location-Header
  • [ ] 4xx vs. 5xx wird korrekt unterschieden
  • [ ] 401 vs. 403 ist klar definiert
  • [ ] Async-Operationen nutzen 202 mit Status-URL
  • [ ] Rate Limits nutzen 429 mit Retry-After
  • [ ] Methoden/Statuscode-Matrix ist dokumentiert

Wie es weitergeht

Im nächsten Teil geht es um Request/Response-Design: Was bedeutet null vs. fehlendes Feld? Wie verhinderst du Typwechsel? Und wie gehst du mit Geld, Datum und großen Payloads um?

Alle Teile der Serie: Serie: API-Design

Mehr Beiträge aus dem Blog.