Zum Inhalt springen

5. HTTP und Schnittstellen

Zu Zen-Modus wechseln

Wenn zwei Programme über das Netz miteinander sprechen sollen, brauchen sie eine gemeinsame Sprache: ein Protokoll. Das Protokoll des Webs ist HTTP (Hypertext Transfer Protocol), und es transportiert längst nicht mehr nur Webseiten: Apps holen darüber ihre Daten, Server sprechen darüber miteinander, und praktisch jede öffentliche API (Application Programming Interface, eine programmierbare Schnittstelle) ist eine HTTP-Schnittstelle. Der Lehrplan nennt Protokolle und Schnittstellen ausdrücklich; dieses Kapitel legt das Fundament, auf dem später eigene Schnittstellen entworfen und implementiert werden.

Der Weg: erst das Protokoll verstehen und sichtbar machen, dann mit fetch aus eigenem Code sprechen, mit REST den gängigen Baustil hinter HTTP-APIs kennenlernen, schließlich fremde APIs konsumieren und dabei den Begriff des Schnittstellen-Vertrags schärfen.

HTTP ist ein Frage-Antwort-Spiel: Der Client schickt einen Request, der Server antwortet mit einer Response, Verbindung fertig. Beide Nachrichten sind im Kern lesbarer Text. Eine Anfrage:

GET /api/jobs/42 HTTP/1.1
Host: render.example.at
Accept: application/json

Und die Antwort darauf:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 64
{"id": 42, "state": "running", "progress": 0.62}

Die Bestandteile, die uns das ganze Jahr begleiten:

  • Methode (GET): was getan werden soll.
  • Pfad (/api/jobs/42): worauf es sich bezieht.
  • Header (Accept, Content-Type, …): Metadaten über Anfrage und Antwort, als Schlüssel-Wert-Paare.
  • Statuscode (200 OK): das Ergebnis in einer Zahl.
  • Body: die eigentlichen Daten, bei APIs fast immer JSON.

Wichtig für alles Weitere: HTTP ist zustandslos. Der Server erinnert sich zwischen zwei Anfragen an nichts; jede Anfrage muss alles Nötige mitbringen. Was nach einer Einschränkung klingt, ist der Grund, warum das Web skaliert: Jede Anfrage kann von irgendeinem Server beantwortet werden.

MethodeBedeutungBody?wiederholbar ohne Schaden?
GETDaten lesenneinja
POSTetwas Neues anlegen / Aktion auslösenjanein
PUTRessource komplett ersetzenjaja
PATCHRessource teilweise ändernjameist
DELETERessource löschenseltenja

Die letzte Spalte heißt Idempotenz: Ein idempotenter Request darf gefahrlos wiederholt werden, weil das Ergebnis dasselbe bleibt (zweimal dasselbe DELETE löscht nicht mehr als einmal). POST ist die Ausnahme, und genau daraus entstand die Doppelbestellungs-Race-Condition aus dem vorigen Kapitel. Browser und Werkzeuge verhalten sich entsprechend: GET-Anfragen werden gecacht und wiederholt, POST nicht.

Die erste Ziffer sortiert:

KlasseBedeutungwichtige Vertreter
2xxErfolg200 OK, 201 Created, 204 No Content
3xxUmleitung301/302 (woanders), 304 Not Modified
4xxClient-Fehler400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found
5xxServer-Fehler500 Internal Server Error, 503 Service Unavailable

Die Trennlinie zwischen 4xx und 5xx ist die Schuldfrage, und sie entscheidet über die richtige Reaktion: Bei 4xx war die Anfrage falsch (Wiederholen bringt nichts, die Anfrage muss sich ändern); bei 5xx hat der Server ein Problem (später wiederholen kann helfen). Wer eigene APIs baut, wird diese Codes selbst vergeben; wer fremde konsumiert, muss sie lesen können.

HTTP versteht am schnellsten, wer es beobachtet. Zwei Werkzeuge dafür:

Der Netzwerk-Tab der Browser-DevTools (F12) zeigt jede Anfrage der aktuellen Seite: Methode, URL, Status, Dauer, sämtliche Header und den Body. Öffnen Sie eine beliebige Webseite mit offenem Netzwerk-Tab: Dutzende Anfragen für Seiten, Bilder, Skripte und Daten, jede einzeln inspizierbar. Dieser Tab ist für den Rest des Jahres das wichtigste Diagnosewerkzeug.

curl (überall vorhanden) beziehungsweise das komfortablere HTTPie stellen Anfragen direkt aus dem Terminal, ohne jeden Code:

Terminal-Fenster
curl -i https://api.example.at/jobs/42 # -i: show response headers
curl -X POST https://api.example.at/jobs \
-H "Content-Type: application/json" \
-d '{"filename": "intro.mp4"}'
http GET https://api.example.at/jobs/42 # HTTPie: same, friendlier

Ebenfalls überall zu finden ist wget, das eigentlich zum Herunterladen von Dateien gedacht ist, sich aber genauso zum schnellen Antippen einer API eignet:

Terminal-Fenster
wget -qO- https://api.example.at/jobs/42 # -q: quiet, -O-: body to stdout
wget -S --spider https://api.example.at/jobs/42 # inspect the response headers only

Faustregel: curl zum Ausprobieren und Fehlersuchen (feine Kontrolle über Methode, Header, Body), wget wenn es ums bloße Herunterladen geht.

Terminal-Anfragen sind das schnellste Mittel, eine API auszuprobieren oder einen Fehler einzugrenzen (“Liegt es an meinem Code oder an der API?”), und sie bleiben es auch, wenn ab dem Next.js-Kapitel die eigene API getestet wird.

Die eingebaute Funktion fetch stellt Anfragen aus JavaScript, in Browser und Node identisch. Sie ist asynchron und liefert Promise<Response>; hier zahlt das Async-Kapitel direkt ein:

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.at/api/jobs/${id}`);
if (!response.ok) {
throw new Error(`loading job ${id} failed: ${response.status}`);
}
return (await response.json()) as Job;
}

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 API.
  2. Zwei awaits: eines für die Antwort-Header, eines für response.json(), das den Body liest und parst.
  3. Der Typ ist ein Versprechen an uns 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 Validierung an der Grenze folgt im Middleware-Kapitel.

Daten senden funktioniert über den zweiten Parameter:

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

JSON.stringify macht aus dem Objekt Text, der Content-Type-Header deklariert das Format. Die Gegenrichtung (JSON.parse beziehungsweise response.json()) macht aus Text wieder Objekte; JSON ist damit das Austauschformat in beide Richtungen, das im Lehrplan als strukturiertes Datenformat firmiert.

Vollständige Fehlerbehandlung unterscheidet drei Fälle, analog zu jeder Datengrenze: Netzwerk weg (fetch wirft), Server antwortet mit Fehlerstatus (!response.ok), Antwort ist kein brauchbares JSON (response.json() wirft). Eine kleine Hilfsfunktion, die alle drei in einen aussagekräftigen Fehler übersetzt, gehört in jedes Projekt dieses Jahres.

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 2000 von Roy Fielding beschrieben wurden. 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 dem obigen Abschnitt sagen, was mit ihr geschehen soll. Genau daher rührt die Tabelle der Methoden ihre Bedeutung:

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 benennt 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.

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 exakt die Zustandslosigkeit von HTTP, hier zur Regel erhoben. Der Vorteil ist wieder die Skalierbarkeit: Jede Anfrage kann von irgendeinem Server beantwortet werden.
  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. Dies ist das zentrale Prinzip, das REST seine Wiedererkennbarkeit gibt.
  5. Schichtensystem (layered system): Zwischen Client und Server dürfen Zwischenschichten liegen (Caches, Load Balancer, 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.

Öffentliche APIs gibt es zu fast allem; zum Üben eignen sich etwa Open-Meteo (Wetter, ohne Anmeldung), PokeAPI oder die diversen 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-Doku enthält Beispiel-Requests samt Beispiel-Antworten.
  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.

Zwei Realitäten fremder APIs gehören von Anfang an ins Bild: API-Schlüssel (viele Dienste verlangen einen Token pro Anfrage; sie gehört in eine Konfigurationsdatei, niemals ins Repository) und Rate Limits (zu viele Anfragen in kurzer Zeit führen zu 429 Too Many Requests; die Sammelanfrage-Disziplin aus dem Koordinationskapitel ist hier bares Geld).

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. 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?).

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 (ein Feld umbenannt, ein Statuscode geändert), gehen fremde Programme kaputt, deren Autoren er nicht kennt. Deshalb werden Schnittstellen entworfen statt gewachsen; wie, ist Thema des nächsten Kapitels.

Lernergebnisse: Was Sie nach diesem Kapitel können sollten

Abschnitt betitelt „Lernergebnisse: Was Sie nach diesem Kapitel können sollten“

Nach Abschluss dieses Kapitels sollten Schülerinnen und Schüler in der Lage sein:

  • Erklären: den Aufbau von HTTP-Anfragen und -Antworten (Methode, Pfad, Header, Statuscode, Body) und die Zustandslosigkeit des Protokolls erklären.
  • Nennen: die Bedeutung der Methoden samt Idempotenz und die wichtigsten Statuscodes mit ihrer Klasseneinteilung nennen.
  • Anwenden: HTTP-Verkehr mit DevTools und curl/HTTPie sichtbar machen und gezielt Anfragen von Hand stellen.
  • Anwenden: Anfragen mit fetch und async/await stellen, response.ok prüfen und alle drei Fehlerfälle behandeln.
  • Erklären: REST als Architekturstil beschreiben, Ressourcen und Methoden auseinanderhalten und die sechs Prinzipien nennen.
  • Anwenden: eine fremde API anhand ihrer Dokumentation konsumieren und den Datenvertrag als TypeScript-Interface festhalten.
  • Erklären: die Schnittstelle als Vertrag zwischen Anbieter und Konsument beschreiben und begründen, warum Vertragsbrüche fremde Programme beschädigen.
  • Aufgabe 09 - API-Konsument