CodeGym /Kurse /Java Server /Wohin mit den Anfragedaten?

Wohin mit den Anfragedaten?

Java Server
Level 8, Lektion 3
Verfügbar

1. Datenplatzierung als Teil des API-Designs

Wenn man gerade anfängt, scheint es das Wichtigste zu sein, „dass der Server es versteht“. Und wenn der Server es versteht, kann man auch id in die Query packen, status in den Header, title in den Path, und obendrauf noch alles im body duplizieren – der Sicherheit halber, wie Tesafilm auf einem Koffer. Das Problem ist, dass APIs nicht nur von Computern, sondern auch von Menschen gelesen werden. Und Menschen im Gegensatz zu Computern werden müde, verwirrt und beginnen, Ihren Vertrag mit stillem professionellem Hass zu hassen.

Die Sache ist einfach: Verschiedene Teile einer HTTP-Anfrage wurden ursprünglich für verschiedene Rollen entwickelt. Der Path antwortet auf „wohin gehen wir“ und „welche Ressource berühren wir“, die Query – auf Präzisierungen beim Lesen („zeig mir nur dies“, „filtere so“), die Header – auf dienstliche Verarbeitungsregeln (Format, Erwartungen, Besonderheiten), und der body – auf die primäre Nutzlast beim Erstellen/Ändern. Wenn Sie diese Logik befolgen, liest sich die Anfrage fast wie ein Satz in menschlicher Sprache. Wenn nicht, erhalten Sie ein Rätsel, bei dem /api/v1/reading-list/42?id=42 aussieht wie der Versuch, dem Server das Offensichtliche zu erklären, aber mit Angst.

Wichtig ist auch, dass verschiedene Teile der Anfrage „verschiedene Leben leben“. Die URL wird häufiger in Logs und im Verlauf gesehen, Header werden leicht in ihrer Bedeutung verwechselt, die Query-Zeichenfolge verwandelt sich schnell in ein langes Band, und der body enthält normalerweise das, weswegen die Anfrage überhaupt gemacht wurde. Daher ist die Frage „wohin mit den Daten“ keine dekorative Ästhetik, sondern eine Grundlage der Disziplin, die die Interaktion vorhersagbar macht.

2. Path: Identität der Ressource

Der Path wird meistens als „Adresse“ in HTTP wahrgenommen. Und das ist eine gute Intuition: Der Pfad beantwortet die Frage „Welche Ressource wollen wir genau berühren?“. Daher ist der Path ideal, wenn Sie die Identität eines Objekts angeben. Wenn Sie ein bestimmtes Element der Leseliste möchten, wenden Sie sich an dieses als eine bestimmte „Adresse“, und id lebt in diesem Fall im Path als Teil der Route.

Stellen Sie sich vor, reading-list sei ein „Haus“ und ein konkretes Listenelement eine „Wohnung“. Dann ist /api/v1/reading-list das Haus und /api/v1/reading-list/42 die Wohnung Nr. 42. Und wenn Sie „in Wohnung 42“ wollen, ist es sinnlos, auf den Umschlag zu schreiben: „Haus reading-list, Wohnung 42, und unten nochmal Wohnung=42 in Kleinschrift“. Die Adresse sollte eine sein und klar.

Mini-Illustration auf der Ebene „nur Zeichenfolgen“ (vorerst ohne echten Client und Server):

// Pfad – das ist die "Adresse" der Ressource: Pfad ändern -> das angesprochene Objekt ändert sich.
String url1 = "/api/v1/reading-list";    // Sammlung (Haus)
String url2 = "/api/v1/reading-list/42"; // konkretes Element (Wohnung)

// Zur Veranschaulichung geben wir einfach die resultierenden URLs aus.
System.out.println(url1); // /api/v1/reading-list
System.out.println(url2); // /api/v1/reading-list/42

