CodeGym /Kurse /Java Server /201 Created, Location und JSON-Antwort

201 Created, Location und JSON-Antwort

Java Server
Level 25, Lektion 1
Verfügbar

1. Erstellen ist nicht 200 OK

Wenn Sie früher Konsolenprogramme geschrieben haben, tappen Sie leicht in die Falle: „Hauptsache, es wird erstellt, und die Antwort … nun, wir geben 200 und irgendeinen Text zurück.“ In der HTTP-Welt ist das ungefähr so, als würden Sie zum Kurier sagen: „Ich habe das Paket angenommen, aber die Frachtnummer gebe ich nicht – vertrau mir einfach.“ Kunden lächeln höflich … und fangen an, Krücken zu schreiben, um zu erraten, was passiert ist.

Wenn Sie einen POST auf eine Sammlung machen, erstellen Sie eine neue Ressource, die es „gestern“ noch nicht gab. Sie bekommt eine neue Identität (id) und, was besonders wichtig ist, eine Adresse, unter der sie gelesen werden kann (GET /api/v1/reading-list/{id}). Das ist nicht einfach nur „Anfrage erfolgreich verarbeitet“, sondern ein eigener Erfolgstyp.

Deshalb ist es wichtig zu lernen, mit dem Client auf der Sprache von HTTP zu sprechen, nicht auf der Sprache „meine Methode lief ohne Exception ab“. Erfolg in Java und Erfolg in HTTP sind zwei verschiedene Dinge. In Java können Sie ein Objekt zurückgeben und zufrieden sein. In HTTP müssen Sie ein Dreigestirn zusammenstellen: Status + Header + Body, sonst wird der Vertrag unscharf.

Die gute Nachricht: Das ist kein „Akademismus um des Akademismus willen“. Es vereinfacht wirklich das Leben Ihres zukünftigen Clients – sei es Postman, ein Frontend oder ein anderer Dienst.

Bedeutung des Status 201 Created

Der Status 201 Created ist die Art des Servers, klar zu sagen: „Ich habe nicht nur deine Anfrage verarbeitet, ich habe eine neue Ressource erstellt.“ Und das sind wertvolle Informationen, denn der Client kann sich danach anders verhalten. Zum Beispiel kann die UI „Erstellt“ anzeigen, das Element ohne erneute Anfrage zur Liste hinzufügen oder zum Detailbildschirm wechseln – und das alles, ohne im Kaffeesatz lesen zu müssen.

Um die Bedeutung zu verdeutlichen, lohnt sich ein Vergleich der häufigsten „Erfolgs“-Status, die uns in der CRUD-Welt begegnen:

Operation (im normalen CRUD) Typischer Status Gibt es einen Body? Was bedeutet es auf menschlich?
GET /... 200 OK meistens ja „Hier sind die Daten, die du angefordert hast“
POST /collection 201 Created oft ja „Ich habe eine neue Ressource erstellt; hier ist das Ergebnis“
PUT/PATCH /resource 200 OK oft ja „Ich habe die Ressource aktualisiert; hier ist der neue Zustand“
DELETE /resource 204 No Content nein „Gelöscht. Hier gibt es nichts zu zeigen – weiß einfach, dass alles ok ist“

In diesem Kurs lernen wir nicht „viele Status zu kennen“, sondern einen Status nach dem Sinn der Operation auszuwählen. Und 201 ist eine direkte Fortsetzung dieser Gewohnheit.

Es gibt noch einen wichtigen Punkt. 201 Created geht fast immer mit dem Header Location einher. Formal kann man 201 auch ohne Location zurückgeben, aber das wäre, als würde man jemandem einen neuen Reisepass aushändigen und nicht sagen, wo er ihn später abholen kann. Der Client möchte nicht nur sehen, dass es erstellt wurde, sondern auch, „wohin man jetzt für diese Ressource gehen kann“.

2. Der Header Location: Adresse der neuen Ressource

Location ist ein Antwort-Header, der die URI der erstellten Ressource enthält. Sein Zweck ist einfach: dem Client eine konkrete Adresse zu geben, unter der er das gerade erstellte Objekt abrufen kann. Beachten Sie: Das ist keine Weiterleitung (wir machen kein 302), wir zwingen den Client nicht, „irgendwohin zu gehen“. Wir teilen lediglich mit: „Hier ist die Adresse“.

In unserer API sieht das maximal natürlich aus:

- Element erstellen via POST /api/v1/reading-list

- id erhalten, z.B. 42

- also die Adresse des Elements: /api/v1/reading-list/42

Und genau diesen Wert setzen wir in Location.

Der häufigste Anfängerfehler ist es, Location: /api/v1/reading-list zu setzen (also die Adresse der Sammlung). Das ist nutzlos: Der Client weiß bereits, wohin er seinen POST gesendet hat. Er braucht die Adresse des konkreten Elements.

Relativer und absoluter Location

In der idealen Welt wird Location oft absolut angegeben, wie http://localhost:8080/api/v1/reading-list/42. In unserer lokalen Übungs-API ist es zulässig, den relativen Pfad /api/v1/reading-list/42 zurückzugeben. Postman und die meisten Clients verstehen das problemlos, und für ein Übungsprojekt ist es auch einfacher: Wir müssen nicht scheme/host/port, Proxys und andere erwachsene Sorgen erraten.

Wenn man es „etwas korrekter“ machen möchte, kann man einen absoluten Location über den Host-Header zusammenbauen, aber ich möchte das jetzt nicht in eine separate Mini-Quest verwandeln. Hauptsache der Sinn: Location muss auf die erstellte Ressource verweisen, nicht auf die Sammlung.

Ein kleiner Code-Ausschnitt, der die Idee zeigt:

// created ist das bereits gespeicherte Objekt, das eine id erhalten hat
long id = created.getId();

// Location muss auf die spezifisch erstellte Ressource verweisen, nicht auf die Sammlung
String location = "/api/v1/reading-list/" + id;

// Antwort-Header setzen wir vor sendResponseHeaders(...)
exchange.getResponseHeaders().set("Location", location);

Hier ist wichtig, dass id erst erscheint, nachdem der Service das Objekt erstellt hat. Das heißt, Location sind „Post-factum“-Daten; sie können nicht korrekt „im Voraus“ gebildet werden.

3. JSON in der Antwort: Content-Type und DTO

Status und Location sind großartig, aber in der Praxis ist es für den Client oft bequem, auch einen Antwortkörper zu erhalten. Warum? Weil die Ressource nach der Erstellung eine id bekommt, Standardwerte angewendet werden können, der status normalisiert werden kann, und der Client möchte den endgültigen Zustand sehen, „wie der Server ihn akzeptiert hat“.

Dabei gibt es zwei gute Gewohnheiten.

Die erste Gewohnheit: kein Domänenobjekt direkt zurückgeben. Ja, technisch können Sie ReadingListItem in JSON serialisieren und senden. Aber dann wird der externe API-Vertrag von der internen Domänenstruktur abhängig. Heute fügen Sie ein Feld internalNotes für sich selbst hinzu – morgen „lecken“ Sie dieses Feld versehentlich in die API. So beginnen normalerweise die Geschichten „warum sieht das Frontend etwas, das es nicht sehen sollte“.

Die zweite Gewohnheit: den korrekten Content-Type setzen. Wenn Sie JSON ausliefern, schreiben Sie application/json. Sonst muss der Client raten: Ist es JSON? Text? „Was für Bytes haben Sie mir geschickt, geehrter Server?“

Wenn Sie bereits ein ReadingItemResponse haben (wir haben das Antwortmodell früher entworfen), dann sieht das Mapping genauso geradlinig aus wie eine Übersetzung von Deutsch auf Deutsch:

public ReadingItemResponse toResponse(ReadingListItem item) {
    // DTO ist das, was wir nach außen versprechen (API-Vertrag),
    // während das Domänenmodell sein eigenes Leben führen und sich unabhängig ändern kann
    return new ReadingItemResponse(
            item.getId(),         // id erscheint nach der Erstellung
            item.getTitle(),      // das, was der Client zu sehen erwartet
            item.getAuthor(),
            item.getStatus(),     // z.B. könnte der Server den Status normalisiert/gesetzt haben
            item.getExternalId(),
            item.getComment()
    );
}

Und die Serialisierung mit Jackson bleibt langweilig und vorhersagbar (und das ist ein Kompliment):

// Jackson gibt sofort Bytes aus, praktisch für HttpExchange (dort schreiben wir in OutputStream)
byte[] json = objectMapper.writeValueAsBytes(response);

// Content-Type – Teil des Vertrags: der Client soll das Format nicht erraten müssen
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");

Beachten Sie charset=utf-8. Für JSON ist das eine häufig gute Gewohnheit. Und das schützt auch vor der Situation, wenn jemand plötzlich die Antwort als windows-1251 liest und „Привет“ sieht.

4. Antwort 201 + Location + JSON in HttpServer

Jetzt setzen wir die gesamte Antwort zusammen: 201, Location, JSON-Body. Es ist wichtig, die Reihenfolge der Aktionen in HttpExchange zu verstehen: zuerst setzen Sie die Header, dann rufen Sie sendResponseHeaders(...) auf, und erst danach schreiben Sie in den response body. Wenn Sie die Reihenfolge vertauschen, ist es wie im Leben: „Wir haben den Brief schon abgeschickt, aber lass uns noch ein paar Zeilen hinzufügen“.

Wir haben bereits created

An dieser Stelle hat der Create-Ablauf bereits alle Vorarbeiten erledigt: Der Handler hat den Body gelesen, CreateReadingItemRequest zusammengestellt, der Service hat das Objekt gespeichert, und wir haben ein ReadingListItem created mit einer neuen id in der Hand. Es ist wichtig, diesen Weg nicht noch einmal zu gehen, sondern ruhig die Antwort nach außen zusammenzustellen: Location, Header und JSON-Body.

Location und JSON vorbereiten

Wir bauen die Adresse der neuen Ressource und erzeugen JSON-Bytes. Ein wichtiger Hinweis: ObjectMapper.writeValueAsBytes(...) gibt sofort Bytes in UTF-8 aus, das heißt, wir müssen nicht manuell „einen String umkodieren“.

// id ist erst nach der Erstellung verfügbar – daher wird Location nachträglich gebildet
long id = created.getId();

// Location verweist auf die erstellte Ressource
String location = "/api/v1/reading-list/" + id;

// Bereiten den Antwortkörper (JSON) als Bytes vor
byte[] json = objectMapper.writeValueAsBytes(response);

Header und Antwortkörper

Hier beginnt das „manuelle Spring MVC“. Nichts Schlimmes, einfach sorgfältig vorgehen.

// Header setzen wir vor dem Senden der Statuszeile
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
exchange.getResponseHeaders().set("Location", location);

// Senden Status und Körperlänge (wichtig: vor dem Schreiben in den body)
exchange.sendResponseHeaders(201, json.length);

// Schreiben den Antwortkörper und schließen den Stream (damit der Client versteht, dass die Antwort beendet ist)
try (var os = exchange.getResponseBody()) {
    os.write(json);
}

Betrachtet man diesen Ausschnitt als Vertrag, liest er sich ideal „von oben nach unten“: Wir haben den Datentyp deklariert, die Adresse der Ressource mitgeteilt, den Status gesagt und den Körper gesendet. Alles.

Am Ende sollten Sie normalerweise den exchange schließen (manchmal reicht es, den OutputStream zu schließen, aber für Übungscode ist es besser, explizit zu sein):

// Explizites Schließen des exchange: das vermeidet einige „seltsame Hänger“ auf der Clientseite
exchange.close();

Ja, das ist eine weitere Zeile. Aber dann haben Sie weniger Chancen, ein merkwürdiges Hängen des Clients zu erleben, der auf das Ende der Antwort wartet.

Helper gegen Copy-Paste

In der Praxis werden Sie ziemlich schnell sehen, dass das Setzen von Content-Type, die Serialisierung und das Schreiben von Bytes in verschiedenen Endpunkten wiederholt werden. Es ist wichtig, nicht in ein „lass uns unser eigenes Spring bauen“ zu verfallen. Aber eine kleine utility-Methode, die die Mechanik nicht versteckt, sondern nur Copy-Paste vermeidet, ist völlig in Ordnung.

Zum Beispiel eine Methode „JSON mit Status und zusätzlichem Header senden“:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;

static void sendCreated(HttpExchange exchange, ObjectMapper mapper, Object body, String location) throws IOException {
    // Serialisieren das Objekt in JSON (Bytes lassen sich bequem direkt in den response body schreiben)
    byte[] json = mapper.writeValueAsBytes(body);

    // Content-Type – damit der Client das Format eindeutig erkennt
    exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");

    // Location – Adresse der erstellten Ressource
    exchange.getResponseHeaders().set("Location", location);

    // Zunächst senden wir Status und Körperlänge
    exchange.sendResponseHeaders(201, json.length);

    // Dann schreiben wir den Antwortkörper
    try (var os = exchange.getResponseBody()) {
        os.write(json);
    }

    exchange.close();
}

Beachten Sie: Das ist kein „Framework“, sondern einfach eine kurze Funktion, die genau das tut, was Sie auch von Hand gemacht haben. Sie sehen immer noch, was passiert: Status, Header, JSON.

5. Typische Fehler beim Erstellen einer Ressource

Fehler Nr. 1: 200 OK zurückgeben, „weil alles erfolgreich war“.
Auf der Ebene „es funktioniert“ – ja, das wird funktionieren. Aber auf der Vertragsebene verlieren Sie den Sinn der Operation. 201 Created trägt eine spezifische Semantik: eine neue Ressource wurde erstellt. Der Client muss nicht anhand der JSON-Form oder des Textes „created: true“ erraten, was genau passiert ist.

Fehler Nr. 2: Location auf die Sammlung setzen, nicht auf die konkrete Ressource.
Wenn Sie nach POST /api/v1/reading-list Location: /api/v1/reading-list zurückgeben, teilen Sie nichts Neues mit. Der Client braucht die Adresse des Elements: /api/v1/reading-list/{id}. Das ist besonders nützlich, wenn der Client-Code „zur Karte des erstellten Elements wechseln“ möchte, ohne zusätzliche Berechnungen.

Fehler Nr. 3: Versuchen, Location vor der Erstellung des Objekts zu bilden.
Location ist ohne id nicht möglich. Wenn Sie Location „nach Augenmaß“ oder basierend auf den Daten der Anfrage generieren, werden Sie früher oder später eine Diskrepanz erhalten: Der Server hat tatsächlich ein Element mit einer id erstellt, aber in Location liegt eine andere. Am Ende geht der Client zu Location, bekommt dort 404 und bekommt eine existenzielle Krise: „Wurde überhaupt etwas erstellt?“

Fehler Nr. 4: Content-Type bei JSON-Antwort vergessen.
Postman „errät“ es normalerweise, aber echte Clients könnten es nicht erraten. Content-Type: application/json ist Teil der Vereinbarung. Wenn Sie eine API erstellen, sollten Sie den Client nicht zwingen, ein Hellseher zu sein.

Fehler Nr. 5: 201 senden, aber „das falsche Objekt“ zurückgeben.
Manchmal serialisiert man aus Unachtsamkeit das Request-DTO oder ein anderes Zwischenobjekt. Die Antwort muss genau den erstellten Zustand widerspiegeln, einschließlich der generierten id. Wenn im Body keine id steht, muss der Client einen zusätzlichen GET durchführen oder Location parsen, um die ID zu erfahren. Das ist nicht immer schlecht, aber in unserem Übungs-CRUD-Szenario ist es einfacher und klarer, die erstellte Ressource sofort zurückzugeben.

1
Aufgabe
Java Server, Level 25, Lektion 1
Gesperrt
Erstellen einer Lernnotiz mit 201 Created und Location
Erstellen einer Lernnotiz mit 201 Created und Location
1
Aufgabe
Java Server, Level 25, Lektion 1
Gesperrt
Podcast-Abo mit der Adresse der erstellten Ressource
Podcast-Abo mit der Adresse der erstellten Ressource
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION