wann /orders/1/items wirklich die richtige URL ist
Verschachtelte Ressourcen wie Bestellpositionen unter einer Bestellung wirken auf den ersten Blick intuitiv, führen aber schnell zu unübersichtlichen URL Strukturen und doppelter Autorisierungslogik, wenn sie unüberlegt eingesetzt werden. Dieser Artikel zeigt, wie man Subresources in API Platform sauber über uriTemplate modelliert und wann eine eigenständige Ressource die bessere Wahl ist.
Inhaltsverzeichnis
- 1. Warum verschachtelte Ressourcen kein Selbstläufer sind
- 2. Subresources über uriTemplate definieren
- 3. IRI Referenz gegen eingebettete Relation
- 4. Wie tief sollte Verschachtelung überhaupt gehen
- 5. Autorisierung entlang der Elternressource vererben
- 6. Normalization Groups für unterschiedliche Verschachtelungstiefen
- 7. Wann eine eigenständige Ressource die bessere Wahl ist
- 8. Performance bei verschachtelten Collections
- 9. Subresource gegen eigenständige Ressource im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum verschachtelte Ressourcen kein Selbstläufer sind
Sobald eine Domäne aus mehreren zusammenhängenden Entities besteht, etwa Bestellungen und ihre Positionen, liegt die Idee nahe, diese Beziehung auch in der URL abzubilden: /orders/1/items statt einer flachen Liste unter /order-items mit Filterparameter. Solche verschachtelten Ressourcen, in API Platform als Subresources bezeichnet, kommunizieren die fachliche Beziehung direkt in der URL Struktur und sind für viele Konsumenten intuitiv lesbar.
Der Reiz verschachtelter Ressourcen hat aber Grenzen. Jede zusätzliche Verschachtelungsebene erhöht die Komplexität der Routing Konfiguration, der Autorisierungslogik und der URL Struktur insgesamt. Eine URL wie /customers/1/orders/2/items/3/refunds ist zwar technisch möglich, aber weder für Menschen noch für generische API Clients angenehm zu handhaben, und häufig ein Zeichen dafür, dass die Ressourcenmodellierung neu überdacht werden sollte.
API Platform bietet mit dem uriTemplate Argument ein flexibles Werkzeug für verschachtelte Ressourcen, das aber bewusst und nicht reflexartig für jede Eltern Kind Beziehung eingesetzt werden sollte. Die Entscheidung, ob eine Beziehung als Subresource, als eingebettete Relation im Response Body oder als komplett eigenständige Ressource abgebildet wird, ist eine der wichtigsten Design Entscheidungen beim Aufbau einer API.
2. Subresources über uriTemplate definieren
API Platform bildet Subresources nicht über ein eigenes Attribut ab, sondern über das reguläre uriTemplate Argument einer GetCollection Operation, kombiniert mit einem Platzhalter für die ID der Elternressource. Intern übersetzt API Platform diesen Platzhalter automatisch in einen Doctrine Filter, der nur die Kinder der referenzierten Elternressource liefert, ganz ohne manuell geschriebenen Query Code.
Diese verschachtelten Ressourcen erscheinen zusätzlich zur regulären, flachen Collection Route, sofern beide konfiguriert sind. Ein Client kann dann sowohl /order-items?order=1 als auch /orders/1/items nutzen, wobei Letzteres vor allem für Frontend Anwendungen praktisch ist, die ohnehin bereits im Kontext einer konkreten Bestellung navigieren.
<?php
declare(strict_types=1);
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use App\Entity\Order;
/**
* Order item resource, exposed both as a flat collection
* and as a nested subresource under its parent order.
*/
#[ApiResource(
operations: [
new GetCollection(),
new GetCollection(
uriTemplate: '/orders/{orderId}/items',
uriVariables: [
'orderId' => new \ApiPlatform\Metadata\Link(
fromClass: Order::class,
toProperty: 'order',
),
],
),
new Get(),
],
)]
final class OrderItem
{
public int $id;
public string $productName;
public int $quantity;
}
3. IRI Referenz gegen eingebettete Relation
Unabhängig von Subresources muss jede Relation zwischen zwei Ressourcen entscheiden, wie sie im JSON Response dargestellt wird. Der Standardfall in API Platform ist die IRI Referenz, eine URL als String, über die der Client bei Bedarf die verlinkte Ressource separat nachladen kann. Das hält jede einzelne Antwort klein und performant, erfordert aber einen zusätzlichen Request, wenn der Client die verlinkten Daten sofort braucht.
Über Serialization Groups lässt sich eine Relation stattdessen vollständig einbetten, sodass die verschachtelten Daten direkt im ersten Response mitgeliefert werden. Diese eingebetteten Relationen sparen zusätzliche Requests, vergrößern aber jede Antwort und können bei tief verschachtelten Objektgraphen schnell zu unübersichtlichen, schwer cachebaren JSON Strukturen führen, wenn sie unkontrolliert für jede Relation aktiviert werden.
<?php
declare(strict_types=1);
namespace App\ApiResource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use Symfony\Component\Serializer\Attribute\Groups;
/**
* Order resource with items embedded directly in the response,
* avoiding an extra request for the client.
*/
#[ApiResource(
operations: [
new GetCollection(normalizationContext: ['groups' => ['order:read']]),
],
)]
final class Order
{
#[Groups(['order:read'])]
public int $id;
// Embedded relation: full item data instead of just an IRI
#[Groups(['order:read'])]
public array $items = [];
}
4. Wie tief sollte Verschachtelung überhaupt gehen
Eine praktische Faustregel: Verschachtelte Ressourcen über mehr als eine Ebene sind selten die richtige Wahl. Statt /customers/1/orders/2/items anzubieten, bewährt sich meist ein flacherer Zugriff, etwa /orders/2/items in Kombination mit einem Filter Parameter auf der Kundenebene, falls dieser überhaupt gebraucht wird. Jede zusätzliche Ebene in der URL bedeutet eine zusätzliche Link Definition, einen zusätzlichen Doctrine Join und eine zusätzliche potenzielle Fehlerquelle bei nicht existierenden Zwischenressourcen.
Ein zweiter Grund gegen tiefe Verschachtelung ist die Bookmark und Cache Fähigkeit von URLs. Eine URL wie /orders/2/items/5 lässt sich bookmarken und in einer zweiten Anwendung direkt referenzieren, ohne den vollständigen Elternpfad kennen zu müssen. Sobald mehrere Elternebenen in die URL codiert sind, wird jede Referenz auf eine tiefer liegende Ressource unnötig lang und an die Existenz aller Zwischenressourcen gekoppelt.
5. Autorisierung entlang der Elternressource vererben
Ein oft übersehener Aspekt bei verschachtelten Ressourcen ist die Autorisierung. Wenn /orders/1/items abgefragt wird, muss geprüft werden, ob der aktuelle Nutzer überhaupt Zugriff auf Bestellung 1 hat, bevor die zugehörigen Positionen zurückgegeben werden. Ohne diese Prüfung könnte ein Nutzer über die Subresource Route Daten einsehen, die über die reguläre Route korrekt geschützt wären, ein klassisches Beispiel für eine Broken Object Level Authorization Lücke.
In API Platform lässt sich das über das security Argument der Subresource Operation lösen, das zusätzlich zur Prüfung auf der Kind Ressource auch die Elternressource über die injizierte URI Variable referenzieren kann. Der Security Ausdruck prüft dann explizit, ob der Nutzer Zugriff auf die referenzierte Elternbestellung hat, und lehnt den Zugriff auf die Subresource andernfalls konsequent ab.
6. Normalization Groups für unterschiedliche Verschachtelungstiefen
Je nachdem, ob eine Ressource als Top Level Endpunkt oder als eingebettete Relation einer anderen Ressource ausgegeben wird, sind oft unterschiedliche Felder relevant. Ein Kunde direkt über /customers/1 abgerufen braucht vollständige Kontaktdaten, während dieselbe Kundenreferenz eingebettet in einer Bestellung meist nur Name und ID benötigt. Normalization Groups lösen dieses Problem, indem verschiedene Serialisierungskontexte für denselben Entity Typ definiert werden.
Diese Kontext abhängige Serialisierung verhindert, dass jede eingebettete Relation automatisch alle Felder der Zielressource mitliefert, was sowohl die Antwortgröße reduziert als auch versehentliches Daten Leaking vermeidet, etwa wenn ein sensibles Feld nur im Top Level Kontext, nicht aber in eingebetteten Relationen erscheinen soll.
7. Wann eine eigenständige Ressource die bessere Wahl ist
Nicht jede Beziehung rechtfertigt eine Subresource. Sobald ein verschachteltes Objekt eigene Filter, eigene Paginierung oder eigene Schreiboperationen jenseits des Elternkontexts braucht, spricht das für eine eigenständige, flache Ressource statt einer echten Subresource. Ein Beispiel: Bestellpositionen, die auch unabhängig von einer konkreten Bestellung durchsucht werden müssen, etwa für eine unternehmensweite Umsatzauswertung nach Produkt, sollten primär als eigenständige /order-items Ressource mit optionalem Filter existieren, die verschachtelte Route bleibt dann ein bequemer Zusatzpfad.
Ein weiteres Signal für eine eigenständige Ressource: Wenn ein verschachteltes Objekt auch ohne seinen aktuellen Elternkontext einen sinnvollen, eigenständigen Lebenszyklus hat, etwa eine Rechnung, die auch nach Löschung der ursprünglichen Bestellung archiviert bleiben muss. Solche Objekte gehören konzeptionell nicht dauerhaft unter die Elternressource, auch wenn sie zum Zeitpunkt der Erstellung aus ihr hervorgehen.
8. Performance bei verschachtelten Collections
Verschachtelte Collections können bei unbedachter Implementierung zu denselben N Plus 1 Problemen führen wie normale Relationen: Wird für jede Bestellung in einer äußeren Liste separat die Subresource der Positionen abgefragt, entstehen unnötig viele Datenbankabfragen. In der Praxis betrifft das vor allem Frontend Anwendungen, die zunächst die Bestellliste laden und danach für jede sichtbare Bestellung eine zusätzliche Subresource Anfrage auslösen, statt die Positionen direkt eingebettet mitzuliefern.
Die Lösung liegt in einer bewussten Entscheidung zwischen eingebetteten Relationen für den Listen Anwendungsfall und separaten Subresource Requests nur dann, wenn tatsächlich Detaildaten einer einzelnen Bestellung benötigt werden. Diese Entscheidung sollte anhand des tatsächlichen Nutzungsmusters des Frontends getroffen werden, nicht anhand einer pauschalen Regel für alle Relationen im Projekt.
9. Subresource gegen eigenständige Ressource im Vergleich
Die folgende Tabelle fasst zusammen, wann eine Subresource sinnvoll ist und wann eine eigenständige, flache Ressource vorzuziehen ist.
| Kriterium | Subresource | Eigenständige Ressource | Empfehlung |
|---|---|---|---|
| Existiert nur im Elternkontext | Passend | Unnötiger Umweg | Subresource für strikt abhängige Kinder |
| Eigene Filter und Paginierung nötig | Eingeschränkt möglich | Voll flexibel | Eigenständige Ressource bei komplexer Suche |
| Bookmark und direkte Referenz | Langer, gekoppelter Pfad | Kurze, stabile URL | Flache Ressource für Direktzugriff |
| Autorisierung | Muss Elternressource explizit prüfen | Eigener, klar abgegrenzter Voter | Subresource Security sorgfältig konfigurieren |
| Lebenszyklus unabhängig vom Elternteil | Konzeptionell unpassend | Korrekte Abbildung | Eigenständige Ressource bei eigenem Lebenszyklus |
In der Praxis kombinieren gut modellierte API Platform Projekte beide Ansätze bewusst: echte Subresources für Objekte, die strikt an ihren Elternkontext gebunden sind, und eigenständige, flache Ressourcen überall dort, wo eigene Filter, eigener Lebenszyklus oder direkte Referenzierbarkeit gefragt sind. Diese Entscheidung pro Relation zu treffen, statt eine pauschale Regel für das gesamte Projekt zu verwenden, ist der entscheidende Unterschied zwischen einer wartbaren und einer verschachtelt zugewucherten API Struktur.
Mironsoft
Symfony und API Platform Architektur für anspruchsvolle APIs
Eine saubere Ressourcenstruktur für euer API Platform Projekt?
Wir modellieren Subresources und Relationen so, dass URLs stabil und referenzierbar bleiben, Autorisierung entlang der Elternressource korrekt greift und N Plus 1 Probleme bei verschachtelten Collections gar nicht erst entstehen.
Ressourcen Design
Subresources und eigenständige Ressourcen fachlich sauber trennen
Security Audit
Prüfung auf Broken Object Level Authorization in verschachtelten Routen
Performance Review
N Plus 1 Analyse bei verschachtelten Collections und Relationen
10. Zusammenfassung
Verschachtelte Ressourcen über uriTemplate sind in API Platform ein mächtiges Werkzeug für Objekte, die strikt an ihren Elternkontext gebunden sind, sollten aber nicht reflexartig für jede Beziehung eingesetzt werden. Mehr als eine Verschachtelungsebene, fehlende Autorisierungsprüfung entlang der Elternressource und unkontrolliert eingebettete Relationen sind die häufigsten Fehler bei der praktischen Umsetzung.
Wer bei jeder Relation bewusst zwischen Subresource, eingebetteter Relation und eigenständiger Ressource entscheidet, statt eine einzige Regel für das gesamte Projekt zu verwenden, bekommt eine API Struktur, die sowohl für Menschen lesbar als auch technisch wartbar bleibt. Diese Entscheidung früh im Projekt zu treffen, erspart spätere, schwer rückgängig zu machende Umbauten an der URL Struktur.
Verschachtelte Ressourcen in API Platform: Das Wichtigste auf einen Blick
uriTemplate Subresources
Verschachtelte Collection Routen über einen Link auf die Elternressource, ohne manuellen Query Code.
IRI vs eingebettet
Standardmäßig IRI Referenz für kleine Antworten, eingebettete Relation nur gezielt über Serialization Groups.
Autorisierung
Security Ausdruck der Subresource muss die Elternressource explizit mit prüfen, sonst droht BOLA.
Wann eigenständig
Eigene Filter, eigener Lebenszyklus oder Bookmark Fähigkeit sprechen für eine flache, eigenständige Ressource.