{ }
GET
REST API Design · HTTP · Idempotenz · Statuscodes · Ressourcen
REST API Design: Ressourcen, Verben, Idempotenz
und Statuscodes sauber trennen

Viele REST-APIs nutzen HTTP als Transportprotokoll, ohne seine Semantik auszuschöpfen. Ressourcen falsch modelliert, Verben falsch eingesetzt, Idempotenz nicht garantiert und Statuscodes willkürlich gewählt – das sind keine Stilfragen, sondern Designfehler mit direkten Auswirkungen auf Caching, Retry-Logik und Client-Implementierung.

17 Min. Lesezeit GET · POST · PUT · PATCH · DELETE · Idempotenz · 2xx · 4xx · 5xx RFC 7231 · RFC 7807 · Symfony 7 · REST-Constraints

1. Ressourcenmodellierung: Nomen statt Verben in URLs

Die Grundregel der REST-Ressourcenmodellierung lautet: URLs identifizieren Ressourcen (Dinge), keine Aktionen (Verben). /getUser, /createOrder oder /deleteProduct sind RPC-Muster, keine REST-URLs. Die REST-Alternative: /users/{id}, /orders, /products/{id}. Das HTTP-Verb übernimmt die Semantik der Aktion. Diese Trennung ist nicht semantischer Purismus – sie hat praktische Konsequenzen: REST-konforme URLs ermöglichen generische Proxies, Caches und Gateways, die auf Basis des HTTP-Verbs und der URL entscheiden, wie sie die Anfrage behandeln.

Pluralisierung ist Konvention, nicht Pflicht, aber sie schafft Konsistenz: /users für die Kollektion, /users/{id} für ein einzelnes Element. Hierarchische Ressourcen werden durch die URL-Struktur ausgedrückt: /orders/{id}/items für die Items einer Bestellung. Flache Hierarchien sind besser als tiefe: /order-items/{id} ist oft sinnvoller als /orders/{id}/items/{itemId}/details/{detailId}. Die Faustregel: mehr als zwei Ebenen deuten oft auf ein Modellierungsproblem hin.

# REST-konforme URL-Struktur — Beispiele aus einem Shop-System

# Collections (Plural)
GET    /products              # List all products (with pagination)
POST   /products              # Create a new product

# Single resource
GET    /products/42           # Get product 42
PUT    /products/42           # Replace product 42 completely
PATCH  /products/42           # Partially update product 42
DELETE /products/42           # Delete product 42

# Sub-resources (max. 2 levels deep)
GET    /orders/99/items       # List items of order 99
POST   /orders/99/items       # Add item to order 99
DELETE /orders/99/items/5     # Remove item 5 from order 99

# WRONG — RPC-style URLs (never do this)
# POST /createProduct
# GET  /getProductById?id=42
# POST /deleteProduct/42
# GET  /product/getActiveOnes

2. HTTP-Verben: Semantik statt Konvention

HTTP-Verben haben semantische Bedeutungen, die über reine Konvention hinausgehen und von Proxies, Caches und Browsern ausgewertet werden. GET ist safe und idempotent: kein Seiteneffekt, wiederholbar ohne Konsequenz. Proxies und Caches dürfen GET-Requests speichern und wiederholen. POST ist weder safe noch idempotent: erzeugt Seiteneffekte und kann bei Wiederholung doppelte Ressourcen anlegen. PUT ist idempotent, aber nicht safe: ersetzt eine Ressource vollständig, mehrfache Ausführung hat denselben Effekt. PATCH ist weder safe noch idempotent (per Spezifikation), kann aber idempotent implementiert werden. DELETE ist idempotent: nach dem ersten erfolgreichen Delete gibt es nichts mehr zu löschen.

Ein häufiger Fehler: alle schreibenden Operationen als POST implementieren. POST /products/42/update statt PUT /products/42 oder PATCH /products/42. Das verhindert, dass Clients und Proxies die korrekte Semantik nutzen können. Ein anderer Fehler: DELETE-Requests mit einem Request-Body versehen. HTTP erlaubt es technisch, aber viele Proxies ignorieren oder entfernen DELETE-Body-Daten. Wenn ein DELETE-Request Parameter braucht, gehören sie in die URL oder als Query-Parameter.

3. Idempotenz: GET, PUT, DELETE vs. POST und PATCH

Idempotenz bedeutet: mehrfache Ausführung derselben Operation hat denselben Effekt wie eine einzige Ausführung. Diese Eigenschaft ist für Retry-Logik entscheidend: wenn ein Client nicht sicher weiß, ob seine Anfrage den Server erreicht hat (z.B. nach einem Netzwerktimeout), kann er eine idempotente Anfrage sicher wiederholen. Eine nicht-idempotente Anfrage dagegen kann nicht blind wiederholt werden – es könnte zu doppelten Bestellungen, doppelten Zahlungen oder anderen unerwünschten Seiteneffekten kommen.

Für POST gibt es einen Mechanismus zur Idempotenz: den Idempotency-Key-Header (weit verbreitet in Payment-APIs wie Stripe). Der Client generiert eine UUID für jede Operation und sendet sie als Header. Der Server speichert das Ergebnis unter dieser UUID und gibt bei einer Wiederholung das gecachte Ergebnis zurück, statt die Operation erneut auszuführen. Dieser Mechanismus ist besonders für kritische Operationen wie Bestellungen und Zahlungen unverzichtbar. In Symfony kann er als Middleware implementiert werden, die den Key prüft, bevor der Controller erreicht wird.

 ['onRequest', 20],
            KernelEvents::RESPONSE => ['onResponse', 0],
        ];
    }

    public function onRequest(RequestEvent $event): void
    {
        $request = $event->getRequest();
        if ($request->getMethod() !== 'POST') {
            return;
        }

        $key = $request->headers->get('Idempotency-Key');
        if ($key === null || strlen($key) < 8 || strlen($key) > 255) {
            return; // Key optional — only enforce on /payments/* routes
        }

        $cacheKey = 'idempotency_' . hash('sha256', $key . $request->getPathInfo());
        $cached = $this->idempotencyCache->get($cacheKey, function (ItemInterface $item) {
            $item->expiresAfter(self::TTL);
            return null; // Not yet cached
        });

        if ($cached !== null) {
            // Replay cached response
            $response = new JsonResponse(
                json_decode($cached['body'], true),
                $cached['status'],
                array_merge($cached['headers'], ['X-Idempotent-Replayed' => 'true'])
            );
            $event->setResponse($response);
        }

        $request->attributes->set('_idempotency_key', $cacheKey);
    }

    public function onResponse(ResponseEvent $event): void
    {
        $request = $event->getRequest();
        $cacheKey = $request->attributes->get('_idempotency_key');
        if ($cacheKey === null) {
            return;
        }

        $response = $event->getResponse();
        if ($response->getStatusCode() >= 500) {
            return; // Don't cache server errors
        }

        $this->idempotencyCache->delete($cacheKey);
        $this->idempotencyCache->get($cacheKey, function (ItemInterface $item) use ($response) {
            $item->expiresAfter(self::TTL);
            return [
                'status' => $response->getStatusCode(),
                'body' => $response->getContent(),
                'headers' => ['Content-Type' => $response->headers->get('Content-Type')],
            ];
        });
    }
}

4. Statuscodes: präzise statt approximiert

HTTP-Statuscodes sind semantische Signale, keine Konvention. 200 zurückzugeben, wenn eine Ressource nicht gefunden wurde, oder 500 für Validierungsfehler zu verwenden, bricht die Erwartungen aller Clients und Proxies. Die häufigsten Statuscode-Fehler in REST-APIs: 200 für alles, 400 für alle Client-Fehler ohne Unterscheidung und 500 für alle Server-Fehler. Die korrekte Zuordnung erfordert ein Verständnis der jeweiligen Semantik. 200 OK: Ressource gefunden und zurückgegeben. 201 Created: neue Ressource erzeugt, Location-Header enthält die URL. 204 No Content: erfolgreich, kein Response-Body (typisch für DELETE und erfolgreiche PATCH-Operationen ohne Body-Rückgabe).

Bei Client-Fehlern: 400 Bad Request für syntaktisch fehlerhafte Anfragen (invalid JSON). 401 Unauthorized: kein gültiges Token vorhanden. 403 Forbidden: Token gültig, aber keine Berechtigung für diese Ressource. 404 Not Found: Ressource existiert nicht. 409 Conflict: Konflikt mit dem aktuellen Zustand (z.B. Optimistic Locking, doppelte Email-Adresse). 422 Unprocessable Entity: JSON syntaktisch korrekt, aber Validierung schlägt fehl (fehlende Pflichtfelder, ungültige Werte). 429 Too Many Requests: Rate Limit überschritten. 503 Service Unavailable: Server vorübergehend nicht erreichbar (Wartung, Überlast).

5. Sub-Ressourcen und Beziehungen modellieren

Beziehungen zwischen Ressourcen werden in REST auf zwei Arten modelliert: als eingebettete Daten (inline in der Repräsentation) oder als separate Sub-Ressource mit eigenem URL. Die Entscheidung hängt davon ab, wie die Daten typischerweise abgefragt werden. Order-Items werden fast immer zusammen mit der Bestellung abgefragt – Einbettung ist sinnvoll. Ein User-Profil-Bild ist optional und wird seltener benötigt – separate Ressource ist besser. Zu tiefe URL-Hierarchien (/users/{id}/addresses/{aId}/phones/{pId}) sind ein Signal, dass die Modellierung vereinfacht werden sollte.

Für Many-to-Many-Beziehungen eignen sich oft eigenständige Ressourcen für die Beziehung selbst: /product-categories/{productId}/{categoryId} statt der Beziehung nur über verschachtelte URLs zu modellieren. Das ermöglicht saubere CRUD-Operationen auf die Beziehung. Für die Darstellung von Beziehungen in der Response-Repräsentation sind zwei Ansätze verbreitet: IDs (kompakt, erfordert Folge-Requests) oder eingebettete Objekte mit einem ?embed=category-Parameter (flexibel, aber komplexer zu implementieren).

6. Aktionen auf Ressourcen: wenn Verben Sinn ergeben

Nicht alle API-Operationen lassen sich natürlich als CRUD auf Ressourcen abbilden. Das Stornieren einer Bestellung, das Publizieren eines Artikels oder das Versenden einer E-Mail sind Aktionen, die schwer als reines Ressourcenupdate modelliert werden. Die REST-konforme Lösung: Aktionen als Sub-Ressource modellieren. POST /orders/{id}/cancellations statt POST /orders/{id}/cancel. Das Erstellen einer "Stornierung" ist eine POST-Operation auf eine neue Ressource, die impliziert, dass die Bestellung storniert wird.

Alternativ kann der Zustand der Ressource explizit über PATCH aktualisiert werden: PATCH /orders/{id} mit {"status": "cancelled"}. Das ist einfacher, aber weniger ausdrucksstark bei komplexen Zustandsübergängen mit zusätzlichen Parametern (Stornierungsgrund, Teilbeträge etc.). Die Faustregel: wenn die Aktion zu einer neuen Entität führt oder mehrere Parameter hat, Sub-Ressource verwenden. Wenn es sich um einen einfachen Zustandsübergang handelt, PATCH auf die Hauptressource ist ausreichend.

# OpenAPI: Aktionen als Sub-Ressourcen — REST-konforme Modellierung
paths:
  # Bestellung stornieren — Stornierung ist eine eigene Ressource
  /orders/{orderId}/cancellations:
    post:
      operationId: cancelOrder
      summary: "Cancel an order"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason:
                  type: string
                  enum: [customer_request, fraud, out_of_stock]
                refundAmount:
                  type: number
                  format: float
      responses:
        "201":
          description: "Cancellation created — order is now cancelled"
          headers:
            Location:
              schema:
                type: string
              description: "URL of the cancellation resource"
        "409":
          description: "Order already cancelled or shipped"
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"

  # Produkt publizieren — Status-Übergang via PATCH
  /products/{id}:
    patch:
      operationId: updateProduct
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                status:
                  type: string
                  enum: [draft, published, archived]

7. Content Negotiation und Accept-Header

Content Negotiation ermöglicht es einer API, verschiedene Repräsentationen derselben Ressource auszuliefern – abhängig davon, was der Client anfragt. Der Accept-Header signalisiert, welche MIME-Types der Client versteht. Accept: application/json ist Standard für REST-APIs. Accept: text/csv kann für Export-Endpoints sinnvoll sein. Accept: application/vnd.mironsoft.v2+json ist ein Vendor-spezifischer Media Type, der Versionierung über Header-statt-URL ermöglicht.

In Symfony ist Content Negotiation über den Request::getPreferredFormat()-Mechanismus verfügbar. Eine häufige Fehlerquelle: wenn der Client einen Accept-Header sendet, den der Server nicht unterstützt, muss der Server mit 406 Not Acceptable antworten – nicht mit dem Standard-Format ohne Warnung. 406 wird in der Praxis oft vergessen oder nicht implementiert, was zu stillen Format-Fehlern führt, die der Client erst beim Parsen bemerkt.

8. Vergleich: Anti-Patterns vs. REST-konformes Design

Die häufigsten REST-Design-Fehler in der Praxis sind keine Unkenntnis der HTTP-Spezifikation, sondern Gewohnheiten aus der RPC-Entwicklung oder schnelles Ad-hoc-Design ohne systematische Entscheidungen. Die folgende Tabelle zeigt die verbreitetsten Anti-Patterns und ihre REST-konformen Alternativen.

Anti-Pattern Problem REST-konforme Alternative Konsequenz
POST /getProduct Verb in URL, falsche Methode GET /products/42 Caching und Proxies funktionieren
200 für 404 Clients können nicht unterscheiden 404 Not Found Monitoring und Retry-Logik korrekt
POST für Update Idempotenz verloren PUT (vollständig) / PATCH (partiell) Sichere Retries möglich
400 für alles Kein Unterschied Auth/Validation 401 / 403 / 422 präzise Clients können korrekt reagieren
Kein Location-Header bei 201 Client muss URL raten Location: /products/43 Header Kein Folge-Request nötig

9. Zusammenfassung

REST-konformes API-Design ist kein Dogma, aber ein Werkzeugkasten aus bewährten Konventionen, der Caching, Retry-Logik und Client-Implementierung fundamental vereinfacht. Ressourcen durch Nomen in URLs modellieren. HTTP-Verben nach ihrer semantischen Bedeutung einsetzen, nicht als Konvention. Idempotenz für GET, PUT und DELETE garantieren und für POST über Idempotency-Keys nachrüsten. Statuscodes präzise zuordnen: 201 mit Location-Header, 422 für Validierungsfehler, 409 für Konflikte. Sub-Ressourcen für komplexe Aktionen verwenden.

Der praktische Nutzen dieser Prinzipien zeigt sich vor allem in der Zusammenarbeit zwischen Teams: ein API-Design, das HTTP-Semantik korrekt nutzt, ist für erfahrene HTTP-Clients sofort verständlich, ohne zusätzliche Dokumentation für jede Methode. Proxies, Load Balancer und API-Gateways können auf Basis von Verb und URL automatisch korrekte Entscheidungen treffen. Das reduziert Fehlkonfigurationen und Debugging-Aufwand in der gesamten Infrastruktur.

REST API Design — Das Wichtigste auf einen Blick

Ressourcen

URLs identifizieren Dinge (Nomen), nicht Aktionen. Plural für Collections. Max. 2 Ebenen in der Hierarchie. Sub-Ressourcen für Beziehungen.

HTTP-Verben

GET: safe + idempotent. PUT: idempotent (vollständig). PATCH: partiell. DELETE: idempotent. POST: erzeugt neue Ressourcen, nicht idempotent.

Statuscodes

201 + Location bei Create. 204 bei Delete. 401 vs. 403 trennen. 422 für Validierung. 409 für Konflikte. 429 für Rate Limits.

Idempotenz

POST mit Idempotency-Key-Header für kritische Operationen. Server cached Ergebnis für 24h und replayed bei Wiederholung.

10. FAQ: REST API Design

1Unterschied PUT vs. PATCH?
PUT: vollständiger Ersatz (alle Felder). PATCH: partielle Aktualisierung (nur gesendete Felder). PUT ist idempotent per Spec, PATCH nicht zwingend.
2401 vs. 403?
401: kein Token vorhanden, anmelden hilft. 403: Token gültig, aber kein Zugriff auf diese Ressource für diesen User.
3Warum Location-Header bei 201?
URL der neuen Ressource direkt im Header. Client muss nicht raten, kein Folge-Request nötig. Viele HTTP-Clients folgen Location-Header automatisch.
4Was ist Idempotenz?
Mehrfache Ausführung = selber Effekt wie einmalige. Ermöglicht sichere Retries bei Netzwerktimeouts ohne Doppelbestellungen oder Doppelzahlungen.
5Aktionen wie "cancel" RESTful modellieren?
Sub-Ressource: POST /orders/{id}/cancellations. Oder Zustandsübergang: PATCH /orders/{id} mit {status: cancelled}. Sub-Ressource bei komplexen Parametern bevorzugen.
6400 vs. 422?
400: syntaktisch falsch (ungültiges JSON). 422: syntaktisch korrekt, aber Validierung schlägt fehl (Pflichtfeld fehlt, Business-Rule verletzt).
7Wie tief URL-Hierarchien?
Maximal zwei Ebenen. Tiefer deutet auf Modellierungsproblem hin. Flache Ressourcen (/order-items/{id}) bevorzugen.
8Content Negotiation?
Client signalisiert gewünschtes Format über Accept-Header. Server antwortet im passenden Format. 406 Not Acceptable wenn Format nicht unterstützt.
9Idempotency-Key für POST?
Client schickt UUID als Idempotency-Key-Header. Server cached Ergebnis für 24h. Wiederholter Request gibt gecachtes Ergebnis zurück.
10DELETE ohne Body?
Viele Proxies ignorieren DELETE-Bodies. Parameter in URL oder Query-String, nie im Body. HTTP erlaubt es, Infrastruktur ignoriert es oft.