1. Eine HTTP-Nachricht als Briefumschlag
Wenn du jemals einen Papierbrief verschickt hast (oder sie zumindest aus Filmen kennst), erinnerst du dich: Außen gibt es Adresse, Briefmarken und Dienstvermerke, innen den Inhalt. Eine HTTP-Nachricht funktioniert ähnlich. Einsteiger bemerken oft nur das „Schöne" – den JSON im Body – und betrachten alles andere als langweilige Verpackung. Aber genau diese „langweilige Verpackung" bestimmt, was überhaupt passiert: wo wir uns hinwenden, was wir zu tun bitten, wie der Server das Datenformat verstehen soll und was der Client in der Antwort lesen muss.
HTTP-Nachrichten gibt es in zwei Varianten: Request (Anfrage) und Response (Antwort). Sie haben unterschiedliche erste Zeilen, aber die Logik danach ist sehr ähnlich: Startzeile, Header, eine leere Trennzeile und optional ein Body. Es ist wichtig, eine einfache Sache zu behalten: Wenn du Daten falsch auf die Teile einer Nachricht verteilst, wird der API-Vertrag undurchsichtig. Ein undurchsichtiger Vertrag ist, wenn der Client, der Server und du selbst in drei Wochen auf dasselbe schauen, aber jeder etwas anderes sieht.
Hier ist eine kurze „Landkarte". Wir werden im Verlauf der Vorlesung darauf zurückkommen.
| Teil | Wo er sich befindet | Welche Frage er beantwortet |
|---|---|---|
| method | erste Zeile des Requests | „Was möchtest du tun?" |
| path | innerhalb der URL / des Request-Targets | „Wohin wendest du dich?" |
| query | nach dem ? | „Mit welchen Parametern präzisierst du die Anfrage?" |
| headers | einzelne Zeilen Name: Value | „Welche Dienstbedingungen und Vereinbarungen gelten für den Austausch?" |
| body | nach der Leerzeile | „Welche inhaltlichen Daten werden übermittelt?" |
In der ersten Vorlesung haben wir HTTP als abgeschlossenen Zyklus betrachtet: Der Request wurde gesendet, die Antwort kam oder kam nicht. Jetzt müssen wir den Request und die Antwort selbst aufschlüsseln und in Details zerlegen. Ohne das bleiben method, path, headers und body schöne Wörter statt nützliche Werkzeuge zum Debuggen und Entwerfen. Wenn die Struktur sichtbar wird, hört die erste Zeile einer Antwort auf, wie Rauschen auszusehen, und Header hören auf, langweilige Verpackung zu sein.
Lass uns gleich vereinbaren, wie wir Beispiele aufschreiben. In einer .http-Datei oder in Postman siehst du normalerweise eine vollständige URL wie http://localhost:8080/api/v1/tasks. Im rohen HTTP bleibt in der ersten Zeile normalerweise nur das Request-Target – also /api/v1/tasks?status=TODO&page=0. Ich werde unten häufig diese Form zeigen: ohne Schema und Host, damit die Anatomie der Nachricht leichter zu analysieren ist.
2. Die erste Zeile des Requests
Wenn du einen HTTP-Request sendest, sieht der Server zuerst nicht den JSON und nicht die Header, sondern die allererste Zeile – die Request Line. Sie gibt das „Skelett" des Requests vor: welche Methode verwendet wird, wohin der Request adressiert ist und welche HTTP-Version angewendet wird. Das ist wie der erste Satz in einer Nachricht: Wenn du mit „Hallo, gib mir..." anfängst, kann man danach Details klären. Wenn du mit einem unklaren „Äh..." anfängst, wird es für alle schwer.
Für HTTP/1.1 sieht diese Zeile so aus:
METHOD <Leerzeichen> REQUEST-TARGET <Leerzeichen> HTTP/1.1
In der Praxis wird in Lernbeispielen oft genau diese Form verwendet, weil sie anschaulich und direkt lesbar ist. Selbst wenn in der echten Welt über HTTP/2 und HTTP/3 alles binär ist, bleibt die Bedeutung dieselbe: Methode, Ziel, Version.
Ein Mini-Beispiel „direkt", ohne Spring – nur um die Form zu sehen:
// Wir bauen die Request Line manuell: Methode + Target + Protokollversion.
String method = "GET";
String target = "/api/v1/tasks?status=TODO&page=0"; // Das Target enthält sowohl Path als auch Query
String requestLine = method + " " + target + " HTTP/1.1";
System.out.println(requestLine); // GET /api/v1/tasks?status=TODO&page=0 HTTP/1.1
Beachte ein wichtiges Detail: target enthält hier sowohl Path als auch Query. Das heißt, schon in der ersten Zeile ist erkennbar, „wohin und mit welchen Präzisierungen" wir gehen.
Wenn man sich einen kleinen „Parser der ersten Zeile" basteln möchte (als Kopfübung, nicht für die Produktion), kann man das so machen:
// Lern-Parser der Request Line: Im echten Code sollte man noch die Anzahl der Teile prüfen.
String requestLine = "GET /api/v1/tasks/42 HTTP/1.1";
String[] parts = requestLine.split(" ");
String method = parts[0]; // Request-Methode: GET/POST/...
String target = parts[1]; // Request-Target: /api/v1/tasks/42 (kann auch Query enthalten)
System.out.println(method); // GET
System.out.println(target); // /api/v1/tasks/42
Aus API-Vertragssicht ist hier nicht die Fähigkeit, Strings zu teilen, wichtig, sondern das Verständnis: Method und Target sind nicht „irgendwo da", sie leben immer in der ersten Zeile. Genau sie geben die grundlegende Bedeutung des Requests vor, noch bevor wir über Header und Body sprechen.
3. Path: Adresse der Ressource
Der Path ist der Adressteil des Pfades innerhalb deines Servers. Wenn man eine URL als „Hausadresse" betrachtet, ist der Path sozusagen die Straße und die Hausnummer. Ohne Präzisierungen wie „Eingang 2, Stockwerk 7" – das ist Query – und ohne Dienstsachen wie „welche Sprache bevorzugst du" – das sind eher Header. Der Path sollte stabil und verständlich genug sein: Wenn ein Client ihn sieht, sollte er ungefähr verstehen, was genau er adressiert.
Im HTTP-Request ist der Path normalerweise innerhalb des Request-Targets sichtbar. Zum Beispiel:
GET /api/v1/tasks HTTP/1.1
GET /api/v1/tasks/42 HTTP/1.1
GET /api/v1/tasks/42/comments HTTP/1.1
In unserem Lern-API hat der Pfad bereits das Basispräfix /api/v1, das ebenfalls Teil des Paths ist.
Der Path besteht aus Segmenten, die durch / getrennt sind. In den meisten Servern (und im praktischen API-Design) wird er als Hierarchie wahrgenommen: erst die „Kollektion", dann das konkrete Objekt, dann der Unterbereich. Uns ist hier nicht die „REST-Ideologie" wichtig, sondern die Anatomie: Der Path ist alles, was vor dem ? kommt.
Um den Unterschied zwischen Path und Query auf Java-Typ-Ebene zu sehen, ist URI bequem:
import java.net.URI;
// URI kann Path und Query separat zurückgeben – nützlich um zu verstehen, dass das verschiedene URL-Teile sind.
URI uri = URI.create("https://example.com/api/v1/tasks/42?status=TODO&page=0");
System.out.println(uri.getPath()); // /api/v1/tasks/42
System.out.println(uri.getQuery()); // status=TODO&page=0
Keine Magie hier. Das ist einfach ein guter Weg, „live" zu sehen, dass Path und Query verschiedene Teile der Adresse sind, auch wenn sie wie eine einzelne Zeile in der Browser-Adressleiste aussehen.
Manchmal ist es nützlich, den Path in Segmente zu zerlegen – rein als Übung:
// Path in Segmente aufteilen (Lernbeispiel: Im echten Einsatz gibt es Nuancen mit //, Kodierung usw.).
String path = "/api/v1/tasks/42/comments";
// Führendes "/" entfernen und nach "/" trennen.
String[] segments = path.substring(1).split("/");
System.out.println(segments[0]); // api
System.out.println(segments[1]); // v1
System.out.println(segments[2]); // tasks
System.out.println(segments[3]); // 42
System.out.println(segments[4]); // comments
Beachte, dass 42 in der Mitte des Paths bereits ein Hinweis auf den Bezeichner einer konkreten Ressource ist. In einer echten API ist das oft eine UUID, aber auf der Anatomie-Ebene ist etwas anderes wichtiger: Der Path beantwortet meistens die Frage „Wohin wenden wir uns", nicht „Mit welchen Parametern filtern wir" und nicht „Welchen Inhalt übermitteln wir".
4. Query-Parameter
Der Query String ist der Teil der Adresse nach dem ?. Er wird oft unterschätzt, bis der erste echte Listen-Endpoint kommt und sich herausstellt, dass „zeig mir Aufgaben mit Status TODO, sortiert nach Zeit, erste Seite" in keinen ordentlichen Path passt, ohne die URL in einen Tolstoi-Roman zu verwandeln. Der Query ist genau dafür da, eine Anfrage mit Parametern zu präzisieren.
Die Form des Queries ist normalerweise:
/api/v1/tasks?status=TODO&page=0&size=20
Es gibt also Paare key=value, getrennt durch &. Die Tatsache, dass der Query mit ? beginnt, ist bereits ein wichtiger Hinweis: Das ist eine Ergänzung der Anfrage, keine „neue Ressource". Wenn du den Query entfernst, wendest du dich immer noch an denselben Path, aber ohne Präzisierungen.
Sehr wichtig ist, sich an URL-Encoding zu erinnern: Der Query lebt innerhalb der URL, und einige Symbole müssen dort kodiert werden. Ein einfachstes Beispiel – ein Leerzeichen im Namen:
/api/v1/tasks?assigneeName=John%20Smith
Wenn man ein Leerzeichen direkt „hineinstopft", wird das Verhalten unvorhersehbar: Irgendwo wird es automatisch kodiert, irgendwo bricht alles zusammen, und irgendwo sieht der Server nur „John". Besser davon ausgehen, dass man mit URLs sorgfältig umgehen sollte.
Ein kleines Beispiel zur Dekodierung eines Wertes:
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
// Wir dekodieren einen percent-encoded Wert aus dem Query (z. B. %20 -> Leerzeichen).
String pair = "assigneeName=John%20Smith";
String encodedValue = pair.split("=", 2)[1]; // Rechten Teil nach "=" nehmen
String value = URLDecoder.decode(encodedValue, StandardCharsets.UTF_8);
System.out.println(value); // John Smith
Und ein Beispiel „wie man einen Query richtig zusammenbaut" mit URLEncoder:
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
// Wir kodieren den Wert, um ihn sicher in die URL einzufügen (Leerzeichen und Sonderzeichen werden kodiert).
String assignee = URLEncoder.encode("John Smith", StandardCharsets.UTF_8);
String url = "/api/v1/tasks?assigneeName=" + assignee;
System.out.println(url); // /api/v1/tasks?assigneeName=John+Smith
Ja, hier steht + statt %20. Das ist eine normale Eigenheit des application/x-www-form-urlencoded-Kodierungsstils, der in der Webwelt häufig vorkommt. Auf der Ebene unserer Vorlesung ist nicht wichtig, alle Feinheiten zu merken, sondern einen Gedanken mitzunehmen: Query-Parameter sind der Platz für Präzisierungen, aber sie leben in der URL und erfordern sorgfältigen Umgang mit Strings.
Manchmal möchte man den Query einfach in eine Map parsen. Hier ist ein kleines, bewusst einfaches Stück Code (es hat keinen Anspruch auf „perfekten URL-Parser", es ist Lerncode):
import java.util.HashMap;
import java.util.Map;
// Sehr einfacher Parser: teilt nach "&" und "=", macht kein URL-Decoding und behandelt keine doppelten Keys.
Map<String, String> parseQuery(String query) {
Map<String, String> result = new HashMap<>();
for (String pair : query.split("&")) { // Paare in der Form key=value
String[] kv = pair.split("=", 2); // Limit 2: Wert kann "=" enthalten
result.put(kv[0], kv.length == 2 ? kv[1] : ""); // Falls kein "=" vorhanden – Wert als leer betrachten
}
return result;
}
In einem echten Service macht solche Dinge normalerweise die Infrastruktur (Server, Framework, Bibliothek), aber für das Verständnis der Anatomie ist es nützlich, einmal mit eigenen Händen zu sehen: Der Query ist eine Menge von Paaren, keine „Magie", die plötzlich zu Methodenparametern wird.
5. Headers: Dienstvereinbarungen
Header sind Zeilen im Format Name: Value, die direkt nach der Startzeile kommen. Sie sind keine Domain-Daten (bitte kein title einer Aufgabe dort ablegen), bilden aber trotzdem einen riesigen Teil des Austausch-Vertrags: Format des Bodys, erwartetes Format der Antwort, Inhaltslänge, Request-Bezeichner, Authentifizierungsdaten (in unserem Kurs nutzen wir das nicht, aber die Welt schon) und so weiter.
Das wichtigste visuelle Ding, das man sich merken muss: Header werden vom Body durch eine Leerzeile getrennt. Die Leerzeile ist wie „Dienstteil beendet, jetzt beginnt der Inhalt". Wenn du diese Leerzeile vergisst, könnte der Server versuchen, den Body als Header zu lesen oder umgekehrt. Und das ist der seltene Fall, in dem ein einziger leerer Enter wirklich den Abend ruinieren kann.
Beispiel eines Requests ohne Body, aber mit Headern:
GET /api/v1/tasks HTTP/1.1
Host: localhost:8080
Accept: application/json
User-Agent: IntelliJ HTTP Client
Es gibt hier keinen inhaltlichen Body, aber die Header tragen bereits wichtige Informationen. Selbst Accept ist ein Signal, dass der Client ein bestimmtes Antwortformat erwartet (darauf gehen wir heute nicht im Detail ein, uns ist jetzt die Struktur wichtig).
Beispiel eines Requests mit Body, bei dem der Header erklärt, was dieser Body ist:
POST /api/v1/tasks HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Accept: application/json
{"title":"Write docs","description":"HTTP lesson"}
Beachte die Leerzeile zwischen Headern und JSON. Ohne sie sieht die Nachricht fast gleich aus, kann aber völlig anders funktionieren. HTTP kann keine Gedanken lesen, es liest Bytes und Trennzeichen.
Es ist nützlich, eine kleine Liste „häufig vorkommender" Header zu kennen. Nicht als Referenz, sondern als Orientierung – damit man sie nicht mit Query und Body verwechselt.
| Header | Wo vorkommend | Wozu er dient (kurz) |
|---|---|---|
| Host | Request (HTTP/1.1) | Teilt mit, zu welchem Host sich der Client wendet |
| Accept | Request | Welches Antwortformat der Client bevorzugt |
| Content-Type | Request/Response | Welches Format der Body hat |
| Content-Length | Request/Response | Größe des Bodys (falls vorhanden) |
| Location | Response | Wohin die erstellte/umgeleitete Ressource „umgezogen" ist |
| User-Agent | Request | Wer der Client ist (oft für Logs/Analytik) |
Und noch eine Nuance, die im praktischen Denken nützlich ist: Header-Namen sind nicht case-sensitive. Content-Type und content-type sind inhaltlich derselbe Header. Aber wenn du sie in einer Map speicherst, wird die Java-Welt plötzlich case-sensitive (weil das Strings sind). Deshalb normalisiert man oft die Keys.
Mini-Beispiel „Header in Lowercase speichern":
import java.util.Map;
// Beispielidee: Header-Namen vor der Suche in Lowercase umwandeln (um unabhängig von der Groß-/Kleinschreibung zu sein).
String header(Map<String, String> headers, String name) {
return headers.get(name.toLowerCase());
}
Map<String, String> headers = Map.of(
"content-type", "application/json",
"accept", "application/json"
);
System.out.println(header(headers, "Content-Type")); // application/json
Nochmals: Man muss das nicht in jedem Projekt so machen. Aber als mentales Modell ist es nützlich – um zu verstehen, warum in Logs content-type stehen kann, man aber nach Content-Type sucht und nichts „gefunden wird".
6. Body: Daten des Requests
Der Body (Nachrichtenrumpf) ist der Ort, an den normalerweise der „Inhalt" kommt, der für das Geschäft wirklich wichtig ist: JSON mit Daten, Text, Binärdaten. Aber ein wichtiger Punkt: Der Body ist nicht obligatorisch. Manche Requests (typisches GET) gehen meistens ohne Body. Manche Antworten können ebenfalls keinen Body haben. Das ist normal und bedeutet nicht „der Server hat vergessen, JSON zurückzugeben".
Die Hauptrolle des Bodys ist, Daten zu übertragen, die in den Adressteil (Path/Query) nicht passen und die inhaltlich der eigentliche Payload sind. Wenn du zum Beispiel eine Aufgabe in der Task Tracker API erstellst, ist es logisch, Titel und Beschreibung im Body zu senden, nicht in Headern und nicht im Query.
Hier ist ein sehr kompaktes Beispiel eines HTTP-Requests mit Body:
POST /api/v1/tasks HTTP/1.1
Content-Type: application/json
{"title":"Write docs","description":"HTTP lesson"}
Nochmals betone ich die visuelle Struktur: Startzeile, Header, Leerzeile, Body. Wenn du das mit den Augen sehen kannst, bist du bereits auf halbem Weg dahin, bei seltsamen Fehlern der Art „Server hat 400 zurückgegeben, aber JSON sieht doch normal aus" nicht mehr zu hängen.
Wenn man den Request als strukturiertes Objekt und nicht als „irgendwelche Strings" betrachten möchte, kann man ein einfachstes Modell erstellen:
import java.util.Map;
// Lernmodell eines HTTP-Requests: nur das, was für das Verständnis der Anatomie wichtig ist.
public record HttpRequest(
String method, // GET/POST/...
String target, // Request-Target: Path + Query
Map<String, String> headers, // Header
String body // Body (kann null/leer sein)
) {
boolean hasBody() {
// Einfache Prüfung: Body gilt als „vorhanden", wenn er nicht null und nicht leer ist.
return body != null && !body.isBlank();
}
}
Das ist natürlich keine echte HTTP-Client-Implementierung und kein Versuch, Spring zu ersetzen. Das ist nur ein Lern-„Skelett" für den Kopf: Ein Request hat method, target, headers und body, und der Body kann fehlen.
7. Antwort: Status Line, Headers und Body
Nachdem der Server den Request verarbeitet hat, gibt er eine HTTP-Antwort zurück. Sie ist fast genauso aufgebaut: erste Zeile, Header, Leerzeile, Body. Nur die erste Zeile einer Antwort ist anders: Dort gibt es keine Method, aber dafür die Protokollversion, den Statuscode und eine kurze Phrase.
Die Form der ersten Zeile einer Antwort in HTTP/1.1 sieht so aus:
HTTP/1.1 200 OK
Und danach kommen Header und (falls nötig) Body. Beispiel einer Antwort mit einer Aufgabenliste:
HTTP/1.1 200 OK
Content-Type: application/json
[{"id":"42","title":"Write docs"}]
Selbst wenn du noch nicht weißt, was 200 bedeutet oder welche Statuscodes es gibt, kannst du bereits die Struktur erkennen. Und das ist wichtig: Der Client liest nicht nur den Body. Er liest die erste Zeile (Ergebnis) und die Header (Bedingungen und Format), und der Body ist der „Antwortinhalt", falls vorhanden.
Um die Struktur endgültig im Gedächtnis zu verankern, ist ein einfaches Schema nützlich:
flowchart TD
A["Start line
Request: GET /api/v1/tasks HTTP/1.1
Response: HTTP/1.1 200 OK"] --> B["Headers
Name: Value"]
B --> C["Blank line"]
C --> D["Body (optional)
JSON / text / bytes"]
Wenn man sich zumindest dieses Bild merkt, wird es deutlich weniger Situationen geben, in denen „Postman zeigt einen Fehler, ich schaue nur auf den JSON und verstehe nicht".
8. Task Tracker API: drei Szenarien
Jetzt fassen wir alles zusammen, damit du siehst: method, path, query, headers und body sind keine separaten „Lehrbuchbegriffe", sondern Details desselben Objekts. Stellen wir uns vor, wir rufen unsere Lern-API manuell auf.
Szenario „Aufgabenliste mit Status TODO abrufen" (hier wird kein Body benötigt, der Request ist ein „Daten anfragen", kein „Daten übertragen"):
GET /api/v1/tasks?status=TODO&page=0 HTTP/1.1
Host: localhost:8080
Accept: application/json
Hier ist Method GET, Path /api/v1/tasks, Query status=TODO&page=0, Header sind Host und Accept, Body fehlt. Und das ist normal.
Szenario „Aufgabe erstellen" (hier wird ein Body benötigt, weil wir inhaltliche Daten übertragen):
POST /api/v1/tasks HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Accept: application/json
{"title":"Write docs","description":"HTTP lesson"}
Der inhaltliche Unterschied ist, dass der Body den Payload enthält. Und ohne Content-Type wäre es für den Server schwierig (oder unmöglich), korrekt zu verstehen, was sich darin befindet.
Szenario „Eine konkrete Aufgabe abrufen" (wieder ein Request ohne Body, aber mit einem präziseren Path):
GET /api/v1/tasks/42 HTTP/1.1
Host: localhost:8080
Accept: application/json
Selbst ohne Spring und ohne Controller siehst du: Der Unterschied zwischen „Liste" und „einzelnes Element" spiegelt sich oft direkt im Path wider, und Präzisierungen gehen in den Query.
9. Typische Fehler bei der Arbeit mit Teilen einer HTTP-Nachricht
Fehler Nr. 1: Nur den Body lesen und die erste Zeile sowie die Header ignorieren.
Eine sehr häufige Gewohnheit: „Wenn ich JSON sehe, ist alles ok." Aber in der Realität können Status und Header die Bedeutung des Geschehens komplett verändern. Manchmal gibt der Server eine HTML-Fehlerseite zurück, man versucht sie als JSON zu parsen und wundert sich, warum „Jackson meckert". Eine nützliche Disziplin: Schaue immer die gesamte Nachricht an – Start Line, Headers, Body.
Fehler Nr. 2: Path und Query verwechseln und die URL zum Brei machen.
Wenn Filter und Präzisierungen anfangen, in den Path zu „kriechen", entstehen hässliche Konstruktionen, die schwer zu lesen und zu pflegen sind. Und umgekehrt: Wenn der Ressourcenbezeichner plötzlich ein Query-Parameter wird, wird der Client-Code weniger vorhersehbar. Eine gute Gewohnheit: Path ist „wohin wenden wir uns", Query ist „wie präzisieren wir die Anfrage".
Fehler Nr. 3: Business-Daten über Header übertragen „weil es einfacher ist".
Header sind kein Container für title, description und andere Domain-Felder. Wenn du anfängst, inhaltliche Daten dort abzulegen, schaffst du einen Vertrag, der schwer zu dokumentieren und noch schwerer zu pflegen ist. Header sollen der Dienstteil bleiben: Format, Erwartungen, Korrelation, technische Austauschbedingungen.
Fehler Nr. 4: Die Leerzeile zwischen Headers und Body vergessen.
Das ist ein Klassiker bei .http-Dateien und manuellen Requests. Visuell sieht alles „fast gleich" aus, aber das Protokoll braucht einen klaren Trenner. Fehlt die Leerzeile, kann der Body als Fortsetzung der Header gelesen werden. Das Ergebnis äußert sich meist darin, dass der Server den Body „nicht sieht", während man der Welt beweist, dass er „definitiv da ist".
Fehler Nr. 5: Body ohne Content-Type oder mit falschem Content-Type senden.
Wenn du JSON überträgst, muss der Server verstehen, dass es JSON ist. Ohne den richtigen Header kann er versuchen, die Daten anders zu interpretieren, oder die Verarbeitung verweigern. Selbst wenn ein Client „rät", wird ein anderer Client nicht raten, und der Vertrag wird zur Lotterie.
GO TO FULL VERSION