CodeGym /Kurse /Spring REST & MVC /Evolution von API und OpenAPI

Evolution von API und OpenAPI

Spring REST & MVC
Level 29, Lektion 0
Verfügbar

1. API als langfristiger Vertrag

Fast jeder angehende Backend-Entwickler hat den naiven Gedanken: „Solange der Client noch nicht da ist, kann man beliebig ändern, später wird alles behoben.“ Das klingt logisch, bis Sie selbst zum Client Ihres eigenen API werden, durch .http-Anfragen, Swagger UI, Frontend, mobile App oder die Integration eines Kollegen aus dem Nachbarteam. Der Vertrag beginnt zu leben, bevor Sie sagen können: „Das ist doch nur ein Übungsprojekt.“

Der wichtigste Gedanke dieser Vorlesung: API lebt länger als Ihr aktueller Code. Controller werden umgeschrieben, Services umbenannt, Repository wandert im nächsten Kurs von In-Memory zu JPA – das ist normal. Aber die externen Erwartungen der Clients (welche URL aufrufen, welche Parameter übergeben, welche Felder in JSON erwarten, welche Status und Fehler erhalten) ändern sich schmerzhaft und selten „ohne Folgen“.

Stellen Sie sich API wie eine Speisekarte im Café vor. Die Küche ist Ihr Code: Sie können Köche tauschen, die Pfanne ersetzen, sogar in einen anderen Raum umziehen, solange Geschmack und Zusammensetzung der Gerichte auf der Karte gleich bleiben. Dem Client ist das egal. Aber wenn Sie „Borschtsch“ in „Suppe Nr. 7“ umbenennen und zusätzlich das Rezept so ändern, dass jetzt Banane drin ist, wird der Client es bemerken. Und er hätte recht.

Deshalb fixieren wir heute den „Standpunkt des Clients“: was genau er bemerken kann, selbst wenn Sie eine „kleine Änderung“ im Code vorgenommen haben. In der nächsten Vorlesung lernen wir dann, Änderungen als sicher oder brechend zu klassifizieren, aber zuerst müssen wir verstehen, was überhaupt ein Vertrag ist.

2. Vertragsoberfläche: HTTP statt Java

Wenn Sie das Projekt von innen betrachten, sehen Sie Klassen, Pakete, Services, Mapper und eine „schöne Architektur“. Der Client sieht das gar nicht. Er sieht HTTP: Pfad, Methode, Parameter, Header, Request- und Response-Body, Statuscodes und Content-Type. Wenn der Client auf Codeebene integriert ist (z. B. Frontend oder ein anderer Dienst), sieht er auch die JSON-Struktur und die Enum-Werte – und damit ist Schluss, die „Spring-Magie“ bleibt hinter den Kulissen.

Es ist praktisch, ein einfaches Schema im Kopf zu haben: Die Vertragsgrenze verläuft entlang der HTTP-Linie. Alles „auf dieser Seite“ sind Ihre internen Details, alles „auf der anderen Seite“ sind Verpflichtungen gegenüber dem Client.

flowchart TD
  %% Vertragsgrenze – HTTP zwischen Client und Ihrer Anwendung
  Client["API-Client<br/>frontend / mobile / integration"] -->|HTTP request| MVC[Spring MVC]
  MVC --> Controller[Controller]
  Controller --> Service[Service]
  Service --> Repo[In-memory repo]
  Service --> Storage[File storage]
  Repo --> Service
  Storage --> Service
  Service --> Controller
  Controller -->|HTTP response| Client

Was tatsächlich „der Vertrag ist“, lässt sich in einer Tabelle darstellen. Sie ist nicht perfekt, heilt aber hervorragend von der Illusion „Vertrag = nur JSON“.

