Abschnitt 11 · Lektion 7
Synthese: die Bücher-API
Sechs Lektionen, sechs Bausteine: Ressourcen statt Tätigkeiten, Lesen mit den richtigen Codes,
Anlegen mit Location, Ändern und Löschen, eine Prüfung an der Tür und eine gemeinsame Fehlerform.
Jetzt liegen sie alle auf dem Tisch.
Die API am Stück
import express from "express";
export const router = express.Router();
const notizen = [
{ id: 1, text: "Milch kaufen", wichtig: false },
{ id: 2, text: "Steuer abgeben", wichtig: true },
];
let naechsteId = 3;
function baueFehler(status, code, meldung, felder) {
const fehler = new Error(meldung);
fehler.status = status;
fehler.code = code;
fehler.felder = felder;
return fehler;
}
function pruefe(koerper = {}) {
const verstoesse = [];
if (typeof koerper.text !== "string" || koerper.text.trim() === "") {
verstoesse.push({ feld: "text", meldung: "fehlt oder ist leer" });
}
if (typeof koerper.wichtig !== "boolean") {
verstoesse.push({ feld: "wichtig", meldung: "fehlt oder ist kein Wahrheitswert" });
}
return verstoesse;
}
router.get("/", (req, res) => {
res.json({ anzahl: notizen.length, daten: notizen });
});
router.get("/:id", (req, res, next) => {
const notiz = notizen.find((eintrag) => eintrag.id === Number(req.params.id));
if (notiz === undefined) {
return next(baueFehler(404, "nicht_gefunden", `Notiz ${req.params.id} gibt es nicht`));
}
res.json(notiz);
});
router.post("/", (req, res, next) => {
const verstoesse = pruefe(req.body);
if (verstoesse.length > 0) {
return next(baueFehler(400, "ungueltig", "Die Eingabe ist unvollstaendig", verstoesse));
}
const notiz = { id: naechsteId++, text: req.body.text, wichtig: req.body.wichtig };
notizen.push(notiz);
res.status(201).location(`/api/notizen/${notiz.id}`).json(notiz);
});
router.delete("/:id", (req, res, next) => {
const platz = notizen.findIndex((eintrag) => eintrag.id === Number(req.params.id));
if (platz === -1) {
return next(baueFehler(404, "nicht_gefunden", `Notiz ${req.params.id} gibt es nicht`));
}
notizen.splice(platz, 1);
res.status(204).end();
}); import express from "express";
import { router as notizenRouter } from "./routen/notizen.js";
const app = express();
app.use(express.json());
app.use("/api/notizen", notizenRouter);
// Standardfall und Fehler-Handler, beide ganz unten und beide in
// derselben Form wie alles andere.
app.use((req, res) => {
res.status(404).json({ fehler: { code: "nicht_gefunden", meldung: "Diesen Pfad gibt es nicht" } });
});
app.use((fehler, req, res, next) => {
const status = fehler.status ?? 500;
console.error(fehler.message);
res.status(status).json({
fehler: {
code: fehler.code ?? "serverfehler",
meldung: status === 500 ? "Unerwarteter Fehler" : fehler.message,
felder: fehler.felder,
},
});
});
app.listen(3000); Rechts steht die Liste unter /api/notizen. /api/notizen/2 zeigt eine einzelne, /api/notizen/99
den Fehlerfall, und /gibtsnicht den Standardfall. Die schreibenden Wege stehen gleich darunter.
Zwei Dateien. Im Router stehen die Routen und alles, was mit Notizen zu tun hat. In der server.js
steht, was für die ganze Anwendung gilt: der Parser, das Einhängen unter /api/notizen, der
Standardfall für unbekannte Pfade und der Fehler-Handler.
Diese Aufteilung ist derselbe Gedanke wie in Lektion 10.4, nur mit einem zusätzlichen Vorteil:
Der Präfix steht genau einmal. Im Router heißen die Pfade / und /:id. Wenn die Schnittstelle
morgen unter /api/v2/notizen liegen soll, änderst du eine Zeile.
Achte auf die beiden letzten Anfragen des Beispiels. Ein unbekanntes Einzelstück und ein unbekannter Pfad sind zwei völlig verschiedene Fälle, und trotzdem sieht die Antwort gleich aus. Genau das war das Ziel aus Lektion 11.6.
Prüfen mit curl
Für alles bisher hat ein kleines Skript mit fetch gereicht. Auf einem echten Server willst du
nicht erst ein Skript schreiben, um zu sehen, ob eine Route tut, was sie soll.
import express from "express";
const app = express();
app.use(express.json());
const notizen = [
{ id: 1, text: "Milch kaufen", wichtig: false },
{ id: 2, text: "Steuer abgeben", wichtig: true },
];
let naechsteId = 3;
app.get("/api/notizen/:id", (req, res) => {
const notiz = notizen.find((eintrag) => eintrag.id === Number(req.params.id));
if (notiz === undefined) {
return res.status(404).json({ fehler: { code: "nicht_gefunden", meldung: "gibt es nicht" } });
}
res.json(notiz);
});
app.post("/api/notizen", (req, res) => {
// Der Rueckfall faengt den Fall ab, dass der Content-Type fehlt und
// req.body deshalb undefined ist. Der letzte curl-Aufruf zeigt ihn.
const koerper = req.body ?? {};
if (typeof koerper.text !== "string" || typeof koerper.wichtig !== "boolean") {
return res.status(400).json({
fehler: { code: "ungueltig", meldung: "text und wichtig werden gebraucht" },
});
}
const notiz = { id: naechsteId++, text: koerper.text, wichtig: koerper.wichtig };
notizen.push(notiz);
res.status(201).location(`/api/notizen/${notiz.id}`).json(notiz);
});
app.delete("/api/notizen/:id", (req, res) => {
const platz = notizen.findIndex((eintrag) => eintrag.id === Number(req.params.id));
if (platz === -1) {
return res.status(404).json({ fehler: { code: "nicht_gefunden", meldung: "gibt es nicht" } });
}
notizen.splice(platz, 1);
res.status(204).end();
});
app.listen(3000); import { spawn, execSync } from "node:child_process";
import { setTimeout as warte } from "node:timers/promises";
// Der Server laeuft als eigener Prozess. Muesste er im selben laufen,
// blockierte ihn das synchrone execSync und keine Anfrage kaeme durch.
const kind = spawn("node", ["notizen-server.js"], { stdio: "ignore" });
for (let versuch = 0; versuch < 100; versuch++) {
try {
await fetch("http://127.0.0.1:3000/api/notizen/1");
break;
} catch {
await warte(50);
}
}
const befehle = [
"curl -s -w ' [%{http_code}]' http://127.0.0.1:3000/api/notizen/2",
"curl -s -w ' [%{http_code}]' -X POST -H 'Content-Type: application/json' -d '{\"text\":\"Neu\",\"wichtig\":false}' http://127.0.0.1:3000/api/notizen",
"curl -s -w ' [%{http_code}]' -X DELETE http://127.0.0.1:3000/api/notizen/1",
"curl -s -w ' [%{http_code}]' -X POST -d '{\"text\":\"Ohne Kopfzeile\",\"wichtig\":false}' http://127.0.0.1:3000/api/notizen",
];
for (const befehl of befehle) {
// Das Statusanhaengsel und der Rechnername sind ueberall gleich, die
// Ausgabe kuerzt sie deshalb weg.
console.log(befehl.replace(" -w ' [%{http_code}]'", "").replace("http://127.0.0.1:3000", ""));
console.log(` ${execSync(befehl).toString()}`);
}
kind.kill(); Vier Angaben braucht man, mehr nicht. -X setzt die Methode, -H schickt eine Kopfzeile, -d
schickt den Körper, und -s schaltet die Fortschrittsanzeige ab, die sonst die Ausgabe zumüllt.
Ein GET braucht gar keine davon, da reicht curl und die Adresse.
Das Anhängsel -w ' [%{http_code}]' ist der fünfte Handgriff, den es lohnt zu kennen: Es schreibt
den Statuscode hinter die Antwort. Ohne ihn siehst du bei einem 204 gar nichts und weißt nicht, ob
etwas passiert ist.
Und das kannst du hier wirklich tun, curl liegt in dieser Sandbox. Hol die Aufgabe dieser
Lektion nach vorn, dann läuft dein eigener Server auf Port 3000, und im Terminal tippst du dieselben
Aufrufe mit deinen Pfaden:
curl -s -w ' [%{http_code}]' http://localhost:3000/api/buecher/1
curl -s -w ' [%{http_code}]' -X DELETE http://localhost:3000/api/buecher/1
Beim ersten steht das Buch da und dahinter [200]. Beim zweiten steht nur [204], denn mehr
kommt nicht zurück. Lass beim zweiten das -w einmal weg, dann antwortet der Aufruf mit einer
leeren Zeile, und damit ist klar, wofür das Anhängsel gut ist.
Die Falle im letzten Aufruf
Vergleich die beiden POST-Aufrufe im Beispiel. Derselbe Körper, einmal 201 und einmal 400. Der
einzige Unterschied ist die fehlende Kopfzeile Content-Type.
Das ist der Fund aus Lektion 10.3: curl setzt von sich aus keinen Content-Type, und ohne ihn
lässt express.json() den Körper liegen. In Express 5 ist req.body dann undefined, nicht etwa
ein leeres Objekt. Wer im Handler direkt req.body.text schreibt, bekommt an dieser Stelle einen
Absturz und antwortet mit 500 statt mit 400.
Deshalb steht in beiden Beispielen ein Rückfall auf {}, einmal als Standardargument und einmal als
??. Eine Zeile, und der Unterschied ist ein ehrlicher 400 statt eines Serverfehlers.
Und alles weg nach dem Neustart
import express from "express";
const app = express();
app.use(express.json());
const buecher = [{ id: 1, titel: "Node in der Praxis", jahr: 2024 }];
let naechsteId = 2;
app.get("/buecher", (req, res) => res.json({ anzahl: buecher.length }));
app.post("/buecher", (req, res) => {
const buch = { id: naechsteId++, titel: req.body.titel, jahr: req.body.jahr };
buecher.push(buch);
res.status(201).json(buch);
});
// Dieser Server beendet sich selbst, denn zweimal-starten.js startet
// ihn als Kindprozess und wartet auf sein Ende.
const server = app.listen(3000, async () => {
await fetch("http://127.0.0.1:3000/buecher", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ titel: "Neu", jahr: 2026 }),
});
const liste = await (await fetch("http://127.0.0.1:3000/buecher")).json();
console.log(`nach dem Anlegen ${liste.anzahl} Buecher`);
server.close();
}); import { execSync } from "node:child_process";
// Zweimal derselbe Prozess, zweimal von vorn. Nichts von dem, was der
// erste Lauf angelegt hat, ist im zweiten noch da.
for (const lauf of [1, 2]) {
const ausgabe = execSync("node fluechtig.js").toString().trim();
console.log(`Lauf ${lauf}: ${ausgabe}`);
} Zweimal derselbe Lauf, zweimal dasselbe Ergebnis. Nicht weil nichts angelegt wurde, sondern weil der zweite Lauf wieder bei einem Buch anfängt.
Deine Daten liegen in einem Array, und ein Array lebt im Prozess. Solange er läuft, funktioniert alles. Sobald er neu startet, weil du eine Zeile geändert hast, weil der Server neu gestartet wurde oder weil ein Absturz dazwischenkam, ist der Bestand wieder der vom Programmstart.
Das ist keine Schwäche deiner API. Alles, was du in diesem Abschnitt gebaut hast, bleibt richtig. Es fehlt nur ein Ort, an dem die Daten den Prozess überleben, und genau den bekommt die Bücher-API im nächsten Abschnitt.
Zum Mitnehmen
Alle sechs Bausteine des Abschnitts an einem Stück. Und ein Satz, den man ehrlich dazusagen muss: Beim Neustart ist alles wieder weg.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.