TS
Thomas Schmitz

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

Serie: API-Design · Teil 4 von 22

API-Design Teil 4: Ressourcenmodellierung

Welche Substantive werden zu Ressourcen, wie stabil müssen IDs sein, und wie tief darf Nesting gehen.

Praxisnah Checkliste

Ressourcen sind das Rückgrat jeder REST-API. Sie bestimmen, wie Clients mit deinem System interagieren, welche URLs existieren und wie Daten strukturiert sind. Ein schlechtes Ressourcenmodell führt zu umständlichen Endpoints, inkonsistenten Hierarchien und APIs, die niemand intuitiv versteht.

Dieser Artikel hilft dir, Ressourcen sauber zu identifizieren, IDs richtig zu gestalten und Hierarchien bewusst zu begrenzen. Am Ende hast du einen Ressourcenkatalog und eine URL-Map, die als Grundlage für alle weiteren Designentscheidungen dienen.

Zielbild

Nach diesem Artikel kannst du:

  • Substantive von Aktionen unterscheiden und richtig modellieren
  • Stabile, sichere Identifikatoren für Ressourcen wählen
  • Hierarchien und Nesting bewusst begrenzen
  • Zwischen Referenzen und Embedding entscheiden
  • Batch- und Bulk-Operationen sinnvoll einsetzen

Kernfragen

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

  1. Was sind die Kern-Ressourcen? Welche Substantive repräsentieren deine Domäne?
  2. Wie stabil müssen IDs sein? Können sie sich ändern? Sind sie öffentlich?
  3. Wie tief darf Nesting gehen? /users/{id}/orders/{id}/items/{id} – zu viel?
  4. Referenz oder Embedding? Wann liefern wir IDs, wann vollständige Objekte?
  5. Brauchen wir Bulk-Operationen? Oder reichen Einzelanfragen?

Ressourcen identifizieren

Substantive vs. Aktionen

Ressourcen sind Substantive – Dinge, die existieren und einen Zustand haben. Aktionen sind Verben – Operationen, die etwas tun.

Beispiel Ressource? Begründung
User Ja Hat Zustand, kann gelesen/aktualisiert werden
Order Ja Existiert unabhängig, hat Lebenszyklus
Payment Ja Eigene Entität mit eigenem Zustand
Login Nein Aktion, keine persistente Entität
Export Kommt drauf an Einmalige Aktion vs. persistenter Export-Job

Regel: Wenn du etwas mit GET abrufen und später wieder finden willst, ist es eine Ressource. Wenn es nur einmal passiert und keinen eigenen Zustand hat, ist es eine Aktion.

Aktionen als Ressourcen modellieren

Manchmal lassen sich Aktionen als Ressourcen darstellen. Das macht die API konsistenter und ermöglicht Statusabfragen.

Aktion Als Ressource
E-Mail senden POST /emails → erstellt E-Mail-Ressource
Export starten POST /exports → erstellt Export-Job
Zahlung durchführen POST /payments → erstellt Payment
Order stornieren POST /orders/{id}/cancellations → erstellt Cancellation

Anti-Pattern: Verben in URLs ohne Ressourcenbezug: POST /sendEmail, POST /doExport. Das ist RPC, nicht REST. Wenn du RPC willst, mach es konsistent – aber misch nicht. Klar benannte Sammel-Endpunkte wie /users/batch sind ok, wenn sie dokumentiert und konsistent genutzt werden.

Granularität finden

Nicht jede Datenbanktabelle wird zur Ressource, und nicht jedes Domänenobjekt braucht einen eigenen Endpoint.

Zu fein: Jedes Attribut als eigene Ressource (/users/{id}/email, /users/{id}/name). Führt zu vielen Roundtrips.

Zu grob: Alles in einer Mega-Ressource. Führt zu Over-fetching und komplexen Updates.

Richtig: Ressourcen orientieren sich an Domänengrenzen und Zugriffsmustern. Eine Order enthält line_items inline, aber referenziert den Kunden nur per customer_id.

