Übungsaufgaben zu REST-API und Fehlerbehandlung: Gesuche über die API einstellen

https://bildung.social/@oerinformatik/

https://oer-informatik.de/spring-boot-rest-api-fehlerbehandlung-uebungsaufgaben

REST-API, Fehlerbehandlung, Problem Details, RFC 9457, ProblemDetail, RestControllerAdvice, ExceptionHandler, HTTP-Statuscode, Mapper, LazyInitializationException, Übungsaufgaben

tl/dr; (ca. 33 min Lese- und Bearbeitungszeit): Übungen zur Fehlerbehandlung im Mitfahr-Backend: Du ordnest Fehlersituationen rund um Mitfahrgesuche den Statuscodes zu, liest eine Fehlerantwort nach RFC 9457, schreibst einen GesuchMapper und einen GesuchController, findest heraus, warum der Flurmonitor mit Gesuchen plötzlich 500 meldet, und bewertest einen Vorschlag, den Statuscode in die fachlichen Ausnahmen zu verlagern [AI-Assisted]. (Zuletzt geändert am 11.11.2026)

Dieser Artikel ist Teil einer Artikelreihe über ein REST-Backend mit Spring Boot: Überblick und Fahrplan / 01 Anforderungen und API-Vertrag / 02 Projekt anlegen / 03 Codegenerierung / 04 Fachlogik und Komponententest / 05 Build-Qualität und CI / 06 Persistenz mit JPA / 07 Repository-Integrationstest / 08 Service und Dependency Injection / 09 REST-API und Fehlerbehandlung / Übungsaufgaben zu 09

Diese Übungsaufgaben gehören zum Artikel Fehler mit Format: restliche Endpunkte, Mapper und Problem Details nach RFC 9457 und setzen den Übungsstrang zu den Mitfahrgesuchen fort. Ausgangspunkt ist das Projekt im Ordner src/09-rest-api-fehlerbehandlung des Repositorys, ergänzt um die Klassen aus den bisherigen Übungen: Zeitfensterregel, Gesuchstatus, die Entity Gesuch (mit Gettern für alle Attribute und der Methode zieheZurueck()), GesuchRepository, NeuesGesuch und GesuchService aus Übung 08. Der GesuchService hat zusätzlich die Methode Optional<Gesuch> finde(Long gesuchId). Die fachlichen Ausnahmen des Übungsstrangs sind so eingeordnet:

Fachliche Ausnahmen im Übungsstrang Gesuch
Ausnahme Oberklasse wird geworfen, wenn …
ZeitfensterUngueltigException FachregelVerletztException das Zeitfenster nicht vor seinem Ende beginnt oder länger als 90 Minuten ist
GesuchUnzulaessigException FachregelVerletztException das Datum vor dem heutigen Tag liegt
GesuchNichtOffenException ZustandskonfliktException ein Gesuch zurückgezogen werden soll, das nicht mehr offen ist
NichtGefundenException MitfahrlaborException Nutzer, Haltepunkt oder Gesuch nicht existieren

Der Vertrag openapi.yaml ist um die Schemas GesuchNeu, Gesuch, GesuchStatus und GesuchStatusAenderung (nur der Wert zurueckgezogen) aus Übung 01 erweitert, dazu um die Operationen stelleGesuchEin (POST /gesuche), holeGesuch (GET /gesuche/{gesuchId}) und aendereGesuch (PATCH /gesuche/{gesuchId}) mit dem Tag gesuche. Im MonitorEintrag darf art jetzt Angebot oder Gesuch sein. Wie in Übung 01 heißt das Feld für die Person im Vertrag mitfahrerId, im Record NeuesGesuch aber nutzerId.

Fahrplan der Artikelreihe im V-Modell; auf der Ebene Implementierung ist Artikel 09 hervorgehoben (mit KI erstellt)

