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:
- Was sind die Kern-Ressourcen? Welche Substantive repräsentieren deine Domäne?
- Wie stabil müssen IDs sein? Können sie sich ändern? Sind sie öffentlich?
- Wie tief darf Nesting gehen?
/users/{id}/orders/{id}/items/{id}– zu viel? - Referenz oder Embedding? Wann liefern wir IDs, wann vollständige Objekte?
- 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