Ein wichtiger Gedanke: Der Path-Parameter (z.B. 42) sollte normalerweise so sein, dass man ihn leicht lesen, parsen und als Identifikator verwenden kann. In Lehrbeispielen ist das meistens eine Zahl (long) oder eine Zeichenkette (externe ID). Aber große Datenblöcke in den Path zu „stopfen“ ist eine schlechte Idee. Der Path ist der Platz für „wer genau“, nicht für „hier die ausführliche Buchbeschreibung über 3 Absätze“.

Es gibt noch ein nützliches Merkmal: Wenn Sie den Wert im Path ändern, ändern Sie mit hoher Wahrscheinlichkeit das „angesprochene Objekt“. /api/v1/reading-list/41 und /api/v1/reading-list/42 sind zwei verschiedene Objekte. Das ist Identität.

3. Query: Filter, Suche, Präzisierungen

Query-Parameter (?key=value&key2=value2) sind wie präzisierende Sätze in einer Bitte. Sie ändern nicht die grundlegende „Adresse“ der Ressource, aber sie ändern, wie Sie sie lesen oder auswählen möchten. Daher eignet sich die Query hervorragend zum Filtern einer Liste, zur Suche nach einem Teil des Titels, für einfache zusätzliche Bedingungen. Das Schlüsselwort ist „Präzisierung“: Sie lesen immer noch /api/v1/reading-list, fordern aber, es nicht vollständig, sondern in einer bestimmten Form anzuzeigen.

Zum Beispiel möchten Sie eine Liste von Büchern nur mit dem Status PLANNED oder nur die, in deren Titel das Wort clean vorkommt. Das ist keine „andere Ressource“, sondern dieselbe Liste, aber mit einem Filter.

// Query – das sind "Präzisierungen beim Lesen": Filter, Suche, optionale Flags.
String url = "/api/v1/reading-list?status=PLANNED&title=clean";

// Wichtig: Die Basisressource bleibt dieselbe (/api/v1/reading-list), nur "wie gelesen wird" ändert sich.
System.out.println(url); // /api/v1/reading-list?status=PLANNED&title=clean

Hier ist es wichtig, nicht in zwei Extreme zu verfallen. Das erste – zu versuchen, eine einzelne konkrete Ressource über die Query zu identifizieren: /api/v1/reading-list?id=42. Formal mag das funktionieren, aber sinnhaft haben Sie eine Anfrage an die Sammlung gestellt und dann gebeten: „Bitte, gib mir nur eine zurück“. Normalerweise liest es sich schlechter als /api/v1/reading-list/42. Das zweite Extrem – die Query in einen Koffer ohne Henkel zu verwandeln und alles hineinzustopfen, einschließlich der „primären Erstellungsdaten“. Wenn die Query-Zeichenfolge wie eine kleine Novelle aussieht, wird Ihre URL schwer lesbar und zerbricht die Hauptidee der Query: ein kurzer Satz von Bedingungen zu sein.

Um den Unterschied zwischen Path und Query zu fixieren, ist es hilfreich, nicht philosophisch, sondern praktisch darauf zu schauen:

Frage Pfad Abfrage
Was beschreibt es? „Welche Ressource?“ „Welche Präzisierungen beim Lesen?“
Normalerweise obligatorisch? Ja, um zum richtigen Objekt zu gelangen Normalerweise nein, oft optional
Beispiel /api/v1/reading-list/42 /api/v1/reading-list?status=PLANNED
Schlechter Geruch „Im Pfad liegen Filter“ „In der Abfrage liegt der Identifikator eines einzelnen Objekts“

Und noch eine Kleinigkeit, die für den Anfängerkopf nützlich ist: Die Query ist fast immer besonders passend bei GET, weil GET normalerweise als „gib mir Daten“ gelesen wird, und es praktisch ist, wenn alle Leseparameter direkt in der URL sichtbar sind. Wir gehen nicht auf technische Details zu GET und body ein, aber die praktische Gewohnheit ist: Filter und Suche zum Lesen einer Liste – in die Query.

4. Header: Dienstinformationen

Header werden von Anfängern oft als „ein weiterer Ort, an den man Daten legen kann“ wahrgenommen. Technisch – ja, man kann fast alles hineinlegen. Aber sinnhaft sind Header nicht die „Daten der Ressource“, sondern dienstliche Regeln zur Verarbeitung der Anfrage und Antwort. Stellen Sie sich vor, die URL ist die Adresse, der body ist der Brief im Umschlag, und die Header sind die Aufkleber auf dem Umschlag: „Zerbrechlich“, „Per Express liefern“, „Briefformat soundso“. Die Aufkleber sind wichtig, aber Sie schreiben doch nicht den gesamten Brieftext auf den Aufkleber.

Aus der vorherigen Vorlesung haben wir bereits zwei Hauptbeispiele: Accept und Content-Type. Sie betreffen das Format, nicht die Domänenentität (Buch). Wenn der Client JSON erhalten möchte, sagt er: Accept: application/json. Wenn der Client JSON im Body sendet, sagt er: Content-Type: application/json.

Hier ist ein Beispiel „Lesen der Liste mit Filter“: Filter in der Query, Antwortformat im Header.

# Lesen der Sammlung mit Filter (Filter in der Abfrage).
GET /api/v1/reading-list?status=PLANNED HTTP/1.1
Host: localhost:8080
# Accept – das ist die Erwartung bezüglich des Antwortformats (Dienstinformation).
Accept: application/json

Beachten Sie, dass status=PLANNED nicht in den Header gewandert ist. Es ist nicht „dienstlich“. Es ist eine anwendungsbezogene Lesebedingung, also Query.

Und hier ist ein Beispiel „Erstellen eines Elements“: Die primären Daten im Body, das Format dessen, was Sie senden, im Content-Type, und das Format, das Sie erhalten möchten, im Accept.

# Element erstellen: Entitätsdaten im Body.
POST /api/v1/reading-list HTTP/1.1
Host: localhost:8080
# Content-Type – Format dessen, was im Body der Anfrage liegt.
Content-Type: application/json
# Accept – Format, das der Client in der Antwort erhalten möchte.
Accept: application/json

{"title":"Clean Code","author":"Robert C. Martin","status":"PLANNED"}

Wir analysieren heute nicht, wie genau der Inhalt des body aufgebaut ist und warum dort JSON steht (das wird ein separates, großes Thema). Wichtig ist uns die Disziplin: Header – für Metadaten, Body – für die Nutzlast, Query/Path – für Adressierung und Präzisierungen.

5. Body: Nutzlast

Body – das ist der Ort, an dem es logisch ist, die „Hauptdaten der Operation“ abzulegen, besonders wenn Sie eine Ressource erstellen oder ändern. Wenn der Client sagt: „Erstelle mir ein neues Element der Leseliste“ oder „Aktualisiere dieses Element“, muss er eine Reihe von Feldern übergeben: Titel, Autor, Status, Kommentar. Das ist offensichtlich keine „Präzisierung beim Lesen“ und schon gar kein „dienstlicher Header“. Das ist der Inhalt, für den die Operation überhaupt begonnen wird. Also – body.

Wenn man den Body in einem Satz beschreiben sollte, dann sind es die „Innereien der Anfrage“. Deshalb kommt der Body am häufigsten bei Methoden vor, die den Zustand ändern: POST, PUT, PATCH. Wenn Sie versuchen, diese Daten über die Query zu übergeben, erhalten Sie mit ziemlicher Sicherheit eine Anfrage, die unangenehm zu lesen, schwer zu erweitern und leicht zu zerstören ist, weil jedes Feld als Zeichenfolge kodiert werden muss.

Auf der Ebene des Lehrcodes kann man es sich so vorstellen: Die URL gibt die Route an, der Body enthält die „Daten der neuen Entität“.