Was der Client sieht Wie es in unserem Spring MVC-Code ausgedrückt ist Warum dies Teil des Vertrags ist
URI (z. B. /api/v1/tasks/{taskId}) @RequestMapping, @GetMapping und dergleichen Das ist die „Adresse“ der Ressource. Wenn die Adresse wechselt, kann der Client nicht zugreifen.
HTTP-Methode (GET/POST/PUT/PATCH/DELETE) Mapping-Annotationen (@GetMapping, @PostMapping…) Die Methode trägt Semantik. Der Client kann GET cachen, PUT wiederholen, bei DELETE auf 204 warten.
Query-Parameter (page, size, status, q…) @RequestParam, @ModelAttribute Das ist Teil des Eingabevertrags. Name und Bedeutung des Parameters sind ein Versprechen.
Header (Content-Type, Accept, Location, Download-Header) consumes/produces, ResponseEntity, HttpHeaders Header steuern das Format und das Verhalten des Clients (insbesondere bei Dateien und 201 Created).
Request body JSON @RequestBody, request DTO Der Client baut den Payload strikt nach der Form. Jede „Kleinigkeit“ fällt hier auf.
Response JSON response DTO, Jackson-Regeln Der Client parst die Antwort. Wenn ein Feld verschwindet/umbenannt wird – Hallo, Bug.
Status codes (200/201/204/400/404/409/415/500) ResponseEntity, global exception handling Der Client reagiert auf den Status, manchmal sogar ohne Body zu lesen.
Error payload ProblemDetail + Erweiterungen (code, fieldErrors) Fehler sind auch Vertrag. Der Client schreibt oft seine Logik genau darauf.
Multipart parts (file, metadata) @RequestPart / MultipartFile Das sind die „Feldnamen“ der Multipart-Anfrage. Wenn man sie ändert, bricht der Client genauso wie beim Umbenennen eines JSON-Feldes.

Beachten Sie, dass es hier überhaupt keine Zeile „Methodenname im Controller“ oder „Paketstruktur“ gibt. Dem Client ist es egal, wie Sie die Methode getById() genannt haben und in welchem Paket sie liegt. Aber dem Client ist es nicht egal, dass der Pfad jetzt ein anderer ist oder dass status nicht mehr DONE ist, sondern COMPLETED.

3. Vertragsdaten: URI, Query, DTO und JSON

Es ist sehr einfach, den Vertrag des API auf „welches JSON wir liefern“ zu reduzieren. Und ja, JSON ist der wichtigste Teil des Vertrags in unserem Kurs. Aber er ist immer an den restlichen HTTP-Kontext gebunden: an URIs, Methoden, Status und sogar daran, was wir genau als „leeres Ergebnis“ für List-Endpunkte betrachten. Daher betrachten wir hier die Daten als eine Kombination: „wohin und wie der Client zugreift“ plus „welche Datenform er senden/empfangen muss“.

Beginnen wir mit einer erfreulichen Tatsache: interne Namen im Domain-Modell können ihr eigenes Leben führen, solange DTO und Mapping den Vertrag stabil halten. Das ist einer der Gründe, warum wir überhaupt DTOs gemacht haben, anstatt Task „wie es ist“ zu serialisieren.

Minibeispiel: Ein internes Feld wurde umbenannt (oder hieß historisch so), aber der externe Vertrag bleibt gleich.

public class Task {
    // Der interne Feldname kann beliebig sein – der Client sieht ihn nicht
    private String ownerName;

    // Der interne Getter ist auch kein Teil des HTTP-Vertrags
    public String getOwnerName() { return ownerName; }
}

// Der externe Vertrag wird durch das DTO definiert: seine Felder werden zu Schlüsseln in JSON
public record TaskSummaryResponse(String assigneeName) {}

Wenn Ihr Mapper assigneeName liefert, muss der Client gar nicht wissen, dass Sie es intern ownerName nennen. Das ist eine gewöhnliche Refactoring „in der Küche“, das „die Speisekarte“ nicht ändert.

Dann beginnen die „rutschigeren“ Zonen, wo naives Refactoring zu einer externen Änderung wird. Ein Feld im Response-DTO umzubenennen, ist keine interne Angelegenheit mehr, denn der Feldname = der Schlüssel in JSON. Den Typ eines Feldes zu ändern, fällt ebenfalls auf: aus einer Zahl wird ein String, und der Client stürzt bereits beim Parsen ab. Das Ändern des Datums-/Zeitformats ist ein Klassiker: Der Server gibt freudig „wie es gerade kommt“ zurück, und der Client ist dann traurig, weil ISO-8601 plötzlich zu „21.03.2026 18:00“ geworden ist.

Und es gibt noch eine Art von Änderungen, die meistens als „Kleinigkeit“ angesehen werden, die der Client aber trotzdem bemerkt: das Hinzufügen eines neuen Feldes in der Antwort. Normalerweise ist das eine Erweiterung, kein Bruch (wir werden das in der nächsten Vorlesung detailliert besprechen), aber der Client wird diese Änderung in der Dokumentation, in Logs und Beispielen sehen – und das ist in Ordnung. Wichtig ist, dass Sie verstehen: der externe Vertrag hat sich geändert, nur nicht immer „schmerzhaft“.

So sieht ein typischer „erweiternder“ Schritt im Response-DTO aus:

import java.time.Instant;

public record TaskDetailsResponse(
        String id,
        String title,
        String status,
        Instant updatedAt,
        // Neues Feld in der Antwort: alte Clients können es normalerweise ignorieren,
        // neue Clients können es nutzen (z. B. für ein Badge mit der Anzahl der Kommentare).
        Integer commentCount
) {}

Wenn commentCount in der Antwort erscheint, kann ein Client, der es nicht kennt, das Feld normalerweise ignorieren. Aber ein Client, der den Kommentar-Button „färben“ möchte, kann dies endlich ohne eine separate Anfrage tun. Der Vertrag ist also reicher geworden – auch das ist eine Änderung, nur eine nützliche.

Eine weitere wichtige Vertragsschicht sind die Query-Parameter und ihre Bedeutung. Wir haben page, size, sort, Filter nach status/priority/tag, Textsuche q und Datumsbereiche. Die Namen dieser Parameter und die Standardregeln sind Teil des Versprechens. Wenn Sie die Standard-Sortierung von updatedAt,desc auf title,asc ändern, wird der Client es bemerken: Die Ergebnisse werden „andere“ sein, auch wenn sich das JSON-Format nicht geändert hat. Dies ist der Fall, wenn das Schema sich nicht geändert hat, aber das Verhalten sich geändert hat, und für den Client kann das ein echtes Problem sein.

4. Fehlervertrag: ProblemDetail und Codes

Normalerweise denkt man beim Vertrag an den Happy Path: „hier Anfrage, hier Antwort, alles schön“. Aber das reale Leben einer API ist eine Mischung aus Erfolgen und Fehlern. Manchmal scheint es sogar, dass es mehr Fehler gibt: ungültige UUID, ungültiger Payload, Statuskonflikt, nicht unterstützter Dateityp. Und wenn die Fehler unvorhersehbar sind, muss der Client entweder einen „Spaghetti“-Handler schreiben oder dem Benutzer bei jeder Situation einfach „etwas ist schiefgelaufen“ anzeigen. In beiden Fällen gewinnen Sie nicht.

Wir haben bereits einen einheitlichen Fehlervertrag um ProblemDetail herum gebaut und dort anwendungsspezifische code und, wo nötig, fieldErrors hinzugefügt. Jetzt ist es wichtig zu erkennen: Das ist keine „interne Implementierung“, das ist ein öffentlicher Teil der API. Der Client kann sich auf status für die UX (z. B. „nicht gefunden“ oder „Konflikt“ anzeigen), auf code für die maschinelle Logik (z. B. eine bestimmte Ursache hervorheben) und auf fieldErrors verlassen, um dem Benutzer zu sagen, was genau zu korrigieren ist.

Wenn man es formalisiert, ist es für den Client nützlich, wenn der Fehler-Payload stabil in der Struktur ist. Etwa so (ohne Bindung an eine bestimmte Klasse): type, title, status, detail, instance als Standardfelder von ProblemDetail, plus unser code, plus optionale Erweiterungen wie timestamp und fieldErrors. Auch wenn ein Mensch den Fehler mit den Augen liest, liest der Client-Code normalerweise status und code, während er die Textfelder (title/detail) als „menschlichen Hinweis“ verwendet.

Genau deshalb ist eine „kleine“ Änderung wie die Umbenennung eines Fehlercodes keine Kosmetik. Wenn der Client gestern TASK_NOT_FOUND verarbeitet hat und Sie heute entschieden haben, dass TASK_MISSING „schöner klingt“, wird der Client die Situation nicht mehr erkennen. Es ist immer noch 404, aber die Geschäftslogik auf Clientseite (oder Integration) kann an den code gebunden sein und wird brechen.

Das Gleiche gilt für die Struktur von fieldErrors. Wenn der Client erwartet, dass dort Schlüssel wie title oder tags[0] vorhanden sind, und Sie plötzlich das Format auf „menschliche“ Meldungen ohne Pfade geändert haben, haben Sie dem Client die Möglichkeit genommen, den Fehler genau dem Formularfeld zuzuordnen. Und ja, das ist auch eine Vertragsänderung, selbst wenn der Server-Code „einfacher geworden ist“.

Und noch eine Nuance: Fehler bei Multipart und Download sind ebenfalls Teil des Fehlervertrags. Wenn der Upload-Endpunkt früher 415 mit code=UNSUPPORTED_MEDIA_TYPE zurückgab und Sie jetzt 400 INVALID_INPUT zurückgeben, „weil es einfacher ist“, wird der Client es bemerken. Er kann unterschiedliche Meldungen an den Benutzer zeigen oder zum Beispiel automatisch ein anderes Dateiformat nur bei 415 vorschlagen.

5. Dateivertrag: Multipart und Download

Datei-Endpunkte werden oft als nebensächlich angesehen: „Das ist doch nur ein MultipartFile, was gibt es da zu dokumentieren und als Vertrag zu betrachten.“ In der Praxis ist dies eines der vertragsempfindlichsten Szenarien, weil es sich stark von normalem JSON unterscheidet: anderer Content-Type, andere Art der Datenübertragung, manchmal andere Clients (Browser, mobile App, Integration). Und wenn Sie hier „ein bisschen ändern“, stürzt der Client mit besonders rätselhaftem Gesichtsausdruck ab.

Bei uns sieht der Upload als multipart/form-data mit zwei Teilen aus: einer Binärdatei und JSON-Metadaten. Das wichtigste Detail: Die Namen der Teile (file, metadata) sind genau derselbe Vertrag wie die Namen von JSON-Feldern. Wenn Sie den Teil metadata in meta umbenennen, wird der alte Client weiterhin metadata senden, und der Server wird plötzlich mit einem Fehler „missing part“ antworten. Im Projekt haben Sie eine Zeile geändert, aber außerhalb funktioniert das Schlüsselszenario des Clients nicht mehr.

Ein Minibeispiel, das sehr anschaulich die „Vertraglichkeit“ von Multipart zeigt: Direkt in der Signatur ist ersichtlich, dass der Client einen Part mit einem bestimmten Namen senden muss.

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;

@PostMapping(
        path = "/api/v1/tasks/{taskId}/attachments",
        // Wichtig: Der Client muss genau multipart/form-data senden, sonst erhält er 415/400 je nach Verarbeitung
        consumes = MediaType.MULTIPART_FORM_DATA_VALUE
)
public void upload(
        // Wichtig: Der Part-Name ist Teil des öffentlichen Vertrags ("file")
        @RequestPart("file") MultipartFile file,
        // Wichtig: Der Part-Name ist Teil des öffentlichen Vertrags ("metadata")
        @RequestPart("metadata") AttachmentUploadMetadataRequest metadata
) {
    // Hier kann jede interne Implementierung stehen – der Vertrag wird durch die HTTP-Signatur oben definiert
}

Auch wenn Sie nichts über Spring wissen, können Sie den Vertrag bereits „lesen“: Multipart-Anfrage, zwei Teile erforderlich, Namen festgelegt.

Auch das Download-Szenario ist vertraglich. Es gibt kein JSON, dafür Header. Content-Type sagt dem Client, um welche Daten es sich handelt, Content-Disposition sagt, wie die Datei zu benennen ist und ob sie heruntergeladen werden soll, anstatt versucht zu werden, sie als Seite zu öffnen. Wenn Sie die Angabe von Content-Disposition einstellen, könnte der Browser anfangen, PDFs in einem Tab zu öffnen, statt sie herunterzuladen, und der Benutzer sagt: „Das Herunterladen ist kaputt.“ Technisch hat der Server „die Datei geliefert“, aber der Verhaltensvertrag für den Client hat sich geändert.

Es gibt auch „Kleinigkeiten“, die keine Kleinigkeiten sind: Wenn Sie den Fallback-Content-Type von application/octet-stream auf text/plain geändert haben, könnte der Client beginnen, binäre Daten falsch zu verarbeiten. Oder wenn Sie den Endpunkt von /download auf /content aus ästhetischen Gründen geändert haben – der Client wird es bemerken, weil die URL sich geändert hat.

6. Refactoring und Änderung des API: Wie überprüfen?

Der gefährlichste Moment ist, wenn Sie aufrichtig glauben, ein „rein internes Refactoring“ gemacht zu haben, und der Client plötzlich bricht. Normalerweise passiert das, wenn Sie die Änderungen aus der Serverperspektive betrachten: „Ich habe doch nur umbenannt“, „Ich habe nur den Standard geändert“, „Ich habe doch nur ein Feld entfernt, es war überflüssig“. Der Client sieht es anders: „Meine Anfrage war so – jetzt funktioniert sie nicht“ oder „Die Antwort war so – jetzt kann ich sie nicht parsen“.

Der einfachste praktische Test klingt fast albern, aber er funktioniert: Wenn Sie nach der Änderung Ihre .http-Anfragen (oder den Frontend-Code des Clients) korrigieren müssen, dann hat sich der Vertrag geändert. Und es spielt keine Rolle, dass Sie „nichts dergleichen“ gemacht haben – dem Client ist das egal.

Um sich leichter zu ertappen, ist eine Tabelle „Änderung → bemerkt der Client?“ nützlich. Hier geht es nicht um das Thema safe/breaking (das kommt in der nächsten Vorlesung), hier nur die Tatsache der Wahrnehmbarkeit.

Änderung im Code Bemerkt der Client? Warum
Methode TaskService.getById() in findById() umbenannt Nein Das geht nicht nach außen; HTTP hat sich nicht geändert.
Mapper-Klasse in ein anderes Paket verschoben Nein Der Client sieht keine Java-Pakete.
Feld im Domain-Modell ownerName -> assigneeName umbenannt, aber DTO nicht geändert Nein DTO und JSON sind gleich geblieben.
@RequestMapping("/api/v1/tasks") auf "/api/v1/work-items" geändert Ja Die URL der Ressource hat sich geändert – der Client wird ins Leere klopfen.
Query-Parameter q in query umbenannt Ja Der alte Client sendet weiterhin q, aber der Server filtert nicht mehr.
JSON-Feld title in taskTitle im Response-DTO umbenannt Ja Der Client parst nach Schlüsseln; der Schlüssel ist verschwunden.
HTTP-Status für Löschen von 204 auf 200 geändert und Body zurückgegeben Ja Der Client erwartet möglicherweise einen leeren Body und einen anderen Status (insbesondere in Tests/SDK).
Fehler-code von TASK_NOT_FOUND auf TASK_MISSING geändert Ja Die Client-Logik basierend auf Codes funktioniert nicht mehr vorhersagbar.
Multipart-Part metadata in meta umbenannt Ja Der Client sendet den alten Part-Namen – der Server sieht die Daten nicht.
Standardsortierung in GET /tasks geändert Ja Die gleiche Anfrage beginnt ein anderes Ergebnis zu liefern, obwohl das Antwortschema gleich ist.

Und genau hier taucht der „tückische“ Gedanke auf: Der Vertrag ist nicht nur die Struktur des DTO, sondern auch das Verhalten. Selbst wenn das JSON identisch ist, aber „dieselbe Anfrage“ sich anders verhält, wird der Client es spüren. Manchmal sofort (Tests fallen), manchmal nach einer Woche (der Benutzer sagte, dass es früher bequemer war).

Abschließend verfestigen wir das offensichtlichste Beispiel einer externen Änderung: den Ressourcenpfad. Der Code im Controller kann leer sein, aber der Client ist bereits gebrochen, weil sich die Adresse geändert hat.

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
// Wichtig: Der URI der Ressource ist Teil des Vertrags; sein Wechsel bricht Clients
@RequestMapping("/api/v1/work-items") // war: /api/v1/tasks
class TaskController {
    // Das Innere des Controllers kann sich ändern, aber der URI ist das, was der Client sieht
}

Für den Server sieht das aus wie „Ressource umbenannt, kann passieren“. Für den Client sieht das aus wie „Anwendung gestorben“. Und genau deshalb haben wir die URIs bereits ganz am Anfang des Kurses so pedantisch festgelegt.

Mit diesem Verständnis sind wir bereit für die nächste Vorlesung, in der wir zwischen Änderungen unterscheiden werden, die den Vertrag relativ sicher erweitern, und Änderungen, die Clients fast garantiert brechen. Aber das wird der nächste Schritt sein; jetzt ist es wichtig, dass Sie gelernt haben, die Vertragsoberfläche als Ganzes zu sehen.

7. Typische Fehler bei der Evolution des API

Fehler in diesem Thema entstehen normalerweise nicht aus böser Absicht, sondern aufgrund eines „Tunnelblicks“: Sie betrachten die API als Serverentwickler, nicht als Cliententwickler. Aus diesem Grund treffen Sie Entscheidungen „nach der Schönheit des Codes“, und die Konsequenzen zeigen sich nur von außen – im Frontend, in der Integration oder sogar in Ihren eigenen .http-Szenarien, die plötzlich nicht mehr funktionieren.

Fehler Nr. 1: Nur JSON als Vertrag betrachten und URI, Methoden, Status und Header ignorieren.
Wenn man denkt „Vertrag = DTO“, kann man den Client sehr leicht durch eine Statusänderung (z. B. 204 auf 200), Verschieben der Ressource auf einen anderen Pfad, Ändern von Content-Type oder Verschwinden von Location bei 201 Created brechen. Der Client lebt in der HTTP-Welt, und all das sind für ihn genauso wichtige Signale wie JSON-Felder.

Fehler Nr. 2: Internes Refactoring und externe Änderung verwechseln, indem man sich auf IDE und Compiler verlässt.
Die IDE freut sich: alles kompiliert, Server-Tests sind grün. Aber dem Client hilft das nicht. Die richtige Prüfung ist hier nicht „hat es kompiliert“, sondern „könnte der alte Client dieselbe Anfrage senden und die erwartete Antwort erhalten“. Manchmal reicht es, einfach die alten .http-Anfragen zu öffnen und sie ehrlich auszuprobieren, bevor man sagt „nichts hat sich geändert“.

Fehler Nr. 3: Die Bedeutung eines Feldes ändern, ohne seinen Namen zu ändern, und dies für „sicher“ halten.
Zum Beispiel bedeutete das Feld archived früher „Status ARCHIVED“ und jetzt bedeutet es „einfach aus der Liste ausgeblendet“. Formal ist der JSON-Schlüssel derselbe, aber die Semantik ist eine andere, und der Client, der sein Verhalten auf die alte Bedeutung aufgebaut hat, wird sich seltsam verhalten. Der Vertrag ist nicht nur die Form, sondern auch die Bedeutung.

Fehler Nr. 4: Den Fehlervertrag als „inneres Detail“ behandeln und code oder Format von fieldErrors ohne Bedenken umbenennen.
Fehler sind das, worauf der Client Verzweigungen schreibt. Wenn Sie den code geändert, fieldErrors entfernt oder begonnen haben, unterschiedliche Formate für verschiedene Controller zurückzugeben, wird der Client entweder aufhören, Situationen zu unterscheiden, oder anfangen, Workarounds zu schreiben. Am Ende leidet nicht nur der Client, sondern auch Ihre API: Sie wird schwieriger zu testen und zu dokumentieren.

Fehler Nr. 5: Multipart und Download als „Sonderfall“ betrachten, der nicht dieselbe Disziplin wie JSON erfordert.
Im Multipart-Vertrag gibt es eigene „Feldnamen“ (Parts), eigene erforderliche Elemente, eigene Media Types und eigene Fehler (415, fehlender Part). Im Download-Vertrag gibt es Header, die das Verhalten des Clients steuern. Wenn man das als „ach, das wird schon irgendwie“ betrachtet, werden Sie mit ziemlicher Sicherheit ein „irgendwie ist es kaputtgegangen“ bekommen.

1
Aufgabe
Spring REST & MVC, Level 29, Lektion 0
Gesperrt
Stabiler JSON-Vertrag bei internem Modell
Stabiler JSON-Vertrag bei internem Modell
1
Aufgabe
Spring REST & MVC, Level 29, Lektion 0
Gesperrt
Download-Endpoint als Teil des Vertrags
Download-Endpoint als Teil des Vertrags
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION