1. Ein schnell aufgesetzter Endpoint ist noch kein Vertrag
Fangen wir mit einem sehr typischen Bild an. Ein Entwickler kann bereits eine Spring Boot-Anwendung starten und schreibt so etwas:
@RestController // Wir teilen Spring mit: das ist ein REST-Controller, Antworten werden serialisiert (normalerweise als JSON)
@RequestMapping("/api/v1/tasks") // Basis-Präfix für alle Routen dieses Controllers
class TaskController {
private final TaskService taskService; // Abhängigkeit zum Service: Geschäftslogik/Datenzugriff gehören nicht in den Controller
TaskController(TaskService taskService) { // Konstruktor-Injektion: die Abhängigkeit muss zwingend übergeben werden
this.taskService = taskService; // Wir speichern den Service, um ihn in den Request-Handlern zu nutzen
}
@GetMapping("/{taskId}") // GET /api/v1/tasks/{taskId} — Ressource per Bezeichner aus dem Pfad abrufen
Task getById(@PathVariable String taskId) { // taskId kommt aus der URL (z. B. /api/v1/tasks/t-42)
return taskService.findById(taskId); // Wir geben das interne Modell direkt zurück (das ist das potenzielle Vertragsproblem)
}
}
Aus der Perspektive von Java und Spring Boot sieht das alles ganz normal aus ⚙️. Es gibt einen Controller, eine Route, einen Service-Aufruf und ein Ergebnis. Die Anwendung startet, der Endpoint antwortet, Postman zeigt JSON. In diesem Moment ist es verlockend zu denken: „Na gut, die API ist fertig." Und genau hier beginnt ein subtiler, aber sehr wichtiger Fehler.
Das Problem ist nicht, dass der Code „schlecht" ist. Das Problem ist, dass aus diesem Fragment überhaupt nicht erkennbar ist, welchen externen Vertrag du mit dem Client hast. Was passiert, wenn taskId nicht existiert? Was gibst du zurück, wenn das interne Task-Objekt Felder enthält, die der Client nicht sehen soll? Wie sieht das Fehlerformat aus? Bekommt der Client 404, 500 oder einfach den String "task not found"? Kann man die Feldnamen als stabil betrachten?
Das heißt: ein funktionierender Endpoint und eine ausgereifte API sind nicht dasselbe. Ein funktionierender Endpoint beantwortet die Frage: „Kann die Anwendung technisch gesehen irgendetwas per HTTP zurückgeben?" Eine ausgereifte API beantwortet die Frage: „Kann der Client sicher und vorhersehbar auf dieses Verhalten zählen?" Das ist eine ganz andere Ebene des Gesprächs.
Es ist sinnvoll, gleich eine nüchterne Sache festzuhalten. Spring Boot hilft sehr gut dabei, eine Webanwendung schnell aufzusetzen 🔮. Aber produktseitige und vertragliche Entscheidungen trifft es nicht für dich. Es entscheidet nicht, welche Felder du dem Client versprichst, welcher Status bei verschiedenen Szenarien gilt, wie du Fehler formatierst, wo die Grenzen zwischen dem internen Modell und dem externen JSON verlaufen. Das Framework hilft dir, eine Straße zu bauen, wählt aber nicht die Route.
2. Was der Client von deiner API wirklich bekommt
Um den Unterschied nicht abstrakt, sondern ganz konkret zu spüren, reicht es, nicht auf die Controller-Methode zu schauen, sondern auf zwei Antworten, die der Client tatsächlich sehen wird. Nehmen wir ein sehr einfaches Szenario: Der Client fragt eine Aufgabe per Bezeichner ab.
Wenn die Aufgabe gefunden wurde, könnte er so etwas bekommen:
# Erfolgsstatus: Ressource gefunden und zurückgegeben
HTTP/1.1 200 OK
# Der Server teilt dem Client explizit mit, dass JSON folgt
Content-Type: application/json
# Antwort-Body: öffentliche Felder, auf die der Client zählen wird
{
"id": "t-42",
"title": "Fix validation error format",
"status": "IN_PROGRESS"
}
Wenn die Aufgabe nicht gefunden wurde, sollte eine ausgereifte API nicht „irgendwie" antworten, sondern strukturiert. Zum Beispiel so:
# Fehlerstatus: Es gibt keine Ressource mit diesem Bezeichner
HTTP/1.1 404 Not Found
# Typisches Format für Fehler nach RFC 7807 (Problem Details)
Content-Type: application/problem+json
# Fehler-Body: strukturierte Beschreibung, damit der Client den Grund versteht und vorhersehbar reagieren kann
{
"type": "https://example.dev/problems/task-not-found",
"title": "Task not found",
"status": 404,
"detail": "Task with id t-42 was not found"
}
Schau, was hier wichtig ist 🍄. Der Client weiß nicht, ob du TaskService, Map, JPA, if, switch oder hundert Zeilen Refactoring hattest. Für ihn zählt nur: Bei Erfolg gibt es 200 OK und verständliches JSON; fehlt die Ressource — 404 Not Found und eine verständliche Fehlerbeschreibung. Darauf baut er sein Verhalten auf. Das Frontend rendert einen Bildschirm. Die mobile App zeigt eine Meldung. Ein anderer Service versteht, ob er die Anfrage wiederholen muss. Ein automatischer Test prüft den Status und die JSON-Struktur.
Und hier der erste Kerngedanke. Eine API ist nicht dein Code. Eine API ist das beobachtbare Verhalten an der äußeren Grenze des Produkts. Der Client integriert sich nicht mit der Klasse TaskController, sondern mit einer Menge von Versprechen: welcher Pfad existiert, welches Antwortformat stabil ist, welche Fehler möglich sind, was die Statuscodes bedeuten.
Kurz gesagt lautet die Formel: Der Client integriert sich mit dem Verhalten, nicht mit der Implementierung. Das ist ein einfacher Gedanke, hat aber sehr weitreichende Konsequenzen. Sobald du eine API so siehst, wird sofort klar, warum man Fehler nicht wie Müll behandeln darf, warum JSON-Felder nicht gedankenlos geändert werden können, warum ein zufälliger 500 das Vertrauen in den Vertrag zerstört – und nicht einfach „das Log rot macht".
3. Qualitätsprüfung: ein Erfolg und ein Fehler
Zu Beginn muss man nicht alle Feinheiten von REST, HTTP-Semantik und MVC-Internals kennen. Schon eine erfolgreiche Antwort und eine Fehlerantwort zeigen sehr schnell, wie ausgereift eine API wirkt 📉📈. Das ist fast wie ein Röntgenbild: Zwei Aufnahmen reichen, um einen Knochenbruch zu sehen.
Vergleich zwei Ansätze:
| Frage des Clients | Einfach funktionierender Endpoint | Ausgereifter API-Vertrag |
|---|---|---|
| Was bekomme ich bei Erfolg? | „Irgendein JSON" | Einen klaren Status und eine vorhersehbare Antwortstruktur |
| Was bekomme ich bei einem Problem? | Einen zufälligen String, einen Framework-Fehler oder 500 | Einen sinnvollen Status und ein einheitliches Error-Payload |
| Welche Felder sind hier stabil? | Unklar, hängt vom internen Modell ab | Klar, weil der Vertrag von der Implementierung getrennt ist |
| Was verspricht der Server genau? | „Die Methode funktioniert irgendwie" | Konkretes Verhalten im Erfolgs- und Fehlerfall |
Hier beginnt das professionelle Niveau. Nicht dort, wo du mehr Annotationen kennst, sondern dort, wo du beide Seiten des Vertrags im Blick behältst: Erfolg und Fehler. Solange ein Entwickler nur an den Erfolgsfall denkt, schreibt er Endpoints für Demos. Wenn er Erfolg und Fehler gleich ernst nimmt, beginnt er, eine API zu schreiben, mit der man in einem echten System leben kann.
Das zeigt sich besonders deutlich an einem typischen Einsteigerproblem 💥. Die Anwendung „funktioniert" oft, solange alle Anfragen perfekt sind. Aber sobald eine schlechte Eingabe kommt – eine nicht existierende ID, ein ungültiger Enum-Wert, ein fehlender Body, ein unerwartetes Feld oder ein falsches Format – wird das Verhalten zufällig. Mal 400, mal 500, mal eine lange Exception-Meldung, mal eine HTML-Fehlerseite, mal einfach eine leere Antwort. Der Client fängt an zu raten. Und Raten ist die schlechteste Art der Integration.
Wir müssen jetzt noch nicht besprechen, wie genau dieser Error-Vertrag innerhalb von Spring MVC aufgebaut wird. Es reicht zu verstehen, dass er überhaupt existieren muss. Dieser Kurs wird genau diese Disziplin Schritt für Schritt aufbauen: damit jeder Endpoint nicht „eine Methode, die irgendetwas zurückgibt" ist, sondern Teil eines durchdachten externen Vertrags.
4. Manchmal hilft Spring Boot – und manchmal schweigt es
Es ist jetzt sehr wichtig, nicht in einen falschen Kampf „Spring Boot gegen API-Design" abzugleiten. Diesen Kampf gibt es nicht 🙂. Spring Boot leistet enorm viel Nützliches: Es startet die Anwendung schnell, gibt einen vorhersehbaren Start, hilft beim Zusammensetzen von Abhängigkeiten, startet die Web-Schicht und lässt uns nicht in Rituale ertrinken. Ohne es wäre dieser Kurs methodisch deutlich schwerer.
Aber Boot hat eine natürliche Verantwortungsgrenze. Es hilft dir, schnell einen funktionierenden Web-Mechanismus zu bekommen – nicht automatisch einen ausgereiften externen Vertrag. Das sind zwei verschiedene Aufgaben.
Hier ist der Unterschied in einem kurzen Vergleich:
| Was Spring Boot stark vereinfacht | Was es nicht für dich entwirft |
|---|---|
| Start der Anwendung und der Web-Schicht | Die Semantik von URIs und Operationen |
| Wiring, Konfiguration, grundlegendes Runtime | Die Struktur der Success-Response |
| Die technische Möglichkeit, JSON zurückzugeben | Die Struktur der Error-Response |
| Einbindung von Standardbibliotheken | Die Grenze zwischen internem Modell und DTO |
| Der schnelle Weg zum ersten Endpoint | Die Regeln zur Evolution des öffentlichen Vertrags |
Das ist kein Nachteil von Boot. Das ist die ehrliche Grenze seiner Rolle. Ein Framework soll produktseitige und vertragliche Fragen nicht durch Magie lösen. Sonst würden wir keine Plattform bekommen, sondern einen Generator zufälliger Architekturentscheidungen. Deshalb ist ein separater Kurs nach dem grundlegenden Spring Boot hier absolut logisch: Zuerst lernst du, eine Anwendung aufzusetzen, dann – ihre äußere Grenze ausreifen zu lassen.
In der Praxis bedeutet das eine einfache Sache. Wenn du bereits weißt, wie man @RestController schreibt, bist du nicht zu spät zu diesem Kurs gekommen. Im Gegenteil, du bist zum richtigen Zeitpunkt hier. Du hast jetzt eine technische Grundlage und kannst endlich nicht mehr darüber reden, wie man einen Endpoint zum Antworten bringt, sondern wie er genau richtig antworten soll.
5. Fünf Client-Fragen an jeden Endpoint
Damit das heutige Gespräch nicht bei schöner Theorie bleibt, nehmen wir daraus eine nützliche Linse mit. Durch sie lässt sich jeder Endpoint gut betrachten – ob Lernprojekt oder Produktionscode. Man kann sie gut neben einem .http-Request haben, neben einem Entwurf einer Controller-Methode oder einfach im Kopf beim Code Review.
| Frage | Was du aus der Perspektive des Clients prüfst |
|---|---|
| 1. Um welche Ressource oder welches Szenario geht es? | Anhand von Methode + Pfad ist klar, was der Client genau anfragt oder ändert |
| 2. Wie sieht die erfolgreiche Antwort aus? | Es gibt einen klaren Status, eine verständliche JSON-Struktur und vorhersehbare Felder |
| 3. Wie sieht ein Fehler aus? | Der Fehler hat einen sinnvollen Status und einen strukturierten Antwort-Body |
| 4. Was kontrolliert der Client, was der Server? | Es ist erkennbar, welche Daten von außen kommen und welche die Anwendung selbst vergibt |
| 5. Was soll hier morgen noch stabil sein? | Es ist klar, welche Feldnamen, Statuscodes und Regeln nicht zufällig geändert werden dürfen |
Diese Tabelle hat eine angenehme Eigenschaft. Sie funktioniert vor dem Code, während des Codes und nach dem Code. Vor dem Code hilft sie, Controller-Methoden nicht blind zu schreiben. Während des Codes verhindert sie, das interne Modell nach außen zu leak-en oder negative Szenarien zu vergessen. Nach dem Code hilft sie schnell zu prüfen, ob ein Endpoint nicht zu „es funktioniert bei mir, aber ich weiß nicht, was ich dem Client versprochen habe" geworden ist.
6. Die Fragen auf die Aufgabe Task Tracker API anwenden 🔎
Jetzt wenden wir diese Linse auf einen künftigen Endpoint unseres Projekts an: GET /api/v1/tasks/{taskId}. Wir brauchen jetzt weder die vollständige API-Karte noch die gesamte Projektarchitektur. Ein einziger Querschnitt reicht.
| Linsen-Frage | Kurze Antwort für GET /api/v1/tasks/{taskId} |
|---|---|
| Um welches Szenario geht es? | Der Client möchte eine einzelne Aufgabe per Bezeichner abrufen |
| Wie sieht Erfolg aus? | 200 OK und JSON mit den öffentlichen Feldern der Aufgabe |
| Wie sieht ein Fehler aus? | 404 Not Found und eine strukturierte Fehlerantwort |
| Was kontrolliert der Client? | Nur taskId im Pfad; der Server bestimmt selbst, wie und woher er die Daten holt |
| Was soll stabil bleiben? | Der Ressourcenpfad selbst, die Struktur des öffentlichen JSON und das Fehlermodell |
Das Interessanteste hier ist das, worüber wir nicht gesprochen haben 🙃. Wir haben nicht besprochen, wo die Daten liegen. Wir haben JPA nicht diskutiert. Wir haben Security nicht angetastet. Wir sind nicht in die Details der Spring MVC Internals eingetaucht. Und trotzdem haben wir bereits ein recht professionelles Gespräch über eine API geführt. Für den Kursstart ist das ein sehr wichtiges Gefühl.
7. Typische Fehler beim ersten Blick 🚧
Fehler Nr. 1: Den Endpoint nur durch die Java-Methode betrachten.
Das ist die häufigste Anfänger-Falle. Der Entwickler sieht @GetMapping, die Methodensignatur und denkt, das Bild ist vollständig. Aber der Client hat keinen Zugang zu deiner Signatur. Für ihn existieren nur Statuscode, Headers und Body. Wenn man nicht auf diese externe Perspektive umschaltet, wird die gesamte API „für den Code-Autor" entworfen, nicht für den Verbraucher.
Fehler Nr. 2: Erfolgreichen JSON für ein ausreichendes Qualitätsmerkmal halten.
Es ist sehr verlockend zu denken: „Na ja, es gibt eine Antwort, also ist alles in Ordnung." Nein. Eine API wird erst ausgereift, wenn sowohl Erfolg als auch Fehler vorhersehbar sind. Wenn nur der Erfolgsfall sauber ist und der negative Fall zufällig, ist der Vertrag trotzdem schwach.
Fehler Nr. 3: Denken, Spring Boot „denkt" den Vertrag für dich aus.
Das Framework wählt nicht für dich aus, was du dem Client versprichst. Es hilft dir, schnell einen Web-Mechanismus aufzubauen, ersetzt aber nicht die technische Entscheidung darüber, wo die Vertragsgrenze liegt, welche Felder öffentlich sind und wie das Fehlermodell aussieht.
Fehler Nr. 4: Eine Error-Response als nebensächliche Kleinigkeit betrachten.
Für den Client ist ein Fehler kein Müll-Szenario, sondern Teil des normalen API-Lebens. Wenn er nicht entworfen wurde, beginnt der Client-Code auf Vermutungen zu arbeiten. Und Vermutungen sind immer teurer als ein einmal durchdachter Vertrag.
GO TO FULL VERSION