Zum Hauptinhalt springen

AB 03 – Die Schnittstelle vervollständigen

Die Situation

Anlegen und Abrufen funktionieren. Die Personalabteilung meldet zurück: Man braucht außerdem eine Übersicht aller Personen, muss Daten ändern können (Namensänderung nach Heirat) und ausgeschiedene Mitarbeiter löschen.

Lernziele

Nach Bearbeitung dieses Arbeitsblatts kannst du:

  • die vier CRUD-Operationen vollständig als REST-Endpunkte umsetzen
  • für jede Operation den passenden HTTP-Statuscode wählen und begründen
  • den Unterschied zwischen PUT und POST an ihrer Idempotenz erklären
  • REST-konforme URLs entwerfen und typische Fehler benennen
  • deine Schnittstelle mit einer Sammlung von Testfällen systematisch prüfen

Die Zuordnung im Überblick

OperationHTTPPfadErfolgsstatus
CreatePOST/api/v1/persons201 Created
Read (alle)GET/api/v1/persons200 OK
Read (eine)GET/api/v1/persons/{id}200 OK
UpdatePUT/api/v1/persons/{id}200 OK
DeleteDELETE/api/v1/persons/{id}204 No Content
Sieh dir die Pfade genau an

Es gibt nur zwei verschiedene Pfade — /persons und /persons/{id}. Fünf Operationen, zwei Adressen. Den Unterschied macht allein die HTTP-Methode.

Genau das meint die einheitliche Schnittstelle aus dem Infoblatt Das REST-Paradigma.

Aufgaben

Aufgabe 1 – Alle Personen abrufen

@GetMapping
public List<Person> getAllPersons() {
return repository.findAll();
}

Beachte: Kein Pfad in der Annotation. Der Endpunkt liegt genau auf dem Klassenpfad /api/v1/persons.

Alle Personen als JSON-Array im Browser

Warum nicht /persons/all oder /getAllPersons?

Weil die Sammlung selbst schon die Adresse ist. /persons bedeutet „die Personen" — alle davon.

/getAllPersons wäre gleich doppelt falsch: ein Verb in der URL, obwohl GET das Verb schon liefert.

Aufgabe 2 – Eine Person ändern

@PutMapping("/{id}")
public Person updatePerson(@PathVariable Long id, @RequestBody Person person) {
Person existing = repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND, "Keine Person mit der Id " + id));

existing.setFirstname(person.getFirstname());
existing.setSurname(person.getSurname());

return repository.save(existing);
}
Ein häufiger Anfängerfehler

Naheliegend wäre:

person.setId(id);
return repository.save(person); // ❌

Das funktioniert scheinbar — hat aber einen Haken: Der Client bestimmt damit jedes Feld der Ressource, auch solche, die gar nicht ihm gehören. Kommt später ein Anlagedatum dazu, das der Server selbst setzt, würde es beim Speichern stillschweigend geleert.

Der Weg über findById lädt erst den vorhandenen Stand und übernimmt daraus genau die Felder, die der Client ändern darf. Was der Server verwaltet, bleibt unangetastet.

Das ist trotzdem ein PUT, kein PATCH

Ein PUT ersetzt die Ressource vollständig — der Client schickt den kompletten neuen Stand, und fehlende Felder gelten als geleert. Genau so verhält sich dein Endpunkt: Wer surname wegläßt, bekommt einen leeren Nachnamen.

Der Unterschied liegt woanders: id und später das Anlagedatum sind keine Felder, die der Client setzen darf. Sie gehören dem Server. Die Auswahl im Code trennt also nicht „geändert / nicht geändert", sondern „gehört dem Client / gehört dem Server".

Was PUT von PATCH unterscheidet, steht im Infoblatt HTTP kompakt.

Aufgabe 3 – Eine Person löschen

@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deletePersonById(@PathVariable Long id) {
Person person = repository.findById(id)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.NOT_FOUND, "Keine Person mit der Id " + id));

repository.delete(person);
}

Zwei Besonderheiten:

Rückgabetyp void. Es gibt nichts zurückzugeben — die Ressource ist weg.

204 No Content statt 200 OK. Der Code sagt genau das aus: erfolgreich, aber kein Inhalt in der Antwort.

Warum nicht einfach deleteById(id)?

Weil deleteById() bei einer unbekannten Id keine Ausnahme wirft, sondern schlicht nichts tut.

Jeder DELETE würde dann mit 204 antworten — auch für Datensätze, die es nie gab. Der Client könnte nicht unterscheiden, ob er etwas gelöscht hat oder ins Leere griff.

Deshalb wird der Datensatz erst geholt. Das ist dasselbe Muster wie bei GET /{id} und PUT /{id}: holen — oder mit 404 abbrechen. Alle drei Endpunkte, die eine Id entgegennehmen, beginnen jetzt mit denselben zwei Zeilen.

Aufgabe 4 – Die Request-Sammlung vervollständigen

requests.http
### Alle Personen
GET http://localhost:8080/api/v1/persons

### Person ändern
PUT http://localhost:8080/api/v1/persons/1
Content-Type: application/json

{
"firstname": "Anna",
"surname": "Schmidt-Meier"
}

### Person löschen
DELETE http://localhost:8080/api/v1/persons/2

### Löschen, was es nicht gibt
DELETE http://localhost:8080/api/v1/persons/999
Warum eine Datei und nicht nur Klicks in einem Werkzeug?

requests.http liegt im Projekt. Die Datei wandert mit in die Versionsverwaltung, jeder im Team hat dieselben Aufrufe, und bei einer Änderung der Schnittstelle sieht man im Diff, was sich geändert hat.

Eine Sammlung, die nur lokal in einem Programm liegt, hat niemand außer dir.

Aufgabe 5 – Idempotenz beobachten

Jetzt, wo alle vier Aufrufe in deiner Datei stehen, geht es um eine Eigenschaft, die man dem Code nicht ansieht — nur dem Verhalten.

Was heißt „idempotent"?

Eine Operation ist idempotent, wenn es keinen Unterschied macht, ob man sie einmal oder mehrfach ausführt: Danach sieht die Welt genauso aus.

Das Wort kommt aus dem Lateinischen: idem = dasselbe, potens = wirkend. Also „dieselbe Wirkung", egal wie oft.

Zwei Beispiele aus dem Alltag:

HandlungEinmalDreimal
Im Aufzug auf 3 drückendu bist im 3. Stockdu bist im 3. Stock✅ idempotent
Ein Stück Kuchen nehmeneins auf dem Tellerdrei auf dem Teller❌ nicht idempotent

Bei Anweisungen an einen Server ist es genau dasselbe. Achte auf das Verb:

  • Setze den Nachnamen auf Meier." → Danach heißt sie Meier. Ob du es einmal oder zehnmal sagst, ändert nichts. Idempotent.
  • Lege eine neue Person an." → Dreimal gesagt, dreimal angelegt. Nicht idempotent.
Ein häufiges Missverständnis

Idempotent heißt nicht, dass nichts passiert. Beim ersten Mal verändert sich sehr wohl etwas.

Es heißt nur: Wiederholungen verändern nichts zusätzlich.

Jetzt selbst nachprüfen

Statt es zu glauben, probierst du es aus.

MethodeNach dreimaligem AusführenIdempotent?
POSTdrei Datensätzenein
PUTein Datensatz, unverändertja
DELETEweg — beim zweiten Mal 404ja
Die Zeile zu DELETE sieht nach einem Widerspruch aus

Beim ersten Mal 204, beim zweiten Mal 404 — zwei verschiedene Antworten. Und trotzdem idempotent?

Ja. Idempotenz ist eine Aussage über den Zustand auf dem Server, nicht über die Antwort. Nach dem zweiten DELETE ist die Welt genau dieselbe wie nach dem ersten: Der Datensatz ist weg. Nur die Auskunft darüber fällt anders aus — beim ersten Mal „habe ich gelöscht", beim zweiten „gibt es nicht".

Prüffrage also nicht „kommt dieselbe Antwort?", sondern „sieht der Server danach gleich aus?"

Warum das praktisch zählt

Ein Client schickt eine Bestellung per POST und bekommt wegen einer Netzstörung keine Antwort. Darf er es erneut versuchen?

Nein — die Bestellung könnte schon angelegt sein. Bei PUT und DELETE wäre ein zweiter Versuch dagegen unbedenklich.

Das ist der Grund für den Hinweis „Bitte nicht zweimal klicken" auf Bezahlseiten.

Eine Übersicht über alle HTTP-Methoden — und dazu den verwandten Begriff sicher — findest du im Infoblatt Das REST-Paradigma.

Aufgabe 6 – URL-Entwurf beurteilen

#AufrufDein Urteil
1GET /api/v1/persons/17
2POST /api/v1/deletePerson/17
3GET /api/v1/persons?surname=Schmidt
4GET /api/v1/person/all
5PUT /api/v1/persons/17
6POST /api/v1/persons/17/setSurname
Auflösung
  1. ✅ Korrekt. Substantiv in der Mehrzahl, Id als Pfadbestandteil.
  2. ❌ Verb in der URL, und POST für ein Löschen. Richtig: DELETE /api/v1/persons/17.
  3. ✅ Korrekt. Ein Filter gehört als Abfrageparameter an die Sammlung — die Ressource bleibt dieselbe.
  4. ❌ Einzahl und ein überflüssiges /all. Richtig: GET /api/v1/persons.
  5. ✅ Korrekt.
  6. ❌ Verb in der URL, und für eine Änderung ist PUT zuständig. Richtig: PUT /api/v1/persons/17 mit dem neuen Stand im Rumpf.

Aufgabe 7 – Vollständige Abnahme

Die Reihenfolge zählt mit

Jeder Testfall baut auf dem Zustand auf, den die vorherigen hinterlassen haben. Deshalb nennt keine Vorbedingung mehr „Datenbank ist leer": Notiere stattdessen vor dem Testfall, wie viele Datensätze es gerade gibt, und prüfe hinterher die Veränderung.

Das ist auch im Betrieb der Normalfall — Testfälle laufen selten auf einer jungfräulichen Datenbank.

IDBeschreibungVorbedingungTestschritteErwartetes ErgebnisErgebnis
TF-01Leere Sammlung abrufenAnwendung frisch gestartet, keine Daten angelegt
  1. GET auf /api/v1/persons
Status 200, Rumpf ist []nicht 404.
TF-02Alle Personen abrufenZwei Personen wurden angelegt
  1. GET auf /api/v1/persons
Status 200, ein JSON-Array mit genau zwei Objekten.
TF-03Person ändernPerson mit Id 1 existiert
  1. PUT auf /api/v1/persons/1 mit geändertem Nachnamen
  2. GET auf /api/v1/persons/1
Status 200. Der GET liefert den neuen Nachnamen, die id bleibt 1.
TF-04Nicht vorhandene Person ändernEs gibt keine Person mit Id 999
  1. PUT auf /api/v1/persons/999 mit einem gültigen Rumpf, z.B. {"firstname":"Test","surname":"Test"}
Status 404. Es wird keine neue Person angelegt.
TF-05Person löschenPerson mit Id 2 existiert
  1. DELETE auf /api/v1/persons/2
  2. GET auf /api/v1/persons/2
DELETE liefert 204 ohne Rumpf, der anschließende GET liefert 404.
TF-06Zweimal löschenTF-05 wurde ausgeführt
  1. DELETE auf /api/v1/persons/2 erneut ausführen
Status 404 — nicht erneut 204.
TF-07POST ist nicht idempotentAnwendung läuft; die Anzahl der Datensätze wurde vorher notiert
  1. Denselben POST dreimal ausführen
  2. GET auf /api/v1/persons
Es sind drei Datensätze dazugekommen, jeder mit einer eigenen Id.
TF-08PUT ist idempotentPerson mit Id 1 existiert; die Anzahl der Datensätze wurde vorher notiert
  1. Denselben PUT dreimal ausführen
  2. GET auf /api/v1/persons
Die Anzahl der Datensätze ist unverändert, und Person 1 sieht nach dem dritten Mal genauso aus wie nach dem ersten.
TF-09Fehlerhaftes JSON wird abgewiesenAnwendung läuft
  1. POST auf /api/v1/persons mit dem Rumpf {"firstname":
Status 400 (Bad Request) — nicht 500.
TF-10POST mit mitgeschickter IdEs existiert eine Person mit Id 1
  1. POST auf /api/v1/persons mit dem Rumpf {"id":1,"firstname":"Test","surname":"Test"}
  2. GET auf /api/v1/persons
Es sollte ein neuer Datensatz mit neuer Id entstehen und Person 1 unberührt bleiben. Prüfe, ob das wirklich passiert.
TF-11Falsche Methode auf der SammlungAnwendung läuft
  1. DELETE auf /api/v1/persons (ohne Id)
Status 405 (Method Not Allowed).
TF-10 besteht nicht — und das ist der Punkt

Probier es aus: Person 1 heißt danach Test, und es ist kein neuer Datensatz entstanden. Der Statuscode lautet trotzdem 201 Created.

Der Grund steht in deinem Controller:

public Person createPerson(@RequestBody Person person) {
return repository.save(person);
}

@RequestBody füllt alle Felder aus dem JSON — auch id. Und save() sieht eine gesetzte Id, hält das Objekt für bekannt und macht daraus ein UPDATE statt eines INSERT.

Damit kann jeder Aufrufer fremde Datensätze überschreiben, indem er einfach eine Id mitschickt.

Die Ursache ist grundsätzlicher Art: Deine Entität Person ist gleichzeitig das, was von außen hereinkommt. Der Client bestimmt damit Felder, die ihm nicht gehören. Die saubere Lösung ist ein eigenes Objekt für die Anfrage, in dem id schlicht nicht vorkommt. Solche Objekte heißen DTOData Transfer Object, ein Objekt, das nur dem Datentransport über die Grenze der Anwendung dient und nicht dem Speichern. Darauf kommen wir zurück.

Notiere den Testfall als nicht bestanden. Ein Testfall, der einen echten Mangel aufdeckt, hat seine Arbeit getan.

TF-01 ist der Testfall, den man am ehesten falsch baut

Eine leere Sammlung ist kein Fehler. /persons existiert, sie ist nur gerade leer — also 200 OK mit [].

404 würde bedeuten, dass es die Sammlung gar nicht gibt. Das ist etwas anderes.

Der Unterschied zwischen [] und null ist im Infoblatt JSON genauer beschrieben.

Ausblick

Deine Schnittstelle ist vollständig, aber noch nicht robust. Diese Fragen bleiben offen:

  • Was passiert, wenn jemand eine Person ohne Namen anlegt? (→ Validierung)
  • Warum verschwinden alle Daten beim Neustart? (→ dauerhafte Speicherung)
  • Der Controller redet direkt mit der Datenbank — wohin gehört eigentlich die Fachlogik? (→ Service-Schicht)
  • Wie verhindert man, dass ein Client Felder setzt, die ihm nicht gehören — siehe TF-10? (→ DTOs)
  • Du hast alle Testfälle von Hand ausgeführt. Beim nächsten Mal wieder? (→ automatisierte Tests)

Das Gästebuch in Tutorial 2 nimmt einige davon auf.

Zusammenfassung

Das hast du gelernt
  • Alle vier CRUD-Operationen liegen auf zwei Pfaden; unterschieden wird über die HTTP-Methode.
  • 201 beim Anlegen, 200 beim Lesen und Ändern, 204 beim Löschen, 404 bei unbekannter Id.
  • Eine leere Sammlung ist 200 mit [], nicht 404.
  • POST ist nicht idempotent, PUT und DELETE sind es.
  • In REST-URLs gehören Substantive; Filter kommen als Abfrageparameter an die Sammlung.
  • deleteById() wirft bei unbekannter Id keine Ausnahme — deshalb vorher prüfen.

Selbstkontrolle

  1. Warum liegt der Endpunkt für „alle Personen" auf demselben Pfad wie der zum Anlegen?
  2. Welchen Statuscode liefert ein erfolgreiches DELETE und warum nicht 200?
  3. Erkläre an einem Beispiel, warum POST nicht idempotent ist.
  4. Wie unterscheidet ein Client „Sammlung ist leer" von „Sammlung gibt es nicht"?
  5. Warum ist PUT /persons/17 besser als POST /persons/17/setSurname?
Antworten
  1. Weil beide auf dieselbe Ressource wirken — die Sammlung aller Personen. Unterschieden wird über die Methode: GET liest sie, POST fügt ihr etwas hinzu.
  2. 204 No Content. Es gibt nichts zurückzugeben; 200 würde einen Inhalt ankündigen, der nicht kommt.
  3. Dreimal POST /persons mit denselben Daten erzeugt drei Datensätze mit drei verschiedenen Ids. Das Ergebnis hängt davon ab, wie oft man die Anfrage schickt — genau das ist nicht idempotent.
  4. Über den Statuscode: 200 mit [] heißt „gibt es, ist leer". 404 heißt „gibt es nicht".
  5. Weil setSurname ein Verb in der URL ist und eine einzelne Methode nach außen sichtbar macht. PUT drückt „ersetze diese Ressource" bereits aus; welche Felder sich ändern, steht im Rumpf. Sonst bräuchte man für jedes Feld einen eigenen Endpunkt.

Musterlösung

Hier steht der vollständige Quelltext, so wie er nach allen drei Arbeitsblättern aussieht: die Entität, das Repository, beide Controller, die application.properties und die Datei requests.http.

Erst selbst versuchen

Eine Lösung zu lesen fühlt sich an wie Verstehen, ist aber keines. Wer sie aufschlägt, bevor er selbst gescheitert ist, nimmt sich genau den Teil weg, an dem man etwas lernt.

Sinnvoll ist sie in zwei Fällen: nach der eigenen Lösung zum Vergleichen — oder wenn du an einer Stelle wirklich nicht weiterkommst. Dann sieh dir nur diese eine Datei an und arbeite danach selbst weiter.

Musterlösung

Das Passwort bekommst du von deiner Lehrkraft.