Identifikatoren richtig wählen

IDs sind der Schlüssel zu deinen Ressourcen. Sie müssen stabil, sicher und praktikabel sein.

Eigenschaften guter IDs

Eigenschaft Warum wichtig
Stabil ID ändert sich nie, auch wenn sich die Ressource ändert
Global eindeutig Keine Kollisionen, auch über Systeme hinweg
Nicht erratbar Verhindert Enumeration-Angriffe
URL-sicher Keine Sonderzeichen, keine Encoding-Probleme
Kurz genug Lesbar in Logs, kopierbar in Slack

Hinweis: Nicht erratbare IDs ersetzen keine Authorization. Jede ID muss serverseitig geprüft werden.

Hinweis: Global eindeutige IDs sind besonders für öffentliche APIs empfehlenswert. Intern kann Eindeutigkeit pro Tenant genügen, wenn Scoping sauber erzwungen wird.

ID-Formate im Vergleich

Format Beispiel Pro Contra
Sequentiell 123 Einfach, kurz Erratbar, verrät Größe
UUID v4 550e8400-e29b-41d4-a716-446655440000 Global eindeutig Lang, unleserlich
ULID 01ARZ3NDEKTSV4RRFFQ69G5FAV Sortierbar, kürzer als UUID Weniger verbreitet
Prefixed ID user_abc123, order_xyz789 Lesbar, selbstdokumentierend Etwas länger
Hashid jR Kurz, obfuskiert Kein Zugriffsschutz, zusätzl. Betrieb

Empfehlung: Prefixed IDs (user_, order_, inv_) kombinieren Lesbarkeit mit klarer Typisierung. Sicherheit kommt vom zufälligen ID-Format, nicht vom Prefix. Stripe, Twilio und andere nutzen dieses Pattern erfolgreich.

Hinweis: Hashids sind Obfuskation, kein Sicherheitsmechanismus. Wenn du sie nicht zwingend brauchst, vermeide sie.

Anti-Pattern: Sequentielle IDs in öffentlichen APIs. Ein Angreifer kann alle Ressourcen durchiterieren.

Zusammengesetzte IDs vermeiden

Manchmal scheint es praktisch, IDs aus mehreren Werten zu bilden: /tenants/{tenant_id}/users/{user_id}. Das funktioniert, aber:

  • Die ID ist nicht global eindeutig (User 123 existiert in jedem Tenant)
  • Clients müssen immer beide Werte kennen
  • Refactoring wird schwierig

Besser: Jede Ressource hat eine eigene, global eindeutige ID. Der Tenant-Kontext kommt aus dem Auth-Token. Wenn du zusätzlich tenant_id im Header oder Query erlaubst (z.B. für Support-Tools), muss der Wert serverseitig gegen das Token validiert werden.

Hierarchien und Nesting

Wann Nesting sinnvoll ist

Nesting drückt Zugehörigkeit aus: /orders/{order_id}/items/{item_id} sagt, dass Items zu Orders gehören.

Nesting ist sinnvoll, wenn:

  • Die Kind-Ressource ohne Eltern keinen Sinn ergibt
  • Der Zugriff fast immer über den Eltern-Kontext erfolgt
  • Die Kind-Ressource keine eigene, unabhängige Existenz hat

Nesting ist problematisch, wenn:

  • Die Kind-Ressource auch unabhängig existiert
  • Clients oft direkt auf die Kind-Ressource zugreifen wollen
  • Die Hierarchie tiefer als zwei Ebenen geht

Maximale Tiefe

Regel: Maximal zwei Ebenen Nesting. Alles darüber wird unhandlich.

Gut:   /orders/{id}/items
Gut:   /users/{id}/addresses
Schlecht: /tenants/{id}/users/{id}/orders/{id}/items/{id}/discounts

Bei tiefen Hierarchien: Mach die Kind-Ressource zur Top-Level-Ressource mit Referenz.

Statt:  /orders/{order_id}/items/{item_id}
Besser: /order-items/{item_id}  (mit order_id im Body/Response)

Flache Hierarchien mit Filtern

Oft ist eine flache Struktur mit Filtern flexibler:

Statt:  GET /users/{user_id}/orders
Besser: GET /orders?user_id={user_id}

Vorteile:

  • Orders sind auch ohne User-Kontext abrufbar
  • Kombinierte Filter möglich (?user_id=...&status=pending)
  • Einfachere URL-Struktur

Wichtig: Filter ersetzen keine Authorization. Scoping muss serverseitig erzwingbar sein (z.B. user_id aus dem Token, nicht aus dem Request).

Referenzen vs. Embedding

Wenn Ressourcen aufeinander verweisen, hast du zwei Optionen: Nur die ID zurückgeben (Referenz) oder das vollständige Objekt einbetten (Embedding).

Referenz

{
  "id": "order_abc123",
  "customer_id": "user_xyz789",
  "items": [
    "item_001",
    "item_002"
  ]
}

Pro: Kleine Payloads, keine Redundanz, klare Grenzen. Contra: Clients brauchen zusätzliche Requests.

Embedding

{
  "id": "order_abc123",
  "customer": {
    "id": "user_xyz789",
    "name": "Max Mustermann",
    "email": "max@example.com"
  },
  "items": [
    {
      "id": "item_001",
      "name": "Widget",
      "quantity": 2
    }
  ]
}

Pro: Alles in einem Request, weniger Roundtrips. Contra: Größere Payloads, Daten können veraltet sein.

Entscheidungshilfe

Situation Empfehlung
Daten werden fast immer zusammen gebraucht Embedding
Daten ändern sich unabhängig Referenz
Tiefe Verschachtelung möglich Referenz
Performance kritisch (mobile Clients) Embedding mit Expansion

Expansion (Best of Both)

Lass Clients entscheiden, was sie brauchen:

GET /orders/abc123                    → Referenzen
GET /orders/abc123?expand=customer    → Customer eingebettet
GET /orders/abc123?expand=customer,items → Beides eingebettet

Regel: Expansion ist additiv. Die Basis-Response enthält nur IDs, Expansion fügt Details hinzu.

Guardrails: Nur Allowlist von expandierbaren Feldern, klare Depth-Limits, Response-Größen begrenzen. Expansion darf nie zusätzliche Berechtigungen umgehen.

Batch- und Bulk-Operationen

Manchmal müssen Clients viele Ressourcen auf einmal verarbeiten. Einzelanfragen werden dann zum Bottleneck.

Batch Read

Mehrere Ressourcen in einem Request abrufen:

GET /users?ids=user_001,user_002,user_003

Oder:

POST /users/batch
{ "ids": ["user_001", "user_002", "user_003"] }

Regel: Limitiere die Anzahl (z.B. max 100 IDs pro Request). Bei langen Listen lieber POST mit Body statt GET, um URL-Limits zu vermeiden.

Security: Jede ID wird autorisiert; Batch ist keine Abkürzung für AuthZ.

Bulk Create

Mehrere Ressourcen auf einmal erstellen:

POST /orders/bulk
{
  "orders": [
    { "customer_id": "user_001", "items": [] },
    { "customer_id": "user_002", "items": [] }
  ]
}

Wichtig: Definiere das Verhalten bei Teilfehlern:

  • All-or-nothing: Alles erfolgreich oder alles rollback
  • Best-effort: Erfolgreiche durchlassen, Fehler einzeln melden
{
  "succeeded": [
    "order_001",
    "order_002"
  ],
  "failed": [
    {
      "index": 2,
      "error": {
        "code": "VALIDATION_ERROR",
        "message": "..."
      }
    }
  ]
}

Bulk Update / Delete

Ähnlich wie Bulk Create, aber für Updates oder Löschungen:

PATCH /orders/bulk
{
  "updates": [
    { "id": "order_001", "status": "shipped" },
    { "id": "order_002", "status": "shipped" }
  ]
}

Anti-Pattern: Bulk-Operationen ohne Limits. Das lädt zu Missbrauch ein und kann das System überlasten.

Regeln & Anti-Patterns

Do

  • Modelliere Ressourcen als Substantive mit eigenem Lebenszyklus
  • Verwende global eindeutige, nicht erratbare IDs
  • Begrenze Nesting auf maximal zwei Ebenen
  • Biete Expansion für flexible Datenabfrage an
  • Definiere klare Semantik für Bulk-Operationen bei Teilfehlern

Don't

  • Verben in URLs verwenden (/createUser, /sendEmail)
  • Sequentielle IDs in öffentlichen APIs
  • Tiefe Hierarchien, die Refactoring verhindern
  • Embedding ohne Opt-out (immer riesige Payloads)
  • Bulk-Operationen ohne Limits und Fehlerhandling

Artefakt: Ressourcenkatalog + URL-Map

Dokumentiere deine Ressourcen und ihre Beziehungen:

# Ressourcenkatalog

## Kern-Ressourcen

| Ressource | ID-Format      | Beschreibung                   |
|-----------|----------------|--------------------------------|
| User      | `user_{ulid}`  | Registrierter Benutzer         |
| Order     | `order_{ulid}` | Bestellung eines Users         |
| OrderItem | `item_{ulid}`  | Position innerhalb einer Order |
| Product   | `prod_{ulid}`  | Artikel im Katalog             |
| Payment   | `pay_{ulid}`   | Zahlungsvorgang                |

## Beziehungen

| Von       | Zu        | Typ       | Anmerkung                 |
|-----------|-----------|-----------|---------------------------|
| Order     | User      | Referenz  | `customer_id` im Order    |
| Order     | OrderItem | Embedding | Items sind Teil der Order |
| OrderItem | Product   | Referenz  | `product_id` im Item      |
| Payment   | Order     | Referenz  | `order_id` im Payment     |

## URL-Map

| Methode | URL                                  | Beschreibung                              |
|---------|--------------------------------------|-------------------------------------------|
| GET     | `/users`                             | Liste aller User (paginiert)              |
| POST    | `/users`                             | User erstellen                            |
| GET     | `/users/{id}`                        | Einzelnen User abrufen                    |
| PATCH   | `/users/{id}`                        | User aktualisieren                        |
| DELETE  | `/users/{id}`                        | User löschen                              |
| GET     | `/orders`                            | Liste aller Orders (paginiert, filterbar) |
| POST    | `/orders`                            | Order erstellen                           |
| GET     | `/orders/{id}`                       | Einzelne Order abrufen                    |
| GET     | `/orders/{id}?expand=items,customer` | Order mit Details                         |
| POST    | `/orders/{id}/cancellations`         | Order stornieren                          |
| GET     | `/products`                          | Produktkatalog (paginiert)                |
| POST    | `/users/batch`                       | Mehrere User abrufen                      |
| POST    | `/orders/bulk`                       | Mehrere Orders erstellen                  |

Checkliste

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

  • [ ] Kern-Ressourcen sind identifiziert und benannt
  • [ ] ID-Format ist gewählt (Empfehlung: Prefixed IDs)
  • [ ] Hierarchien sind auf maximal zwei Ebenen begrenzt
  • [ ] Entscheidung Referenz vs. Embedding ist dokumentiert
  • [ ] Expansion-Mechanismus ist definiert (falls nötig)
  • [ ] Bulk-Operationen sind spezifiziert (Limits, Fehlerhandling)
  • [ ] Ressourcenkatalog und URL-Map sind ausgefüllt

Wie es weitergeht

Im nächsten Teil geht es um HTTP-Semantik und Statuscodes: Wann POST vs. PUT vs. PATCH? Wie gehst du mit Async-Operationen, Konflikten und Rate Limits um?

Alle Teile der Serie: Serie: API-Design

Mehr Beiträge aus dem Blog.