1. Pfad als Teil des Vertrags
Ein Pfad in der API ist Teil des öffentlichen Vertrags — kein unwichtiges technisches Detail. Wenn man bisher Controller „für sich selbst" geschrieben hat, gewöhnt man sich leicht daran, ihn als etwas zu betrachten, das man jederzeit ändern kann. In einer echten API ist der Pfad eine öffentliche Adresse — wie eine Lieferadresse. Man kann die Möbel in der Wohnung umstellen (Code-Refactoring), aber wenn man plötzlich die Adresse des Hauses ändert (URI), kommt der Kurier (Client) auf einem Feld an, ist traurig und ruft den Support an.
Eine URI (ein Pfad) ist der Teil des Vertrags, den der Client sich merkt. Selbst wenn der Client du selbst bist — nur als Frontend-Entwickler „in der Zukunft" —, der in einem Monat den Code öffnet und sagt: „Warum steht hier /api/v1/getTasks, und da /api/v1/tasks/list… wer hat das gemacht?" Eine API hat keine Mimik und keine Betonung: Sie kann dem Client nicht „erklären", was du gemeint hast. Der Client liest den Vertrag wörtlich: URI gesehen — Anfrage gestellt — vorhersehbares Verhalten erwartet.
Und hier ist ein wichtiger Gedanke für den gesamten Kurs: Eine gute URI übersteht Änderungen der internen Implementierung. Du kannst Klassen umbenennen, Services auf Pakete verteilen, das In-Memory-Repository durch eine andere Speicherschicht ersetzen — aber /api/v1/tasks soll /api/v1/tasks bleiben, wenn sich der Sinn der Ressource nicht verändert hat. Deshalb entwirft man URIs „von außen nach innen": aus der Perspektive des API-Konsumenten, nicht nach dem Prinzip „wie heißt unsere Methode im Controller".
2. Anatomie einer URI: Segmente und Vorlagen
Bevor man darüber streitet, ob es „tasks" oder „task-items" sein soll, ist es nützlich, sich auf grundlegende Begriffe zu einigen. Eine URI in HTTP besteht normalerweise aus einem Pfad und möglicherweise einem Query-String, zum Beispiel /api/v1/tasks?status=TODO. Heute konzentrieren wir uns auf Pfade: Das ist der Teil der Adresse, der durch / in Segmente unterteilt ist und zeigt, auf welche Ressource wir zugreifen. Query-Parameter lassen wir vorerst beiseite: Für das Gespräch über den Pfad brauchen wir den Adressbaum der Ressource, nicht die Abfrageeinstellungen wie status=TODO.
Am einfachsten denkt man sich einen Pfad als Kette von Segmenten, wobei jedes davon wie ein Wort in einem kleinen Satz lesbar sein sollte. Wenn Segmente eine zufällige Sammlung von Abkürzungen und Verben sind, muss der Client raten. Wenn es Substantive und stabile Bezeichner sind, liest sich die API fast wie eine Karte.
Kleiner Spickzettel zu den Begriffen (ohne Langeweile, aber mit Nutzen):
| Begriff | Was das ist | Beispiel |
|---|---|---|
| URI | „Adresse" der Ressource (im Kontext der HTTP-API spricht man meistens von Pfad + Query) | /api/v1/tasks/123 |
| Pfadsegment | Teil einer URI zwischen / | api, v1, tasks, 123 |
| Collection-URI | Pfad zu einer Menge gleichartiger Ressourcen | /api/v1/tasks |
| Ressourcen-URI | Pfad zu einer einzelnen Ressource per ID | /api/v1/tasks/{taskId} |
| URI-Vorlage | Pfadnotation mit Platzhalter (praktisch in Dokumentation und Code) | /api/v1/tasks/{taskId} |
Um den Pfad visuell in Segmente zu zerlegen, hilft manchmal ein ganz einfaches Diagramm. Ja, ein Diagramm in einer Lektion über URIs ist bereits ein leichtes Ingenieursdrama — aber besser das als später Bugs in einem nicht existierenden Endpoint zu suchen:
flowchart LR
A["/api/v1/tasks/7b9b2d5a-2a32-4b79-9a9c-2a9c2d0b9d1f"]
A --> B["api"]
B --> C["v1"]
C --> D["tasks"]
D --> E["7b9b... (taskId)"]
Beachte eine wichtige Nuance: In einer echten Anfrage sendest du niemals geschweifte Klammern. {taskId} ist nur ein praktischer „Platzhalter" für die Dokumentation und die Routenbeschreibung im Code. In einer echten Anfrage steht dort ein konkreter Wert, zum Beispiel eine UUID.
3. Collection und einzelne Ressource
Der Großteil einer API wird aus zwei Bausteinen aufgebaut: „Collection" und „eine Ressource". Wenn ein Anfänger „eine API entwerfen" hört, malt er sich oft dutzende Pfade im Kopf aus. In der Praxis entsteht die URI der einzelnen Ressource fast automatisch, wenn man die URI der Collection sorgfältig gewählt hat: Man muss nur einen Bezeichner hinzufügen. Das ist eine großartige Nachricht — Konsistenz ist hier fast gratis zu haben.
In der Task Tracker API ist das grundlegende Beispiel die Aufgaben. Die Aufgaben-Collection ist eine Liste, mit der der Client als Menge von Elementen arbeitet. Eine einzelne Aufgaben-Ressource ist eine konkrete Aufgabe per ID. Bereits auf dieser Ebene kann man die API vorhersehbar machen: Ein Client, der /tasks sieht, erwartet fast ohne Hinweise, dass /tasks/{taskId} eine einzelne konkrete Aufgabe liefert.
Zur Festigung — eine kleine Tabelle. Wir verwenden das vollständige Präfix /api/v1, aber jetzt geht es nicht um die Version, sondern um die Logik der Adresse selbst: Der Pfad beginnt mit dem Ressourcennamen.
| Was wird adressiert | URI | Wie das ein Mensch liest |
|---|---|---|
| Aufgaben-Collection | /api/v1/tasks | „Alle Aufgaben" (genauer: Menge der Aufgaben) |
| Eine Aufgabe | /api/v1/tasks/{taskId} | „Die Aufgabe mit dem Bezeichner taskId" |
Diese Unterscheidung reicht vorerst: Die Collection lebt unter /api/v1/tasks, und die konkrete Ressource unter /api/v1/tasks/{taskId}. Auf Code-Ebene hält man solche Strings am besten zentralisiert, aber die Formel selbst ist einfach: Erst der Name der Collection, dann der Ressourcenbezeichner.
4. Namen von Collections: Plural
Collections benennt man besser vorhersehbar als originell. Auf diesem Schritt ist es leicht, kreativ zu werden: Aufgaben irgendwie ungewöhnlich nennen, zum Beispiel /issues, /todoItems, /mySuperTasks. Und Kreativität ist wunderbar… bis genau zu dem Moment, wo man sich daran erinnert, dass eine API einen Konsumenten hat. Der Konsument braucht keine Inspiration — er braucht Vorhersehbarkeit. Deshalb wählt man im API-Design normalerweise das Langweilige, aber Stabile: verständliche Substantive und einen einheitlichen Benennungsstil.
Die praktische Regel für Collections in REST-APIs: Collections benennt man im Plural. Das ist kein Naturgesetz, sondern eine Konvention, die die kognitive Last verringert. /tasks wirkt wie eine Liste. /task wirkt wie „eine Aufgabe", und dann beginnt die Verwirrung: „Warum gibt /task ein Array zurück?" Nichts Tragisches passiert, aber die API wird weniger lesbar — das ist wie ein kleiner Riss im Fundament: Heute sieht man ihn nicht, aber in einem Monat lebt man in einem Haus aus Rissen.
In unserem Projekt ist das Ressourcenvokabular bereits von der Fachdomäne vorgegeben: tasks, comments, attachments, tags. Wichtig ist dabei nicht nur „Wie nennen wir es", sondern auch denselben Namen überall zu halten. Wenn man eine Ressource heute tasks nennt, soll man morgen nicht daneben /todo anlegen, „weil das kürzer ist". Kurz ist es die ersten 10 Minuten, und danach hat man zwei parallele Welten, die dasselbe tun.
Gut und schlecht lässt sich manchmal am besten direkt gegenüberstellen:
| Sinn | Schlecht | Gut | Warum |
|---|---|---|---|
| Aufgaben-Collection | /api/v1/task | /api/v1/tasks | Plural deutet sofort auf eine Liste hin |
| „Aufgaben abrufen" | /api/v1/getTasks | /api/v1/tasks | Die Aktion wird durch die HTTP-Methode ausgedrückt, nicht durch den Pfad |
| „Aufgabenliste" | /api/v1/taskList | /api/v1/tasks | „list" muss nicht dupliziert werden — eine Collection ist schon eine Liste |
Eine separate Nuance, die oft vergessen wird: Eine URI ist kein Platz für interne Begriffe deiner Codebasis. Dem Client ist es egal, ob deine Java-Klasse TaskAggregateRoot oder TaskEntity heißt. In der URI spricht man die Sprache der Fachdomäne — aber in „menschlicher" Form. Wenn die Fachdomäne Aufgaben sind, heißt die Ressource tasks und nicht task-aggregates.
5. Bezeichner im Pfad: {taskId}
Der Bezeichner im Pfad gibt die Adresse einer konkreten Ressource an. Der Moment mit der ID scheint einfach: Na ja, ID ist ID, was gibt es da zu besprechen? Aber genau hier fangen viele APIs an, sich zu verwirren. Eine Ressourcen-URI ist die Adresse einer konkreten Entität — also muss der Bezeichner im Pfad stehen. Wenn man eine Aufgabe adressiert, geht man logischerweise zu /tasks/{taskId}. Das ähnelt einer Wohnung im Haus: Das Haus ist eine „Collection von Wohnungen", und die Wohnungsnummer ist die konkrete Ressource darin.
In der Task Tracker API sind Ressourcenbezeichner als UUID-Strings definiert. Das ist sowohl für Lernzwecke praktisch als auch realistisch genug: Eine UUID enthüllt keinen Business-Sinn, lässt sich nicht „durch Raten erraten", und sie bleibt stabil, selbst wenn sich später die Art der Datenspeicherung ändert. Auf URI-Ebene gilt: Der Bezeichner sollte für den Client undurchsichtig und stabil sein. Der Client muss nicht wissen, dass „42" „der zweiundvierzigste Datensatz in der Datenbank" ist.
Noch ein nützlicher Vertrag: In Pfadvorlagen verwenden wir sprechende Platzhalternamen. Ja, man kann {id} schreiben, aber sobald Verschachtelung auftaucht (und in unserem Projekt werden taskId, commentId, attachmentId vorkommen), wird man sich für die expliziten Namen bedanken. Sie sind etwas länger, aber die Frage „ID von was genau?" verschwindet.
Vergleiche „Vorlage" und „echte URI":
- Vorlage in der Dokumentation oder Routenbeschreibung: /api/v1/tasks/{taskId}
- Echte Anfrage: /api/v1/tasks/7b9b2d5a-2a32-4b79-9a9c-2a9c2d0b9d1f
Hier ist es wichtig, die Form der Adresse buchstäblich zu sehen: /api/v1/tasks/{taskId} ist eine Vorlage, und /api/v1/tasks/7b9b... ist eine echte URI. Uns interessiert jetzt nicht der Typ von taskId, sondern die Tatsache, dass die Adresse einer Ressource aus der Adresse der Collection plus dem Bezeichner entsteht.
6. Verben im Pfad und HTTP-Methoden
Verben im Pfad sind fast immer überflüssig: In HTTP gibt es für Aktionen bereits die Anfragemethode. Wenn man früher APIs „als Funktionssammlung" gebaut hat, greift die Hand automatisch zu Pfaden wie /getTasks, /createTask, /deleteTask. Das klingt logisch — bis man sich erinnert: In HTTP gibt es bereits ein Verb — die Anfragemethode. Wenn man auch im Pfad ein Verb schreibt, entsteht eine doppelte Codierung des Sinns. Und doppelte Codierung ist der beste Freund von Widersprüchen: Heute hat man GET /createTask, morgen POST /getTasks, und man lebt in einer Welt, wo URI und Methode miteinander in Konflikt stehen.
Im REST-Ansatz beantwortet der Pfad die Frage „Welche Ressource?", und die HTTP-Methode beantwortet die Frage „Was tun?". Wenn wir Aufgaben brauchen, gehen wir zu /tasks. Wenn wir eine Aufgabe erstellen wollen — arbeiten wir immer noch mit der Aufgaben-Ressource, nur die Methode ist eine andere. Das ist einer der Gründe, warum eine REST-API wie ein System lesbar ist und nicht wie ein „Befehlskatalog".
Eine sehr kurze Illustration, fast schon ein Meme:
// Beispiel: Dieselbe Aktion ("Liste abrufen") darf nicht als Verb in der URI kodiert werden.
String bad = "/api/v1/getTasks";
String good = "/api/v1/tasks";
// Der Sinn der Operation wird durch die HTTP-Methode ausgedrückt, nicht durch ein Verb im Pfad.
Und wenn man das als Anfrage-Strings aufschreibt (nur als Vertragsillustration, ohne Response-Details), wird der Unterschied noch klarer:
- Schlecht: GET /api/v1/getTasks
- Gut: GET /api/v1/tasks
Die schlechte Variante sagt „Was tun", zeigt aber kein Ressourcenmodell. Die gute Variante zeigt das Ressourcenmodell, und die Aktion ergibt sich aus der HTTP-Methode. Das ist die Reifeprüfung: Wenn man /api/v1/getTasks sieht, denkt man fast sicher im RPC-Stil. Nicht schlimm — das passiert am Anfang allen, wir kurieren das heute einfach bewusst.
7. Stabilität der URI und einheitlicher Stil
Es reicht nicht, einen Pfad nur funktionsfähig zu machen — er muss stabil sein. Einen Pfad „funktionierend" zu machen ist einfach, aber ihn so zu gestalten, dass er in einem halben Jahr genauso logisch aussieht wie am Tag des Schreibens — das ist schwieriger. Stabilität der URI bedeutet nicht „niemals etwas ändern", sondern dass Änderungen selten, bewusst und begründet sind. Wenn man eine URI ändert, weil man „eine Methode im Controller umbenannt hat" — ist das fast immer der falsche Grund.
Einheitlichkeit beginnt mit kleinen Entscheidungen. Zum Beispiel: Segmente in Kleinbuchstaben schreiben (tasks, nicht Tasks), Segmente kurz halten, keine Unterstriche (task_items) ohne zwingenden Bedarf verwenden, keine Dateiendungen (tasks.json), keine Synonyme anlegen. Eine API ist wie ein Wörterbuch: Wenn derselbe Sinn mit verschiedenen Wörtern bezeichnet wird, versteht der Leser (Client) nicht, ob das „dasselbe" oder „etwas anderes" ist.
Es ist nützlich, kleine „Stilregeln" festzuhalten (klingt langweilig, spart aber Stunden):
| Entscheidung | Empfehlung für den Kurs | Warum das hilft |
|---|---|---|
| Groß-/Kleinschreibung | lowercase | Visuell einheitlich, weniger Überraschungen |
| Ressourcennamen | Substantive | Der Pfad beschreibt „Was", nicht „Wie" |
| Collections | Plural | Schnell als Liste lesbar |
| Abschließender Slash | ohne abschließenden Slash | Weniger Duplikate wie /tasks und /tasks/ |
| Synonyme | Innerhalb einer API verboten | Die API-Karte wird nicht zum Rätsel |
Und es gibt noch eine Sache, die man leicht vergisst: Eine URI soll clientorientiert sein. Intern kann man die Architektur nach Belieben umbauen (heute Services, morgen separate Szenarien, übermorgen „ich habe alles auf funktionalen Stil umgeschrieben und bin jetzt glücklich"). Dem Client ist das egal. Ihm ist wichtig, dass /api/v1/tasks immer dasselbe bedeutet — unabhängig von deiner internen Philosophie.
8. Wörterbuch der Ressourcennamen
Damit die API in der Benennung nicht auseinanderdriftet, ist es besser, das Ressourcenwörterbuch vorab festzulegen. Dann wird der nächste Schritt — verschachtelte Ressourcen — nicht zum Streit „Wie nennen wir etwas, das wir noch nicht benannt haben?" Das ist ein wichtiger Trick: Das Projekt erhält eine „offizielle Terminologie", die vor zufälligen Entscheidungen „im Vorbeigehen" schützt. Wir zählen jetzt nicht die gesamte Endpoint-Liste auf und besprechen keine Verschachtelung — wir fixieren genau die Ressourcennamen, also die Schlüsselsegmente des Pfads.
In der Task Tracker API gibt es vier Hauptressourcennamen, die man immer wieder sehen wird: tasks, comments, attachments, tags. Diese Namen wurden nicht deshalb gewählt, „weil man das im Internet so macht", sondern weil sie die Fachdomäne des Projekts direkt widerspiegeln und eindeutig lesbar sind. Wenn man attachments sieht, denkt man nicht „Sind das wirklich Dateien oder eine Liste von Links zu Dokumenten?" — man versteht: Das sind Anhänge, also etwas, das an eine Aufgabe angehängt ist.
Eine kleine „Wörterbuch-Tabelle" festhalten:
| Fachliches Objekt | Ressourcenname (Segment) | Kommentar |
|---|---|---|
| Task | tasks | Hauptressource des Projekts |
| Comment | comments | Hilfsressource neben den Aufgaben |
| Attachment | attachments | Hilfsressource für Dateien und Metadaten |
| Tag | tags | Nachschlageressource (ohne großes CRUD) |
Wichtig ist, dass wir keine alternativen Namen erfinden: kein todo, kein work-items, kein files statt attachments. Eine Entität — ein Name. Das ist die Vertragsdisziplin: Der Client soll das Vokabular einmal lernen und danach anhand der API-Karte navigieren, ohne Überraschungen.
Pfade im Code: eine einzige Quelle der Wahrheit
Auf Code-Ebene „driftet" eine API meistens wegen banaler String-Kopiererei. Man hat "/api/v1/tasks" in einer Datei geschrieben, dann irgendwo manuell "/api/v1/task" (ohne s) eingetippt — und schon gibt es zwei verschiedene Welten. IDE und Compiler schweigen, aber der Client beschwert sich spät und schmerzhaft. Deshalb ist es nützlich, das Basispräfix, Ressourcennamen und Vorlagen wie /tasks/{taskId} an einem Ort zu halten.
Wichtig ist hier nicht die konkrete Hilfsklasse, sondern das Prinzip selbst: Der Pfad wird einmal gewählt und dann überall wiederverwendet. Wenn eine Ressource tasks heißt, darf das Projekt nicht parallel mit task, todo und taskList leben, nur weil es sich so in verschiedenen Dateien ergeben hat.
9. Typische Fehler beim Entwurf von URIs
Fehler in URIs brechen Code selten sofort, kosten aber fast immer später teuer. Der Code kompiliert, die Anwendung startet, und das Problem taucht erst auf, wenn der Client den Endpoint nicht findet, die Dokumentation der Implementierung zu widersprechen beginnt, und das Team Zeit damit verbringt, „Warum 404?" zu suchen. Deshalb ist es nützlich, die typischen Stolperfallen im Voraus zu besprechen und zu lernen, sie wie Rauchgeruch zu erkennen — statt zu winken: „Na ja, Kleinigkeit."
Fehler Nr. 1: Collection im Singular.
Ein Pfad wie /api/v1/task für eine Aufgabenliste erzeugt fast immer Verwirrung: „Ist das eine Aufgabe oder eine Liste?" Plural (/api/v1/tasks) macht die Absicht offensichtlich und spart Erklärungen. Das ist eine kleine Regel, aber sie steigert die Lesbarkeit der API-Karte erheblich.
Fehler Nr. 2: Verben im Pfad (/getTasks, /createTask, /deleteTask).
Eine solche API beginnt, wie ein Befehlskatalog zu leben, nicht wie ein Ressourcenmodell. Die Aktion soll in der HTTP-Methode stehen, nicht im Ressourcennamen. Sonst entsteht doppelter Sinn, der sich mit der Zeit in Widersprüche und zufällige Entscheidungen verwandelt.
Fehler Nr. 3: Mehrere Namen für denselben Sinn.
Heute nennt man die Ressource tasks, morgen legt man todo an, dann erscheint work-items. Für den Client sieht das so aus, als gäbe es drei verschiedene Entitäten, obwohl der Sinn einer ist. Auch intern zerstört das die Disziplin: Es entstehen verschiedene DTOs, verschiedene Controller, verschiedene „fast identische" Szenarien. Besser ein Vokabular halten und keine Synonyme anlegen.
Fehler Nr. 4: URI an interne Klassennamen anpassen.
Eine URI wie /api/v1/taskController/getAll kann genau bis zum ersten Refactoring „logisch" wirken. Der Client muss nicht wissen, wie man Klassen und Methoden benannt hat. Der Pfad soll die Fachdomäne ausdrücken und bei Implementierungsänderungen stabil bleiben.
Fehler Nr. 5: Zu „clevere" oder „originelle" Pfade.
Manchmal möchte man es schön machen: /api/v1/myTasks, /api/v1/tasksForMe, /api/v1/allTasks. Aber „schön" bedeutet oft „unvorhersehbar". Für den Client ist es viel besser, wenn es eine Basisressource /tasks gibt, und alle Nuancen (Filter, Auswahlen, Zugehörigkeit) durch andere Teile des Vertrags ausgedrückt werden — ohne das grundlegende Ressourcenmodell zu brechen.
GO TO FULL VERSION