Zum Inhalt springen

5.2 HTTP: fetch, REST und der Schnittstellenvertrag

Zu Zen-Modus wechseln

Nach dieser Einheit kannst du:

  • Anfragen mit fetch und async/await stellen, response.ok prüfen und die drei Fehlerfälle an der Netzwerkgrenze behandeln.
  • REST als Architekturstil beschreiben, Ressourcen und Methoden auseinanderhalten und eine fremde API anhand ihrer Dokumentation konsumieren.
  • die Schnittstelle als Vertrag zwischen Anbieter und Konsument beschreiben und begründen, warum Vertragsbrüche fremde Programme beschädigen.

Eine Wetter-App zeigt “Villach: undefined °C”. Kein Absturz, keine Fehlermeldung, nur ein undefined. Der Server war gar nicht kaputt: Er hat ehrlich mit 404 geantwortet, weil der Ort falsch geschrieben war. Nur hat der Code nie nachgesehen, was auf dem Bon steht.

flowchart LR
    accTitle: Ein 404 wird wie eine erfolgreiche Antwort verarbeitet
    req["fetch(.../forecast?city=Vilach)"] --> res["Response 404,<br/>Body: {error: ...}"]
    res --> json["response.json()<br/>ohne Prüfung"]
    json --> ui["Anzeige:<br/>undefined °C"]

Abb. 5.5: fetch meldet einen 404 nicht als Fehler; wer response.ok nicht prüft, verarbeitet die Fehlerantwort wie Daten (eigene Darstellung).

In Einheit 5.1 hast du HTTP-Nachrichten gelesen und von Hand verschickt. Diese Einheit schreibt sie im Code, mit fetch. Dazu kommen REST, der übliche Baustil für HTTP-Schnittstellen, und der Begriff, der die nächsten Kapitel trägt: die Schnittstelle als Vertrag. Im Bild vom Lokal ist dieser Vertrag die Speisekarte.

  • fetch: die eingebaute Funktion, mit der JavaScript in Browser und Node.js HTTP-Anfragen stellt.
  • Ressource: ein Ding, das eine API anbietet, etwa ein Job oder ein Benutzer, mit einer eigenen URL.
  • Endpunkt: eine Kombination aus Methode und URL, die eine API beantwortet, etwa GET /api/jobs/42.
  • REST: ein Architekturstil für HTTP-Schnittstellen, bei dem Ressourcen über URLs und Tätigkeiten über HTTP-Methoden ausgedrückt werden.
  • Schnittstellenvertrag: die Zusicherungen einer API über Endpunkte, Datenformate und Fehlerverhalten.

Die eingebaute Funktion fetch stellt Anfragen aus JavaScript, in Browser und Node.js gleich. Sie ist asynchron und liefert Promise<Response>; hier zahlt sich Kapitel 3 direkt aus:

interface Job {
id: number;
state: "queued" | "running" | "done" | "failed";
progress: number;
}
async function loadJob(id: number): Promise<Job> {
const response = await fetch(`https://render.example.org/api/jobs/${id}`);
if (!response.ok) {
throw new Error(`loading job ${id} failed: ${response.status}`);
}
return (await response.json()) as Job;
}

Beispielprojekt: 05_http_und_schnittstellen_examples/ex01_fetch_load_job

render.example.org ist eine Beispieladresse, die es nicht gibt; das Beispielprojekt simuliert den Server, damit der Code ohne Netz läuft. Drei Punkte sind hier Handwerksregeln:

  1. fetch scheitert nur bei Netzwerkfehlern. Ein 404 oder 500 ist für fetch eine erfolgreiche Antwort; das Promise wird erfüllt. Die Prüfung if (!response.ok), wahr bei Status 200 bis 299, ist deshalb Pflicht in jedem fetch-Aufruf. Ihr Fehlen ist der häufigste Anfängerfehler mit dieser Funktion.
  2. Zwei await: eines für die Antwort samt Headern, eines für response.json(), das den Body liest und in ein Objekt umwandelt.
  3. Der Typ ist ein Versprechen an dich selbst. as Job behauptet, dass die API dieses Format liefert; geprüft wird es nicht. Für den Konsum fremder APIs reicht das zunächst, die saubere Prüfung an der Grenze folgt in Kapitel 12.

Daten senden funktioniert über den zweiten Parameter:

const response = await fetch("https://render.example.org/api/jobs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ filename: "intro.mp4", fps: 25 }),
});

Beispielprojekt: 05_http_und_schnittstellen_examples/ex02_fetch_post

JSON.stringify macht aus dem Objekt Text, der Content-Type-Header sagt dem Server, welches Format kommt. Die Gegenrichtung, JSON.parse beziehungsweise response.json(), macht aus Text wieder Objekte. JSON ist damit das Austauschformat in beide Richtungen, das der Lehrplan als strukturiertes Datenformat nennt. Im Beispielprojekt antwortet der simulierte Server mit 201 und dem neu angelegten Job.

Vollständige Fehlerbehandlung unterscheidet drei Fälle, wie an jeder Grenze zu fremden Daten:

  • Netz weg: fetch selbst wirft, etwa bei einer falschen Domain.
  • Fehlerstatus: Der Server antwortet, aber response.ok ist false.
  • Unbrauchbarer Body: response.json() wirft, weil die Antwort kein gültiges JSON ist.
flowchart LR
    accTitle: Die drei Fehlerfälle an der Netzwerkgrenze
    request["await fetch(url)"] -- "wirft" --> net["Netz weg"]
    request --> ok{"response.ok?"}
    ok -- "nein" --> status["Fehlerstatus"]
    ok -- "ja" --> json["await response.json()"]
    json -- "wirft" --> body["unbrauchbarer Body"]
    json --> data["Daten"]

Abb. 5.6: Drei Stellen, an denen eine Anfrage scheitern kann, und nur eine davon meldet fetch selbst (eigene Darstellung).

Eine kleine Hilfsfunktion, die alle drei in einen aussagekräftigen Fehler übersetzt, gehört in jedes Projekt dieses Jahres; in Aufgabe 05-1a baust du sie selbst.

Wo begegnet dir das? In jeder App, die Daten nachlädt: Ein Feed, der “Konnte nicht geladen werden, erneut versuchen?” anzeigt, statt leer zu bleiben, unterscheidet genau diese Fälle.

Auflösung

Der Server antwortet mit 404 und einem Fehler-Body, etwa {"error": "job 99 not found"}. Ohne die Prüfung wird dieser Body als Job zurückgegeben; job.state und job.progress sind undefined. Der Fehler fällt erst viel später auf, etwa als “undefined %” in der Anzeige, weit weg von der eigentlichen Ursache. Mit der Prüfung wirft loadJob sofort loading job 99 failed: 404.

Bisher war HTTP das Transportmittel und JSON das Format. Aber wie ordnet man eine API dahinter sinnvoll? Die mit Abstand häufigste Antwort heißt REST (Representational State Transfer). REST ist kein Standard und keine Bibliothek, sondern ein Architekturstil: eine Sammlung von Prinzipien, die Roy Fielding im Jahr 2000 in seiner Dissertation beschrieben hat. Eine API, die sie einhält, heißt RESTful.

Der Kerngedanke ist einfach: Alles, was die API anbietet, wird als Ressource modelliert (ein Job, ein Benutzer, ein Rezept), jede Ressource hat eine eigene URL, und die HTTP-Methoden aus Einheit 5.1 sagen, was mit ihr geschehen soll:

GET /api/jobs alle Jobs lesen
POST /api/jobs neuen Job anlegen
GET /api/jobs/42 einen bestimmten Job lesen
PUT /api/jobs/42 Job 42 komplett ersetzen
DELETE /api/jobs/42 Job 42 löschen

Die URL benennt also das Ding (das Substantiv), die Methode die Tätigkeit (das Verb). Ein häufiger Anfängerfehler ist, das Verb in die URL zu packen (/api/getJob?id=42, /api/deleteJob); RESTful ist das nicht.

flowchart LR
    accTitle: In REST benennt die URL das Ding und die Methode die Tätigkeit
    method["Methode<br/>= Verb<br/>GET, POST, DELETE"] --> endpoint["DELETE /api/jobs/42"]
    url["URL<br/>= Substantiv<br/>/api/jobs/42"] --> endpoint
    wrong["✗ /api/deleteJob?id=42<br/>Verb in der URL"]

Abb. 5.7: Methode und URL teilen sich die Arbeit; ein Verb in der URL ist nicht RESTful (eigene Darstellung).

Fielding beschreibt REST über sechs Bedingungen (constraints). Fünf sind verpflichtend, eine ist optional.

  1. Client-Server: Anbieter und Konsument sind getrennt und kennen voneinander nur den Vertrag über die Schnittstelle. Der Client kümmert sich um die Darstellung, der Server um Daten und Logik. Beide können sich unabhängig weiterentwickeln, solange der Vertrag hält.
  2. Zustandslosigkeit (stateless): Jede Anfrage bringt alles Nötige selbst mit; der Server merkt sich zwischen zwei Anfragen keinen Sitzungszustand. Das ist die Zustandslosigkeit von HTTP aus Einheit 5.1, hier zur Regel erhoben.
  3. Cachebarkeit (cacheable): Antworten kennzeichnen, ob und wie lange sie zwischengespeichert werden dürfen. Weil GET Daten nur liest, lässt es sich gefahrlos cachen; das spart Anfragen und macht die API schneller.
  4. Einheitliche Schnittstelle (uniform interface): Alle Ressourcen werden nach demselben Muster angesprochen: URLs als Ressourcen, Standardmethoden, Standard-Statuscodes. Wer eine RESTful API bedienen kann, findet sich in jeder anderen sofort zurecht. Das ist das zentrale Prinzip, das REST seine Wiedererkennbarkeit gibt.
  5. Schichtensystem (layered system): Zwischen Client und Server dürfen Zwischenschichten liegen (Caches, Lastverteiler, Gateways), ohne dass der Client davon weiß. Er spricht immer nur mit “der API”, nicht mit einem bestimmten Rechner dahinter.
  6. Code on Demand (optional): Der Server darf ausführbaren Code an den Client nachliefern, etwa JavaScript. Als einzige der sechs Bedingungen ist sie freiwillig; die meisten APIs verzichten darauf.
Auflösung

RESTful: GET /api/students, POST /api/students und DELETE /api/students/7. Gefährlich ist der dritte: Ein GET darf nichts verändern, Browser und Zwischenspeicher rufen GET-Adressen auch von sich aus auf, etwa beim Vorausladen von Links. Ein Löschen über GET kann so ohne Absicht ausgelöst werden.

Öffentliche APIs gibt es zu fast allem; zum Üben eignen sich etwa Open-Meteo (Wetter, ohne Anmeldung), die PokéAPI oder Test-APIs mit Beispieldaten. Der Arbeitsablauf ist immer derselbe, und er ist eine eigene Kompetenz:

  1. Dokumentation lesen: Welche Endpunkte gibt es, welche Parameter, welches Antwortformat? Gute API-Dokumentation enthält Beispielanfragen samt Beispielantworten.
  2. Im Terminal oder Browser ausprobieren, bevor Code entsteht: eine Anfrage von Hand, Antwort ansehen.
  3. Datenvertrag als Interface festhalten: die Antwortstruktur oder den benötigten Ausschnitt als TypeScript-Interface notieren. Niemand braucht alle 40 Felder; ins Interface kommt, was das Programm verwendet.
  4. Robust konsumieren: response.ok prüfen, Fehlerfälle behandeln, bei optionalen Feldern ?. und ?? einsetzen.
flowchart LR
    accTitle: Vier Schritte, um eine fremde API zu nutzen
    docs["1. Doku lesen"] --> try["2. von Hand<br/>ausprobieren"]
    try --> contract["3. Interface<br/>festhalten"]
    contract --> code["4. robust<br/>konsumieren"]

Abb. 5.8: Erst lesen und ausprobieren, dann den Vertrag festhalten, zuletzt programmieren (eigene Darstellung).

Zwei Realitäten fremder APIs gehören von Anfang an ins Bild. API-Schlüssel: Viele Dienste verlangen einen Schlüssel pro Anfrage; er gehört in eine Konfigurationsdatei, niemals ins Repository. Rate Limits: Zu viele Anfragen in kurzer Zeit führen zu 429 Too Many Requests; die Begrenzung der Gleichzeitigkeit aus Einheit 4.1 ist hier bares Geld.

Wo begegnet dir das? Jede Wetter-, Fahrplan- oder Karten-App greift auf solche APIs zu. Wenn eine kleine App nach einem Update plötzlich keine Daten mehr zeigt, hat oft der Anbieter der API etwas geändert, und damit sind wir beim Vertrag.

Zum Abschluss der Begriff, der die nächsten Kapitel trägt. Eine Schnittstelle ist mehr als eine erreichbare URL: Sie ist ein Vertrag zwischen Anbieter und Konsument. Im Lokal ist das die Speisekarte: Was darauf steht, bekommt der Gast, egal wie die Küche das Gericht zubereitet. Der Vertrag umfasst:

  • die Endpunkte und ihre Methoden: Was kann man tun?
  • die Datenformate von Anfrage und Antwort: welche Felder, welche Typen?
  • das Fehlerverhalten: welche Statuscodes, welches Fehlerformat?
  • die Zusicherungen: Was bleibt stabil, auch wenn intern umgebaut wird?
flowchart LR
    accTitle: Anbieter und Konsument kennen nur den Vertrag
    provider["Anbieter:<br/>darf intern alles ändern"] --- contract["Vertrag:<br/>Endpunkte, Formate,<br/>Fehler, Zusicherungen"]
    contract --- consumer["Konsument:<br/>verlässt sich nur<br/>auf den Vertrag"]

Abb. 5.9: Zwischen Anbieter und Konsument steht nur der Vertrag (eigene Darstellung).

Der Konsument verlässt sich auf diesen Vertrag, ohne das Innenleben des Anbieters zu kennen; der Anbieter darf intern alles ändern, solange der Vertrag hält. Bricht er ihn, etwa durch ein umbenanntes Feld oder einen geänderten Statuscode, gehen fremde Programme kaputt, deren Autorinnen und Autoren er nicht kennt. Deshalb werden Schnittstellen entworfen statt einfach wachsen gelassen; wie, ist Thema von Kapitel 6.

Auflösung

(b) und (d) brechen den Vertrag: Programme, die progress lesen oder auf 404 prüfen, liefern danach undefined bzw. verarbeiten einen leeren Body als Erfolg. (a) ist ein interner Umbau, den kein Konsument bemerkt. (c) erweitert die Antwort nur; Programme, die das neue Feld nicht kennen, ignorieren es. Zusätzliche Felder gelten deshalb meist als verträglich, umbenannte oder entfernte nicht.

  1. response.ok nicht geprüft: Ein 404 oder 500 wird als Daten weiterverarbeitet; der Fehler zeigt sich erst viel später als undefined.
  2. Ein await vergessen: response.json() liefert ein Promise; ohne await steht das Promise statt der Daten in der Variablen.
  3. as Job für eine Prüfung gehalten: Die Typumwandlung behauptet nur; liefert die API etwas anderes, merkt das TypeScript nicht.
  4. Verb in der URL: /api/deleteJob statt DELETE /api/jobs/42; besonders gefährlich, wenn dabei GET etwas verändert.
  5. API-Schlüssel im Repository: Was einmal in Git ist, ist auch nach dem Löschen in der Historie.

Du kannst dir diese Einheit kostenlos mit der Vorlesefunktion deines Browsers oder Betriebssystems vorlesen lassen (etwa „Laut vorlesen” in Edge), ohne Konto und ohne Upload; die Codebeispiele liest du dabei am Bildschirm mit. Freiwillig kannst du mit Google NotebookLM (Google-Konto nötig) aus diesem öffentlichen Text eine gesprochene Zusammenfassung erzeugen, auch auf Deutsch.

Du kannst jetzt:

  • Anfragen mit fetch und async/await stellen, response.ok prüfen und die drei Fehlerfälle an der Netzwerkgrenze behandeln.
  • REST als Architekturstil beschreiben, Ressourcen und Methoden auseinanderhalten und eine fremde API anhand ihrer Dokumentation konsumieren.
  • die Schnittstelle als Vertrag zwischen Anbieter und Konsument beschreiben und begründen, warum Vertragsbrüche fremde Programme beschädigen.
  • Aufgabe 05-1a - API-Konsument, Stufen Wesentlich_vollständig, PlusPlus und MandalorianPlus
  • Oliver Maurhart (MAO)
  • Michael Gabriel (GAM)

CC-BY-SA

https://qiz.at/rules