Hinweise zur Bearbeitung: Bis zur Teil-1-Prüfung der IT-Berufe solltest Du die Basiskompetenz beherrschen, also Aufgabe 1.1 und Aufgabe 1.2. Als FIA, FIDP oder FIDV musst Du zur Teil-2-Prüfung Aufgaben auf Kernkompetenz-Ebene lösen können, das sind Aufgabe 2.1 und Aufgabe 2.2. Von FIA wird zusätzlich erwartet, dass ihnen Aufgaben auf Profilkompetenz-Ebene (Aufgabe 3.1 und Aufgabe 3.2) keine Probleme bereiten. Notiere Deine Antworten zuerst vollständig, bevor Du die Beispiellösung aufklappst. Code und Tests kannst Du anschließend im Projekt aus dem Repository mit dem Build ausführen; andere fachlich richtige Lösungen sind ebenso richtig.

Du möchtest eigene Aufgaben zum Üben von einem KI-Sprachmodell generieren lassen?

Kopiere den Text in ein KI-Sprachmodell Deiner Wahl und passe Kompetenzstufe, Anzahl, Bearbeitungszeit und Szenario bei Bedarf an. Wie die Aufgaben aufgebaut sind, beschreibt der Artikel Wie sind die Aufgaben aufgebaut?. Prüfe die Ergebnisse mit dem Lehrartikel: KI-Sprachmodelle machen Fehler.

Erstelle neue Übungsaufgaben zum Thema „REST-API und Fehlerbehandlung:
Gesuche über die API einstellen“.

Bauplan: Halte Dich an Aufbau, Kompetenzstufen, Operatoren,
Anforderungsbereiche, Punkte und Zeitschätzung, die im Artikel
https://oer-informatik.de/aufgaben-aufbau beschrieben sind.

Fachliche Grundlage:
- https://oer-informatik.de/spring-boot-rest-api-fehlerbehandlung
- https://oer-informatik.de/rest-grundlagen
- https://oer-informatik.de/spring-boot-anforderungen-api-entwurf
- Stilbeispiel: https://oer-informatik.de/spring-boot-rest-api-fehlerbehandlung-uebungsaufgaben

Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- REST-API
- Fehlerbehandlung
- Problem Details
- RFC 9457
- ProblemDetail
- RestControllerAdvice
- ExceptionHandler
- HTTP-Statuscode
- Mapper
- LazyInitializationException

Kompetenzstufe: Basiskompetenz (alternativ: Kernkompetenz, Profilkompetenz, Expertise)
Anzahl: 1 Aufgabe mit Teilaufgaben
Bearbeitungszeit in Minuten ca.: <5 bis 30 min>
Szenario: eines der Szenarien aus dem Bauplan

Vorgehen:
1. Lies zuerst die verlinkten Artikel. Kannst Du keine Webseiten
   abrufen, sag mir das, statt zu raten.
2. Steht bei einer Angabe noch ein Platzhalter in <…>, frag mich
   zuerst danach. Richte Punkte und Zahl der Teilaufgaben nach der
   Bearbeitungszeit (ca. 0,9 min je Punkt).
3. Gib je Aufgabe Titel mit Kompetenzstufe, Ausgangssituation und
   Teilaufgaben mit Operator und Punktzahl aus, dazu die geschätzte
   Bearbeitungszeit.
4. Gib die Beispiellösungen mit Punkteverteilung erst aus, wenn ich
   danach frage.

Aufgabe 1.1: Statuscodes für Gesuche zuordnen (Basiskompetenz)

Oksana Melnyk testet die neuen Endpunkte für Gesuche. Ordne jeder Anfrage den Statuscode zu, den das Backend nach dem Vertrag zurückgibt, und begründe kurz. Am Tag des Tests ist Dienstag, der 10.11.2026. (6 Punkte)

Anfragen an die Endpunkte für Gesuche
Nr. Anfrage Status Begründung
1 POST /v1/gesuche mit "zeitVon": "07:15" und "zeitBis": "06:45"
2 POST /v1/gesuche mit "mitfahrerId": 99
3 POST /v1/gesuche mit "zeitVon": "7:00"
4 POST /v1/gesuche mit "datum": "2026-11-09"
5 PATCH /v1/gesuche/1 mit {"status": "zurueckgezogen"}, nachdem Gesuch 1 schon zurückgezogen wurde
6 GET /v1/gesuche/2, während die Datenbank nicht erreichbar ist

Je Zeile mit Begründung 1 Punkt:

Statuscodes zu den Anfragen
Nr. Status Begründung
1 422 Das Format stimmt, aber die Fachregel zum Zeitfenster ist verletzt (zeitfenster-ungueltig).
2 404 Die Person mit dieser ID gibt es nicht (NichtGefundenException).
3 400 Die Uhrzeit passt nicht zum Muster HH:MM im Vertrag; Bean Validation lehnt den Body ab, bevor der Service läuft.
4 422 Ein Gesuch für einen vergangenen Tag verletzt eine Fachregel (gesuch-unzulaessig).
5 409 Die Anfrage ist zulässig, passt aber nicht zum Zustand: Das Gesuch ist nicht mehr offen (gesuch-nicht-offen).
6 500 Der Fehler liegt im Server; der Client kann ihn nicht beheben.

Beim Ausprobieren antwortet das Backend etwa bei Nr. 1 mit {"detail":"Das Zeitfenster muss vor seinem Ende beginnen.", …, "status":422,"title":"Fachregel verletzt","type":"https://mitfahrlabor.example/probleme/zeitfenster-ungueltig"}.

Aufgabe 1.2: Eine Fehlerantwort lesen (Basiskompetenz)

Ein Frontend erhält auf eine Anfrage diese Antwort:

HTTP/1.1 409
Content-Type: application/problem+json

{"detail":"Das Gesuch ist nicht mehr offen (Status ZURUECKGEZOGEN).","instance":"/v1/gesuche/1","status":409,"title":"Zustandskonflikt","type":"https://mitfahrlabor.example/probleme/gesuch-nicht-offen"}
  1. Nenne die Anfrage (Methode und Pfad), die zu dieser Antwort geführt hat, und erkläre, woran Du das erkennst. (1 Punkt)

  2. Das Frontend soll bei diesem Fehler den Hinweis „Dieses Gesuch wurde schon zurückgezogen.“ anzeigen. Begründe, welches Feld es dafür auswerten sollte. (2 Punkte)

  3. Ein Teammitglied schlägt vor, bei Fehlern den Stacktrace in detail mitzuschicken, „damit man Fehler schneller findet“. Beurteile den Vorschlag. (1 Punkt)

  1. PATCH /v1/gesuche/1: instance enthält den Pfad der Anfrage, und nur das Zurückziehen (PATCH) kann einen Zustandskonflikt beim Gesuch auslösen (1 Punkt).

  2. Das Feld type (…/gesuch-nicht-offen), zusammen mit status (1 Punkt). type ist ein fester Bezeichner der Problemart, während detail ein für Menschen gedachter Text ist, der sich jederzeit ändern kann, etwa durch eine neue Formulierung oder Übersetzung (1 Punkt).

  3. Abzulehnen: Ein Stacktrace verrät Interna (Klassen, Pakete, Bibliotheken, Versionen, ggf. SQL) und erleichtert Angriffe. Er gehört ins Log des Servers; die Antwort enthält nur, was der Client zum Beheben braucht (1 Punkt).

Aufgabe 2.1: Einen GesuchMapper schreiben (Kernkompetenz)

Das aus dem Vertrag erzeugte Modell api.model.Gesuch hat einen Konstruktor mit allen Pflichtfeldern in dieser Reihenfolge:

public Gesuch(Long mitfahrerId, Long haltepunktId, Richtung richtung, LocalDate datum, String zeitVon,
        String zeitBis, Long id, GesuchStatus status)

GesuchNeu hat die Getter getMitfahrerId(), getHaltepunktId(), getRichtung(), getDatum(), getZeitVon() und getZeitBis(); die Zeiten sind Strings im Format HH:MM.

Schreibe die Klasse GesuchMapper im Paket web nach dem Muster des AngebotMapper mit den Methoden zuApi (Entity Gesuch → api.model.Gesuch) und zuNeuemGesuch (GesuchNeu → NeuesGesuch). Importe musst Du nicht notieren. (7 Punkte)

final class GesuchMapper {

    private GesuchMapper() {
    }

    static Gesuch zuApi(de.dbh.mitfahrlabor.domain.Gesuch g) {
        return new Gesuch(g.getNutzer().getId(), g.getHaltepunkt().getId(),
                Richtung.valueOf(g.getRichtung().name()), g.getDatum(),
                g.getZeitVon().toString(), g.getZeitBis().toString(),
                g.getId(), GesuchStatus.valueOf(g.getStatus().name()));
    }

    static NeuesGesuch zuNeuemGesuch(GesuchNeu neu) {
        return new NeuesGesuch(neu.getMitfahrerId(), neu.getHaltepunktId(),
                AngebotMapper.zuFahrtrichtung(neu.getRichtung()), neu.getDatum(),
                LocalTime.parse(neu.getZeitVon()), LocalTime.parse(neu.getZeitBis()));
    }
}

Punkte: Klasse final mit privatem Konstruktor und statischen Methoden, ohne public (1), voller Paketname für eine der beiden Klassen Gesuch (1), IDs von Nutzer und Haltepunkt (1), Enums über valueOf(….name()) für Richtung und Status (2), Zeiten mit toString() bzw. LocalTime.parse (1), mitfahrerId → nutzerId in der richtigen Reihenfolge des Records (1). Fahrtrichtung.valueOf(neu.getRichtung().name()) statt AngebotMapper.zuFahrtrichtung ist ebenso richtig. LocalTime.toString() liefert bei Zeiten ohne Sekunden genau HH:MM.

Aufgabe 2.2: Einen GesuchController schreiben (Kernkompetenz)

  1. Schreibe die Klasse GesuchController, die die erzeugte Schnittstelle GesucheApi implementiert. Sie soll Gesuche einstellen (201 mit Location-Header /v1/gesuche/{id}), abrufen (unbekannte ID: 404 mit Problem Details) und zurückziehen. Die Methoden der Schnittstelle heißen wie die operationId; aendereGesuch bekommt die ID und ein GesuchStatusAenderung. Importe musst Du nicht notieren. (6 Punkte)

  2. Begründe, warum die Klasse ProblemBehandlung für die neuen Endpunkte nicht geändert werden muss, obwohl drei neue fachliche Ausnahmen dazukommen. (2 Punkte)

@RestController
public class GesuchController implements GesucheApi {

    private final GesuchService gesuchService;

    public GesuchController(GesuchService gesuchService) {
        this.gesuchService = gesuchService;
    }

    @Override
    public ResponseEntity<Gesuch> stelleGesuchEin(GesuchNeu neu) {
        var gesuch = gesuchService.stelleEin(GesuchMapper.zuNeuemGesuch(neu));
        return ResponseEntity.created(URI.create("/v1/gesuche/" + gesuch.getId()))
                .body(GesuchMapper.zuApi(gesuch));
    }

    @Override
    public ResponseEntity<Gesuch> holeGesuch(Long gesuchId) {
        return gesuchService.finde(gesuchId)
                .map(GesuchMapper::zuApi)
                .map(ResponseEntity::ok)
                .orElseThrow(() -> new NichtGefundenException("Gesuch", gesuchId));
    }

    @Override
    public ResponseEntity<Gesuch> aendereGesuch(Long gesuchId, GesuchStatusAenderung aenderung) {
        return ResponseEntity.ok(GesuchMapper.zuApi(gesuchService.zieheZurueck(gesuchId)));
    }
}

Punkte: @RestController, implements GesucheApi, Konstruktorinjektion (1), Einstellen mit Mapper und created(URI…) (2), Abrufen mit orElseThrow und NichtGefundenException (2), Zurückziehen mit ok (1). Weil GesuchStatusAenderung laut Vertrag nur zurueckgezogen erlaubt, muss der Controller den Wert nicht prüfen; jeder andere Wert endet schon beim Lesen des JSON mit 400.

Ausprobiert mit dem Projekt: POST /v1/gesuche für Oksana am 24.11.2026 ergibt 201 mit Location: /v1/gesuche/4, GET /v1/gesuche/99 ergibt 404 mit "detail":"Gesuch 99 existiert nicht.".

  1. Die ProblemBehandlung reagiert auf die Oberklassen NichtGefundenException, FachregelVerletztException und ZustandskonfliktException. Die neuen Ausnahmen erben davon und werden deshalb automatisch mit 422 bzw. 409 beantwortet (1 Punkt). Der type entsteht aus dem Klassennamen, z. B. zeitfenster-ungueltig und gesuch-nicht-offen; dafür ist ebenfalls nichts zu ändern (1 Punkt).

Aufgabe 3.1: Der Flurmonitor zeigt Gesuche und meldet 500 (Profilkompetenz)

Ein Teammitglied erweitert den Flurmonitor um offene Gesuche. Im GesuchRepository kommt eine abgeleitete Abfragemethode hinzu, der MonitorService bekommt eine neue Methode, und der MonitorMapper wandelt Gesuche um:

    // GesuchRepository
    List<Gesuch> findByStatusAndDatumGreaterThanEqualOrderByDatumAscZeitVonAsc(Gesuchstatus status,
            LocalDate ab, Limit limit);

    // MonitorService (Klasse mit @Transactional(readOnly = true))
    public List<Gesuch> naechsteGesuche(int max) {
        return gesuchRepository.findByStatusAndDatumGreaterThanEqualOrderByDatumAscZeitVonAsc(Gesuchstatus.OFFEN,
                LocalDate.now(uhr), Limit.of(max));
    }

    // MonitorMapper
    static MonitorEintrag zuEintrag(de.dbh.mitfahrlabor.domain.Gesuch g) {
        return new MonitorEintrag(MonitorEintrag.ArtEnum.GESUCH, g.getHaltepunkt().getOrt(),
                g.getHaltepunkt().getName(), Richtung.valueOf(g.getRichtung().name()), g.getDatum(),
                g.getZeitVon().toString(), 1);
    }

Der MonitorController ruft monitorService.naechsteGesuche(max) auf und wandelt das Ergebnis mit MonitorMapper::zuEintrag um. Alle Tests sind grün, doch GET /v1/monitor/eintraege antwortet mit 500. Im Log steht:

org.hibernate.LazyInitializationException: Could not initialize proxy [de.dbh.mitfahrlabor.domain.Haltepunkt#1] - no session
  1. Erkläre, wie es zu der Ausnahme kommt. (2 Punkte)

  2. Behebe den Fehler im GesuchRepository. (2 Punkte)

  3. Ein anderes Teammitglied schlägt vor, einfach spring.jpa.open-in-view=true zu setzen; das funktioniert tatsächlich. Bewerte den Vorschlag. (2 Punkte)

  1. Gesuch.haltepunkt ist LAZY verknüpft; die Abfrage lädt nur die Gesuche, der Haltepunkt ist ein Platzhalter (Proxy). Die Transaktion des Service endet mit der Rückkehr von naechsteGesuche. Der Mapper ruft getOrt() erst im Controller auf; weil open-in-view ausgeschaltet ist, gibt es keine offene Session mehr, und der Platzhalter kann nicht nachladen (2 Punkte; 1 Punkt für LAZY/Platzhalter, 1 Punkt für das Ende der Transaktion vor dem Zugriff).

  2. Die Haltepunkte per join fetch gleich mitladen, z. B. mit einer eigenen Abfrage (2 Punkte):

    @Query("""
            select g from Gesuch g join fetch g.haltepunkt
            where g.status = :status and g.datum >= :ab
            order by g.datum, g.zeitVon
            """)
    List<Gesuch> findeFuerMonitor(@Param("status") Gesuchstatus status, @Param("ab") LocalDate ab, Limit limit);

Danach liefert der Monitor die drei Gesuche von Oksana für den 23.11.2026 mit Ort und Treffpunkt, und das SQL enthält join haltepunkt. Eine Lösung, die im Service (innerhalb der Transaktion) schon in Monitor-Einträge umwandelt, behebt den Fehler ebenfalls, vermischt aber Web-Modell und Service-Schicht. @EntityGraph(attributePaths = "haltepunkt") an der abgeleiteten Methode ist ebenso richtig.

  1. Mit open-in-view=true bleibt die Session bis zum Ende der Anfrage offen, und Hibernate lädt jeden Haltepunkt beim ersten Zugriff einzeln nach. Im Projekt entstehen dadurch neben der Abfrage der Gesuche zwei zusätzliche select … from haltepunkt … where h1_0.id=?, eine je Haltepunkt (N+1-Problem). Außerdem hält jede Anfrage länger eine Datenbankverbindung, und Datenbankzugriffe passieren unbemerkt in der Web-Schicht. Der Fehler wird so versteckt statt behoben; die Einstellung bleibt deshalb aus (2 Punkte).

Aufgabe 3.2: Der Statuscode in der Ausnahme? (Profilkompetenz)

Ein Teammitglied findet drei @ExceptionHandler-Methoden umständlich und schlägt vor:

public abstract class MitfahrlaborException extends RuntimeException {

    protected MitfahrlaborException(String meldung) {
        super(meldung);
    }

    /** Jede Ausnahme kennt ihren HTTP-Statuscode selbst. */
    public abstract HttpStatus httpStatus();
}

Die ProblemBehandlung hätte dann nur noch eine Methode @ExceptionHandler ProblemDetail behandle(MitfahrlaborException e), die e.httpStatus() verwendet.

  1. Nenne einen Vorteil des Vorschlags. (1 Punkt)

  2. Bewerte den Vorschlag im Hinblick auf die Schichten der Anwendung. (2 Punkte)

  3. Eine neue Ausnahme GesuchAbgelaufenException soll entstehen. Erkläre, wie die bestehende Lösung mit drei Oberklassen dafür sorgt, dass sie den richtigen Statuscode bekommt, und was Entwickler*innen dafür entscheiden müssen. (2 Punkte)

  1. Eine einzige Behandlungsmethode; jede Ausnahme legt ihren Code an einer Stelle selbst fest, auch für Sonderfälle, die in keine der drei Gruppen passen (1 Punkt).

  2. Die Domäne soll nichts von HTTP wissen (Tabelle der Schichten aus Artikel 08). HttpStatus ist eine Klasse von Spring Web; das Paket domain hinge dann von der Web-Schicht ab, und eine zweite Oberfläche (etwa Thymeleaf oder ein Kommandozeilenprogramm) hätte HTTP-Codes in ihren Fachklassen, die sie nicht braucht (1 Punkt). Die bestehende Lösung hält die Übersetzung in der Web-Schicht: Die Domäne sagt nur, welche Art Fehler vorliegt (Fachregel, Zustandskonflikt, nicht gefunden), die Web-Schicht entscheidet, was das in HTTP bedeutet. Der Vorschlag ist deshalb abzulehnen (1 Punkt). Dasselbe gilt für @ResponseStatus an den Ausnahmeklassen.

  3. Die neue Klasse muss von einer der drei Oberklassen erben; die ProblemBehandlung ordnet ihr dann über den Typ automatisch den Code zu (1 Punkt). Zu entscheiden ist, ob ein abgelaufenes Gesuch eine Fachregel verletzt (422) oder ein Zustandskonflikt ist (409). Weil eine Aktion auf einem abgelaufenen Gesuch nicht zum Zustand passt und das Gesuch nicht wieder offen wird, passt ZustandskonfliktException wie bei GesuchNichtOffenException (1 Punkt; eine andere begründete Zuordnung ist ebenso richtig).


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Übungsaufgaben zu REST-API und Fehlerbehandlung: Gesuche über die API einstellen” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/spring-boot-rest-api-fehlerbehandlung-uebungsaufgaben veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/java-springboot/Backend. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 11.11.2026.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: