mitmario.dev

Synthese: die Bücher-API

Node.js Sandbox 3 Min Lesezeit 3 BeispieleLektion 7 von 8

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

Die API am Stück
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.

Prüfen mit curl
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

Alles weg nach dem Neustart
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, kostenlos

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