// URL – das ist die Route (wohin und zu welcher Ressource wir gehen).
String url = "/api/v1/reading-list";

// Body – das ist der Payload: die primäre Nutzlast der Operation (Entitätsdaten).
String body = """
    {"title":"Clean Code","author":"Robert C. Martin","status":"PLANNED"}
    """;

// Beachte: URL ist kurz und lesbar, während die "schweren" Daten im Body leben.
System.out.println(url);  // /api/v1/reading-list
System.out.println(body); // {"title":"Clean Code",...}

Und noch eine praktische Beobachtung: Wenn Sie eine Anfrage sehen, in der die Entitätsfelder (title, author, comment) in der URL liegen, ist das fast immer ein Zeichen, dass jemand „Adresse“ und „Briefinhalt“ verwechselt hat. Die URL ist dafür gedacht, eine lesbare Route und Lesebedingungen zu sein, keine Serialisierung des gesamten Objekts.

6. Auswahl: Path/Query/Headers/Body

Wenn man alles in eine Formel packt, erhält man eine einfache Regel, die einen angehenden Backend-Entwickler wirklich vor dem „HTTP-Chaos“ bewahrt. Zuerst bestimmen Sie die Bedeutung des konkreten Wertes, und erst dann wählen Sie den Ort in der Anfrage. Das ist, als würde man Sachen in Regale sortieren: Zuerst verstehen, was das für ein Ding ist, dann entscheiden, in welche Schublade es kommt. Sonst bekommt man eine Wohnung, in der Socken im Kühlschrank liegen und Essen im Werkzeugschrank.

Hier ist eine kurze „Entscheidungskarte“ in Tabellenform mit Beispielen:

Um welche Art von Wert handelt es sich? Wohin damit? Beispiel
Identifikator einer konkreten Ressource Pfad /api/v1/reading-list/42
Präzisierung beim Lesen einer Liste (Filter/Suche) Abfrage /api/v1/reading-list?status=PLANNED
Dienstliche Verarbeitungsregeln, Format, Erwartungen Header Accept: application/json
Primäre Daten zum Erstellen/Aktualisieren Body { "title": "...", "author": "..." }

Und für diejenigen, die leichter in „wenn … dann …“ denken, kann man es als Mini-Blockdiagramm darstellen:

flowchart TD
    A["Ein Wert muss in der Anfrage übergeben werden"] --> B{"Ist das die Identität der Ressource? (welches Objekt genau?)"}
    B -->|Ja| P["Lege in den Pfad /api/v1/reading-list/{id}"]
    B -->|Nein| C{"Ist das eine Präzisierung beim Lesen? (Filter, Suche, Bedingung)"}
    C -->|Ja| Q["Lege in die Abfrage ?status=PLANNED"]
    C -->|Nein| D{"Sind das dienstliche Regeln? (Format, Erwartungen)"}
    D -->|Ja| H["Lege in die Header Accept/Content-Type"]
    D -->|Nein| E["Höchstwahrscheinlich ist es der Payload – lege in den Body"]

Separat sollte man sich die „Regel des einen Sinns“ merken: Derselbe Wert sollte normalerweise nicht gleichzeitig in Pfad, Abfrage und Body leben. Wenn Sie /api/v1/reading-list/42?id=42 schreiben und zusätzlich im Body "id": 42 hinzufügen, dann vertrauen Sie sich selbst nicht. Der Server kann dann wählen, „welche id wichtiger ist“, und der Client wird raten, warum mal das eine, mal das andere funktioniert. HTTP-Fehler sind hier nicht nötig – genug menschlicher Fehler, um alles instabil zu machen.

Und als Anti-Beispiel – diese Anfrage (so sollte man es nicht machen, selbst wenn es „funktioniert“):

// Schlechtes Beispiel: Derselbe Sinn ist über verschiedene Teile der Anfrage verschmiert.
String badUrl = "/api/v1/reading-list/42?id=42"; // id sowohl im Pfad als auch in der Abfrage
String badBody = "id=42";                        // und nochmal derselbe Sinn im Body

System.out.println(badUrl);  // /api/v1/reading-list/42?id=42
System.out.println(badBody); // id=42

Hier ist ein Sinn über drei Orte verschmiert. Das verschlechtert die Lesbarkeit und wirft Fragen auf: wenn die Werte unterschiedlich sind, was ist dann wahr? Und wer ist schuld, wenn die Wahrheit sich als „die falsche“ herausstellt?

7. ReadLater-Beispiele ohne „Durcheinander“

Damit dies nicht eine Abstraktion „über HTTP im Allgemeinen“ bleibt, halten wir einige Beispiele in unserer Domäne der persönlichen Leseliste fest. Wichtig: Wir schauen jetzt nur auf die Anfrage und die Platzierung der Daten, ohne das vollständige Szenario mit Antworten und Status zu erfassen – das kommt in einem separaten Schritt. Hier ist unsere Aufgabe, dass man auf einen Blick an der Anfrage erkennen kann, was sie tut und wo welche Werte leben.

Das Beispiel „Ein Element der Leseliste abrufen“ sieht natürlich aus, wenn id im Pfad steht. Das Antwortformat – im Accept:

# Lesen einer konkreten Ressource: Identifikator im Pfad.
GET /api/v1/reading-list/42 HTTP/1.1
Host: localhost:8080
# Möchten eine JSON-Antwort erhalten.
Accept: application/json

Das Beispiel „Liste abrufen und nach Status filtern“ sieht natürlich aus, wenn der Filter in der Abfrage steht. Es ist immer noch das Lesen der Sammlung, nur mit einer Präzisierung:

# Lesen der Sammlung: Filter nach Status in der Abfrage.
GET /api/v1/reading-list?status=IN_PROGRESS HTTP/1.1
Host: localhost:8080
Accept: application/json

Das Beispiel „Suche nach Titel“ (keine Identifikation eines einzelnen Objekts, sondern ein Kriterium) – ebenfalls Abfrage:

# Suche/Filter – das ist eine Präzisierung beim Lesen, daher Abfrage.
GET /api/v1/reading-list?title=clean HTTP/1.1
Host: localhost:8080
Accept: application/json

Das Beispiel „Neues Element erstellen“ – das ist bereits Payload im Body, und das Format des Payloads wird durch Content-Type beschrieben:

# Erstellen: Daten der neuen Entität werden im Body gesendet.
POST /api/v1/reading-list HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Accept: application/json

{"title":"Clean Code","author":"Robert C. Martin","status":"PLANNED"}

Und ein kleiner Bonus für diejenigen, die gerne auf Java-Ebene „anfassen“: Die Klasse URI in der JDK hilft zu sehen, dass Path und Query verschiedene Teile derselben Zeichenfolge sind und unterschiedliche Rollen haben.

import java.net.URI;

// In einer URL kann man klar trennen: Path (Ressourcenadresse) und Query (Präzisierungen beim Lesen).
URI uri = URI.create("http://localhost:8080/api/v1/reading-list/42?verbose=true");

// Path beantwortet die Frage "welche Ressource?".
System.out.println(uri.getPath());   // /api/v1/reading-list/42
// Query beantwortet die Frage "mit welchen Parametern/Präzisierungen lesen?".
System.out.println(uri.getQuery());  // verbose=true

Selbst in einem solchen Beispiel sieht man, dass verbose=true nicht die „Ressourcenadresse“ ist, sondern eine zusätzliche Bedingung. Genau diesen Unterschied wollen wir auf der Ebene des Anfragedesigns erreichen: dass Adresse und Präzisierungen nicht vermischt werden.

8. Typische Fehler bei der Datenplatzierung

In diesem Thema sehen Fehler normalerweise „harmlos“ aus: Die Anfrage wurde ja gesendet, der Server hat etwas geantwortet, irgendwie lebt alles. Aber später stellt sich heraus, dass die API unbequem zu lesen ist, das Erweitern Angst macht, und Client und Server regelmäßig streiten, „welcher Teil der Anfrage wichtiger ist“. Die gute Nachricht: Fast alle diese Fehler werden mit einer Frage geheilt: „Welche Bedeutung hat dieser Wert?“

Fehler Nr. 1: Der Ressourcenidentifikator wird in die Abfrage statt in den Pfad gelegt.
Eine Anfrage der Form /api/v1/reading-list?id=42 funktioniert, sieht aber sinnhaft aus wie: „Ich bin zur Sammlung gekommen und habe gebeten, ein Element auszuwählen“. Wenn es um ein einzelnes konkretes Objekt geht, liest sich der Pfad einfacher und ehrlicher: /api/v1/reading-list/42. So zeigen Sie sofort, dass Sie eine einzelne Ressource bearbeiten, nicht eine Liste.

Fehler Nr. 2: Filter und Bedingungen werden in den Pfad gestopft.
Manchmal trifft man auf etwas wie /api/v1/reading-list/status/PLANNED als Filtermethode. Das verwandelt die URL schnell in eine Kette von „fast-Befehlen“, bei denen es schwer ist, die Route von den Lesebedingungen zu unterscheiden. Für Filter und Suche hat die Abfrage bereits einen normalen Platz: /api/v1/reading-list?status=PLANNED. So behalten Sie eine verständliche Ressource (reading-list) und fügen optionale Präzisierungen hinzu.

Fehler Nr. 3: Primäre Daten zum Erstellen/Aktualisieren werden über die Query-Zeichenfolge übergeben.
Eine Anfrage der Form POST /api/v1/reading-list?title=Clean%20Code&author=...&comment=... sieht aus wie der Versuch, einen „Brief“ auf der Adressseite des Umschlags zu schreiben. Das ist unbequem zu lesen, unbequem zu erweitern, leicht zu zerstören bei Sonderzeichen, und insgesamt ist das nicht das, wofür die Query gedacht ist. Die primären Daten der Operation sollten im Body leben, und die Abfrage bleibt für kurze Lesepräzisierungen.

Fehler Nr. 4: Anwendungsdaten der Ressource werden in die Header gelegt.
Header sind kein „zweiter Body“. Wenn Sie versuchen, title oder status als Header zu übergeben, machen Sie den Vertrag weniger offensichtlich: Der Client muss raten, welche Header obligatorisch, welche optional sind, und warum plötzlich ein „Buchfeld“ weder im Body noch in der URL gelandet ist. Header sind gut für Formate und Verarbeitungsregeln (Accept, Content-Type), und Anwendungsdaten sollten besser dort bleiben, wo man sie erwartet: in der Abfrage (wenn es ein Lesefilter ist) oder im Body (wenn es Daten der zu erstellenden/aktualisierenden Ressource sind).

Fehler Nr. 5: Derselbe Sinn wird an verschiedenen Stellen der Anfrage dupliziert.
Die gefährlichste Variante – wenn id sowohl im Pfad als auch in der Abfrage und im Body steht. Heute sind sie gleich, morgen hat sich jemand geirrt und die Werte divergieren. Danach beginnt das Rätselraten: Was ist die „Wahrheit“? Eine gute Disziplin ist, einen Ort nach Sinn auszuwählen und den Wert nicht über die gesamte Anfrage zu verschmieren.

1
Aufgabe
Java Server, Level 8, Lektion 3
Gesperrt
URL-Aufbau für eine Ressource und für die Filterung
URL-Aufbau für eine Ressource und für die Filterung
1
Aufgabe
Java Server, Level 8, Lektion 3
Gesperrt
Vorlage einer POST-Anfrage mit Body
Vorlage einer POST-Anfrage mit Body
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION