Zum Inhalt
wannacode

Daten aus einer API

Kurz gesagt: fetch holt mit async/await Daten aus einer API, prüft response.ok und liest das JSON, und die Seite zeigt einen von vier Zuständen: Laden, Fehler, leer oder Daten.

Problem

Ein leeres Schaufenster

Die Seite mit den Milchprodukten holt ihre Produkte vom Server. Heute hat der Server ein Problem und antwortet mit dem Fehler 500. Die Käuferin sieht eine Überschrift und darunter nichts.

HTML, CSS, JS

Die Konsole meldet products.map is not a function. Der Code hat die Antwort des Fehlers, { "error": "…" }, für eine Liste von Produkten gehalten. Die Käuferin sieht keinen Fehler und kein „Nochmal versuchen“. Und selbst wenn der Server funktioniert, ist die Seite leer, solange die Antwort unterwegs ist.

Die Idee

fetch auf der Seite

fetch(address) schickt eine Anfrage und gibt ein Versprechen (Promise) auf die Antwort zurück. Mit await wartet die Funktion auf die Antwort, ohne die Seite anzuhalten:

const response = await fetch('/api/products?category=4');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json();
  • fetch wirft nur, wenn es keine Antwort gibt: kein Netz. Eine Antwort mit 404 oder 500 ist eine Antwort, und das Promise wird erfüllt.
  • response.ok ist nur für Codes von 200 bis 299 wahr. Das zu prüfen ist deine Aufgabe.
  • response.json() liest den Body und macht daraus ein Objekt. Auch das ist ein Promise, also bekommt es ebenfalls await.

In der Sandbox beantwortet die Schein-API des Shops die Adressen /api/…, ganz ohne Netz: GET /api/products (mit ?q= und ?category=), /api/products/3, /api/categories, /api/orders und POST /api/orders. Eine Lektion kann sie langsam machen, mit 500 antworten lassen oder eine leere Liste zurückgeben lassen. So ist jeder Zustand echt und immer derselbe.

Die Idee

Vier Zustände

Eine Anfrage braucht Zeit und hat ein Ergebnis. Der Bildschirm muss in jedem Moment sagen, was gerade los ist:

  • Laden: Die Anfrage ist unterwegs, es gibt noch keine Antwort;
  • Daten: Eine Liste von Produkten ist angekommen;
  • leer: Eine Liste ist angekommen, aber sie ist leer. Das ist kein Fehler;
  • Fehler: Es gibt keine Antwort, oder die Antwort ist nicht ok.

Jeder Zustand ist ein Element im HTML. Der Code zeigt und versteckt die Elemente mit der Property hidden:

<p id="loading" role="status">Wird geladen…</p>
<p id="error" role="alert" hidden></p>
<p id="empty" hidden>Keine Produkte in dieser Kategorie.</p>
<ul id="products"></ul>

„Wird geladen…“ ist von Anfang an sichtbar, noch bevor JavaScript überhaupt läuft. role="status" sorgt dafür, dass ein Screenreader es vorliest, und role="alert" liest den Fehler vor, sobald er erscheint.

Sieh es dir an

Eine Anfrage beobachten

async function load() {
try {
const response = await fetch('/api/products?category=4');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json();
empty.hidden = products.length > 0;
list.replaceChildren(...products.map(productItem));
} catch {
error.textContent = 'Die Produkte wurden nicht geladen.';
error.hidden = false;
} finally {
loading.hidden = true;
}
}
Schritt für Schritt, in Worten
  1. load() schickt die Anfrage los. Eine Antwort gibt es noch nicht: Der Bildschirm zeigt „Wird geladen…“ aus dem HTML.
  2. await wartet, aber die Seite friert nicht ein: Die Käuferin kann scrollen, und der Spinner dreht sich. Bei einem langsamen Netz dauert das Sekunden.
  3. 200 mit einem Array: ok ist true, json() liefert die Produkte, die Zeilen werden gezeichnet, und finally versteckt „Wird geladen…“.
  4. Dieselbe Anfrage, Antwort 500. fetch wirft nichts: Es hat eine Antwort bekommen. Unsere Prüfung von ok wirft, und catch zeigt den Fehler.
  5. 200 mit []: Das ist kein Fehler. Das Array ist leer, deshalb sagt die Seite „keine Produkte“, statt eine Leerstelle zu zeigen.
  6. Vier Zustände, jeder sichtbar: Laden, Daten, Fehler, leer. finally versteckt „Wird geladen…“ bei jedem Ausgang aus try.

Derselbe Code, drei verschiedene Antworten. Bei 500 wirft fetch nichts: Die Prüfung von ok wirft, und catch zeigt den Fehler. finally versteckt „Wird geladen…“ in allen drei Fällen.

Sieh es dir an

Die langsame API

Hier antwortet die API nach 1,2 Sekunden. Klick auf „Ausführen“ und beobachte „Wird geladen…“.

HTML, CSS, JS

Der ganze Code zum Laden:

async function load() {
try {
const response = await fetch('/api/products?category=4');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const products = await response.json();
empty.hidden = products.length > 0;
list.replaceChildren(...products.map(productItem));
} catch {
error.textContent = 'Die Produkte wurden nicht geladen.';
error.hidden = false;
} finally {
loading.hidden = true;
}
}
load();

Probier es aus

Fehler

Hier bekommt die erste Anfrage eine 500, und die zweite geht durch. Bei einem Fehler soll die Seite „Die Produkte wurden nicht geladen.“ in #error und den Button „Nochmal versuchen“ zeigen, „Wird geladen…“ soll verschwinden, und „keine Produkte“ darf nicht erscheinen. Der Button lädt schon wieder neu: Danach müssen die Produkte auf der Seite stehen.

HTML, CSS, JS

Probier es aus

Leer

Hier gibt die API eine leere Liste zurück. Zeige „Keine Produkte in dieser Kategorie.“, wenn es null Produkte sind, und verstecke die Meldung, wenn es welche gibt.

HTML, CSS, JS

Probier es aus

Finde den Fehler

Der Server antwortet mit 200 und Produkten, und die Seite zeigt einen Fehler.

Wähle die Zeile mit dem Fehler.

Die Idee

Senden: der Button und das Warten

Die Bestellung geht mit POST raus. Die Daten stehen als JSON im Body, und der Header Content-Type sagt dem Server, was er bekommt:

const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...data, items }),
});

data ist das Objekt aus den Feldern des Formulars: Object.fromEntries(new FormData(form)) macht aus den Paaren name=Wert ein Objekt { name: …, email: …, address: … }.

Während die Antwort unterwegs ist, ist der Button „Bestellen“ noch aktiviert. Eine ungeduldige Käuferin drückt zweimal:

HTML, CSS, JS

Zwei Anfragen, zwei Bestellungen: 61 und 62. Die Seite zeigt die letzte, und niemand merkt, dass auch die erste angenommen wurde.

Probier es aus

Ein Klick, eine Bestellung

Deaktiviere den Button „Bestellen“, bevor die Anfrage losgeht, und aktiviere ihn wieder, wenn die Antwort kommt, auch wenn es ein Fehler ist. Die Prüfung klickt schnell zweimal und erwartet eine Bestellung: 61.

HTML, CSS, JS
Genauer, für Neugierige

Ein Netzwerkfehler. Ohne Netz wirft fetch TypeError: Failed to fetch. catch fängt das und auch dein eigenes throw bei !response.ok: Für die Käuferin heißt beides „nicht geladen“.

Der Body eines Fehlers. Ein Server erklärt den Fehler oft im Body, als JSON. Bei 400 gibt die Schein-API { "errors": { "email": "…" } } zurück: einen Fehler pro Feld. Deshalb liest die Aufgabe den Body auch bei einem Fehler.

Nochmal versuchen. Ein Button „Nochmal versuchen“ neben dem Fehler ruft einfach noch einmal load() auf. Vorher muss „Wird geladen…“ wieder erscheinen und der Fehler verschwinden.

Ein Wettlauf zwischen Anfragen. Wechselt die Käuferin schnell zweimal die Kategorie, können die beiden Antworten in umgekehrter Reihenfolge ankommen, und die Seite zeigt die alte Kategorie. Wie du eine alte Anfrage abbrichst (AbortController), steht im Modul über Effekte.

Prüfungen im Browser und auf dem Server. required und type="email" halten das Formular auf, bevor es rausgeht. Der Server prüft trotzdem noch einmal, denn eine Anfrage kann auch ohne unsere Seite kommen. In der Aufgabe haben die Felder keine eingebauten Prüfungen, damit du siehst, was der Server antwortet.

Übung

Die Bestellung

Sende die Bestellung: POST /api/orders mit name, email und address aus dem Formular und items aus dem Array. Solange sie unterwegs ist, ist der Button deaktiviert. Setze bei 400 jede Meldung aus errors in den Absatz unter ihrem Feld (#email-error und so weiter) und lösche bei jedem neuen Senden die alten Meldungen. Schreibe bei Erfolg „Bestellung 61 ist angenommen. Gesamt: 4,00 €“ in #result (die Nummer und die Summe kommen aus der Antwort).

HTML, CSS, JS

Test

Teste dich

1. Der Server antwortet mit 500. Was macht await fetch(…)?
2. Die API gibt [] zurück. Welche Bedingung zeigt „keine Produkte“ an?
3. Warum wird der Button „Bestellen“ deaktiviert, bevor die Anfrage losgeht, und nicht danach?
4. Warum wird „Wird geladen…“ in finally versteckt?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·