Node.js-Training: Vom Event Loop zur Architekturentscheidung
Node.js lässt sich nicht am Framework erklären, sondern am Event Loop, an Streams und an der Frage, welche Architekturentscheidung ein Team morgen noch versteht - genau das übe ich mit Teilnehmern im Schulungsrepo introduction-nodejs.
Node.js-Training: Vom Event Loop zur Architekturentscheidung
In fast jeder Node.js-Schulung, die ich bei Grossweber gebe, passiert an einem bestimmten Punkt dasselbe. Nach der Einführung, nach den ersten eigenen Skripten, fragt jemand aus der Gruppe: Warum bauen wir das nicht einfach mit Express? Die Frage ist berechtigt, und sie kommt trotzdem fast immer zu früh. Wer mit einem Framework anfängt, lernt eine API auswendig. Wer mit dem Event Loop anfängt, versteht, warum diese API so aussieht, wie sie aussieht, und was genau passiert, wenn man sie falsch benutzt. Deshalb starte ich nicht bei app.get(...), sondern bei einer einzigen Zeile, die auf den ersten Blick nichts mit Architektur zu tun hat.
Der erste Codeschnipsel, der schon alles zeigt
const fs = require("fs");
const data = fs.readFileSync("input.txt");
console.log(data.toString());
console.log("Fertig");
Das ist kein aufregender Code. Genau deshalb eignet er sich als Einstieg. readFileSync blockiert den Prozess, bis die Datei komplett gelesen ist. Erst dann läuft die nächste Zeile. Ich lasse Teilnehmer diesen Code laufen, dann verändere ich eine Zeile:
const fs = require("fs");
fs.readFile("input.txt", (err, data) => {
if (err) return console.error(err);
console.log(data.toString());
});
console.log("Fertig");
Jetzt steht "Fertig" zuerst in der Konsole, obwohl die Zeile im Quelltext danach kommt. Für Einsteiger ist das oft der erste kleine Schreckmoment: Der Code läuft nicht von oben nach unten ab, jedenfalls nicht so, wie man es aus anderen Sprachen gewohnt ist. Genau an diesem winzigen Unterschied lässt sich der ganze Rest des Trainings aufhängen. Node.js ist im Kern kein Framework und keine Sammlung von Modulen, sondern eine Entscheidung: Ein Programm soll auf einen langsamen Ein-/Ausgabevorgang nicht mit Warten reagieren, sondern mit Weitermachen.
Der Event Loop ist Architektur, keine Randnotiz
Node läuft auf der V8-Engine aus Chrome und in einem einzigen JavaScript-Thread. Das überrascht Teilnehmer regelmäßig, weil der Server trotzdem Tausende gleichzeitiger Verbindungen bedienen kann. Der Widerspruch löst sich auf, sobald man zwischen nebenläufig und parallel unterscheidet. Node arbeitet zu einem Zeitpunkt immer nur ein Stück JavaScript ab, aber während dieses Stück auf eine Antwort von der Festplatte, dem Netzwerk oder einer Datenbank wartet, gibt es die Arbeit nach unten weiter, an libuv, den Kernel oder einen internen Thread-Pool. Der Event Loop ist die Instanz, die zurückgemeldete Ergebnisse wieder abholt und die passenden Callbacks aufruft, sobald der Hauptthread frei ist.
Das ist der Punkt, an dem ich in der Schulung bewusst langsamer werde, weil hier die eigentliche Architekturaussage steckt: Nebenläufigkeit in Node ist kein Implementierungsdetail, das man ignorieren kann, solange der Code funktioniert. Sie bestimmt, welche Arbeit sofort passieren darf und welche warten muss, und sie bestimmt, was ein einzelner blockierender Aufruf für alle anderen gleichzeitig laufenden Anfragen bedeutet. Eine CPU-lastige Schleife, ein synchrones JSON.parse über mehrere Megabyte, eine verschachtelte Regex mit katastrophalem Backtracking - all das legt nicht nur die eine Anfrage lahm, die es ausgelöst hat, sondern den kompletten Prozess, weil es keinen zweiten Thread gibt, der in der Zwischenzeit übernehmen könnte.
flowchart LR
subgraph Call["Call Stack (ein Thread)"]
direction TB
JS["dein JavaScript"]
end
JS -->|"gibt I/O ab"| LibUV["libuv / Kernel / Thread-Pool"]
LibUV -->|"Ergebnis liegt vor"| Queue["Callback-Queue"]
Queue -->|"Call Stack ist leer"| JS
Für Teilnehmer, die aus Java- oder .NET-Umgebungen kommen, ist das oft die größte Umstellung. Nicht "Node ist schnell", sondern "Node ist ehrlich fair, solange man ihm keine blockierende Arbeit unterschiebt". Das ist ein Betriebsversprechen, kein Marketingsatz, und es hat direkte Konsequenzen für das, was in einem Node-Service überhaupt landen darf.
Warum Callbacks, EventEmitter, Streams und Promises in genau dieser Reihenfolge drankommen
Im Übungsrepo introduction-nodejs, das ich mit den Teilnehmern gemeinsam durcharbeite, folgen die Kapitel bewusst dieser Reihenfolge: erst Callbacks, dann EventEmitter, dann Streams, danach Promises und ganz am Ende RxJS. Das sieht aus wie eine willkürliche Liste von Async-Werkzeugen, ist aber tatsächlich eine Lernkurve mit einer klaren Logik dahinter, und die lohnt es sich einmal auszubuchstabieren.
Ein Callback liefert genau ein Ergebnis, genau einmal, und die Fehlerbehandlung ist reine Konvention: Das erste Argument ist im Fehlerfall gesetzt, sonst nicht. Diese Konvention nennt sich error-first, und sie funktioniert nur, wenn jede Funktion sie diszipliniert einhält. Ein EventEmitter geht einen Schritt weiter und liefert beliebig viele Ereignisse über die Zeit, aber er bringt keinen eingebauten Fehlerkanal mit. Ein error-Event ist reine Verabredung, und wenn niemand darauf hört, wirft Node die Ausnahme ungefangen weiter nach oben. Ein Stream ist im Kern ein spezialisierter EventEmitter, der zusätzlich zwei Dinge mitbringt, die für den Alltag entscheidend sind: eine feste Rolle (Readable, Writable, Duplex oder Transform) und Backpressure, also eine eingebaute Bremse, wenn das Ziel langsamer ist als die Quelle. Eine Promise schließlich kehrt zurück zu genau einem Ergebnis, genau einmal, gewinnt dabei aber einen echten, komponierbaren Fehlerkanal über .catch, den weder der Callback noch der EventEmitter besitzen. RxJS setzt am Ende beide Eigenschaften zusammen: viele Werte über die Zeit, wie beim EventEmitter, aber mit demselben verlässlichen Fehler- und Abschlussvertrag, den Promises eingeführt haben.
Wenn Teilnehmer diesen Bogen einmal gesehen haben, hören sie auf, nach dem "besten" Async-Muster zu fragen. Die Frage verschiebt sich zu etwas Brauchbarerem: Wie viele Werte erwarte ich, wie viele Male darf ein Fehler passieren, und wer trägt die Verantwortung dafür, ihn zu behandeln?
Streams und Backpressure am konkreten Beispiel
Kein Kapitel im Training bringt so viele Aha-Momente wie Streams, weil sie das erste Mal zeigen, dass Node-Architektur auch eine Frage von Speicher und Durchsatz ist, nicht nur von Syntax. Ein kleines, oft gezeigtes Beispiel reicht dafür völlig aus:
const fs = require("fs");
const zlib = require("zlib");
fs.createReadStream("input.txt")
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream("input.txt.gz"));
Auf den ersten Blick ist das nur eine bequeme Art, eine Datei zu komprimieren. Interessant wird die Zeile erst, wenn man sich fragt, was passiert, wenn input.txt zehn Gigabyte groß ist. Ohne Streams müsste man die komplette Datei in den Speicher laden, bevor überhaupt komprimiert werden kann. Mit pipe liest Node einen Chunk, komprimiert ihn, schreibt ihn, und erst dann kommt der nächste Chunk an die Reihe - und zwar automatisch nur so schnell, wie das Schreibziel mithalten kann. Das ist Backpressure: eine eingebaute Warteschlange, die verhindert, dass eine schnelle Quelle ein langsames Ziel mit Daten überschwemmt, bis der Prozess an seiner Speichergrenze erstickt.
flowchart LR Source["Readable (z. B. Datei)"] --> Transform["Transform (z. B. Gzip)"] Transform --> Sink["Writable (z. B. Zieldatei)"] Sink -. "Backpressure" .-> Transform Transform -. "pause / resume" .-> Source
Für die Architektur eines Backends ist das kein Nischenwissen für Dateikonvertierung. Jeder Datei-Upload, jeder CSV-Export, jeder Log-Tail und jede Weiterleitung eines HTTP-Response-Bodys profitiert von genau demselben Prinzip. Wer stattdessen den kompletten Request-Body erst in einen Buffer sammelt, bevor irgendetwas mit ihm passiert, hat eine Speichergrenze eingebaut, die im Test mit einer kleinen Testdatei nie auffällt und im Betrieb mit echten Nutzerdateien plötzlich sehr wohl.
Express ist kein Architekturmuster
Damit sind wir endlich bei der Frage angekommen, mit der die Teilnehmer eigentlich starten wollten. Express ist ein hervorragendes, schlankes Routing-Werkzeug, aber es trifft keine einzige der Entscheidungen, die eine Web-Architektur eigentlich ausmachen. Diese Entscheidungen liegen woanders: Antwortet der Server mit vollständig gerendertem HTML, wie beim klassischen Server-Side-Rendering? Antwortet er mit strukturierten Daten auf einen RPC-artigen Aufruf, bei dem eine Funktion und ihre Parameter im Body stecken? Oder folgt er dem RESTful-Gedanken, bei dem Ressourcen über URIs adressiert werden und HTTP-Methoden ihre jeweilige Bedeutung tragen?
Für Letzteres nutze ich im Training gern das Richardson Maturity Model als kleine Landkarte, weil es die Abstufungen greifbar macht. Auf Stufe 0 gibt es nur eine einzige URI, meist mit POST, und die eigentliche Aktion steckt im Body - im Grunde RPC über HTTP. Auf Stufe 1 kommen mehrere URIs für unterschiedliche Ressourcen dazu, aber noch ohne saubere Nutzung der HTTP-Methoden. Stufe 2 nutzt GET, POST, PUT, DELETE und Statuscodes so, wie sie gemeint sind: 200 für Erfolg, 201 für angelegt, 204 für kein Inhalt, 404 wenn die Ressource fehlt. Stufe 3 fügt Hypermedia hinzu, also Links in der Antwort, die dem Client sagen, welche nächsten Schritte möglich sind, statt dass der Client das vorher wissen muss. Die meisten Systeme, die ich in Projekten sehe, bleiben bei Stufe 2 stehen, und das ist meistens auch völlig in Ordnung. Wichtig ist nur, dass die Entscheidung bewusst getroffen wird und nicht einfach das Ergebnis davon ist, welche Express-Beispiele man zuerst kopiert hat.
Was Express tatsächlich beisteuert, ist etwas anderes, und es hat einen erstaunlichen Bezug zu Streams: eine Middleware-Pipeline.
const express = require("express");
const app = express();
app.use(express.json());
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
app.get("/invoices/:id", async (req, res, next) => {
try {
const invoice = await loadInvoice(req.params.id);
if (!invoice) return res.status(404).json({ error: "not_found" });
res.status(200).json(invoice);
} catch (err) {
next(err);
}
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: "internal" });
});
app.listen(3000);
Jede Middleware bekommt Request und Response, tut etwas und reicht mit next() weiter, oder sie bricht mit einem Fehler ab, der beim vier-Parameter-Error-Handler landet. Das ist strukturell dasselbe Prinzip wie bei den Streams weiter oben: eine Quelle, eine Kette von Transformationen, ein Ziel, und ein eigener Kanal für den Fehlerfall. Sobald Teilnehmer diese Verwandtschaft sehen, wird Express nicht mehr auswendig gelernt, sondern verstanden - als eine von mehreren Ausprägungen desselben Pipeline-Gedankens, der sich durch das ganze Training zieht.
Skalierung ohne Illusionen
Ein einzelner Node-Prozess nutzt genau einen Kern der CPU. Das überrascht Teilnehmer mit Servern, die acht oder sechzehn Kerne haben, regelmäßig, weil unter Last nur einer davon etwas zu tun bekommt, solange man nichts weiter unternimmt. Node bietet dafür child_process an, mit drei Werkzeugen, die unterschiedlich grobkörnig sind: spawn startet einen beliebigen Systembefehl und liefert seine Ausgabe als Stream, exec tut ähnliches, sammelt die Ausgabe aber komplett im Speicher, bevor sie zurückkommt, und fork startet gezielt ein weiteres Node-Skript als eigenen Prozess mit einem eingebauten Nachrichtenkanal. Für rechenintensive Arbeit, die den Event Loop sonst blockieren würde, ist das der saubere Weg: Arbeit auslagern, statt den einzigen Thread zu opfern.
Ein fork sieht im Übungsrepo zum Beispiel so aus:
const { fork } = require("child_process");
const worker = fork("./berechne-schwer.js");
worker.send({ zahlen: [1, 2, 3, 4, 5] });
worker.on("message", (ergebnis) => {
console.log("Ergebnis aus dem Worker-Prozess:", ergebnis);
});
Der Hauptprozess bleibt frei, während berechne-schwer.js in einem eigenen Prozess rechnet und das Ergebnis über den eingebauten Nachrichtenkanal zurückschickt. Das ist grobkörniger als ein Thread, weil jeder child_process seinen eigenen V8-Kontext und seinen eigenen Speicher mitbringt, aber genau diese Isolation ist in vielen Fällen ein Vorteil: Ein abgestürzter Worker reißt den Hauptprozess nicht mit.
Gerade jetzt, im Frühjahr 2019, wird dieses Bild interessanter. Node 10 ist die Long-Term-Support-Version, die aktuell in den meisten Projekten produktiv läuft, solide und ausgereift. Node 12 ist vor wenigen Wochen erschienen und bringt zwei Dinge mit, über die ich in der Schulung schon spreche, ohne sie im Produktivbetrieb zu empfehlen: Worker Threads, die jetzt ohne Kommandozeilen-Flag laufen, aber von den Node-Maintainern selbst noch nicht als vollständig ausgereift bezeichnet werden, und experimentelle Unterstützung für ECMAScript-Module hinter dem Flag --experimental-modules. Beides zeigt, wohin die Reise geht, aber wer heute etwas ausliefert, bleibt bei require, bei CommonJS und bei der 10er-Linie, bis die 12er zur LTS-Version wird. Ich finde diese Unterscheidung wichtig genug, um sie explizit zu machen: Neugier auf das Neue und Verantwortung für das Laufende sind zwei verschiedene Hüte, und ein gutes Training zeigt beide.
Fehler sind auch Architektur, nicht nur ein try/catch
Im Express-Beispiel weiter oben landet ein Fehler beim vier-Parameter-Handler, weil ich ihn mit next(err) explizit dorthin weitergereicht habe. Das ist der saubere Fall. Interessant wird es bei den Fehlern, die niemand explizit weitergereicht hat. Dafür bietet Node zwei globale Netze, und ich zeige Teilnehmern bewusst, dass es Netze und keine Reparaturwerkzeuge sind.
process.on("uncaughtException", (err) => {
console.error("Unerwarteter Fehler, Prozess ist in undefiniertem Zustand:", err);
// nur synchrones Aufräumen, dann beenden
process.exit(1);
});
process.on("unhandledRejection", (reason) => {
console.error("Promise ohne .catch abgelehnt:", reason);
});
Die Node-Dokumentation ist an dieser Stelle ungewöhnlich deutlich: Nach einer uncaughtException befindet sich der Prozess in einem undefinierten Zustand. Das ist keine Floskel, sondern eine Warnung. Irgendein Modul kann mitten in einer Operation stehengeblieben sein, eine Datei halb geschrieben, eine Verbindung halb aufgebaut. Der Handler ist also kein Ort, an dem man weitermacht, sondern ein Ort, an dem man so schnell und so synchron wie möglich aufräumt und den Prozess beendet, damit ein Supervisor - pm2, systemd, Kubernetes, was auch immer im jeweiligen Projekt läuft - ihn frisch neu startet. unhandledRejection ist die Promise-Variante desselben Problems: eine Promise, die abgelehnt wurde, ohne dass irgendwo ein .catch darauf gewartet hat.
Für Teilnehmer, die aus einer Welt mit zentralem Exception Handling kommen, ist das gewöhnungsbedürftig. Die eigentliche Lektion lautet: Ein globaler Handler ersetzt niemals das try/catch oder das .catch an der Stelle, an der der Fehler fachlich etwas bedeutet - dass eine Rechnung nicht gefunden wurde, dass eine Zahlung fehlgeschlagen ist, dass eine Datei nicht existiert. Er ist das letzte Sicherheitsnetz für das, was durch alle bewusst gesetzten Netze hindurchgefallen ist. Dazu passt auch, dass Node über process.on("SIGINT", ...) und verwandte Signale einen kontrollierten Shutdown erlaubt: laufende Verbindungen zu Ende bedienen, Datenbankverbindungen sauber schließen, dann erst beenden. Auch das ist keine Nebensächlichkeit, sondern derselbe Gedanke wie bei Backpressure: Ein System, das ehrlich sagt, wann es fertig ist, statt einfach abgewürgt zu werden.
Testen, bevor es wehtut
Der letzte Baustein, den ich im Training bewusst nicht auslasse, ist Testing, und zwar aus einem einfachen Grund: Alles, was bisher gezeigt wurde - Backpressure, Fehlerpfade, die Reihenfolge von Middleware - lässt sich im Handumdrehen unsichtbar kaputtmachen, wenn niemand eine Erwartung daran festgehalten hat. Ich arbeite mit Mocha, weil es 2019 ein bewährter, unaufgeregter Standard ist, und mit dem eingebauten assert-Modul, weil man dafür keine zusätzliche Bibliothek braucht, um die Idee zu verstehen.
const assert = require("assert");
const { describe, it } = require("mocha");
describe("Array.reverse", () => {
it("dreht die Reihenfolge der Elemente um", () => {
const sut = "ABC".split("");
const actual = sut.reverse();
assert.deepEqual(actual, ["C", "B", "A"]);
});
});
Arrange, Act, Assert: Testdaten aufbauen, die Funktion ausführen, das Ergebnis prüfen. Diese drei Schritte klingen banal, aber die meisten schlecht lesbaren Tests, die ich in Projekten sehe, verletzen genau diese Trennung, indem Aufbau und Prüfung munter durcheinandergemischt werden. Bei asynchronem Code zahlt sich Mocha zusätzlich aus, weil ein Test entweder einen done-Callback entgegennimmt oder schlicht eine Promise zurückgibt:
it("löst nach dem asynchronen Aufruf mit dem erwarteten Wert auf", () => {
return berechneAsynchron().then((actual) => {
assert.equal(actual, 2);
});
});
Gibt die Testfunktion eine Promise zurück, wartet Mocha automatisch darauf, bevor es das Ergebnis bewertet. Vergisst man das return, meldet der Test unter Umständen Erfolg, bevor die eigentliche Assertion überhaupt gelaufen ist - ein Fehler, der in Code-Reviews leicht übersehen wird, weil der Test ja grün ist. Für Integrationstests kommen before, after, beforeEach und afterEach dazu, um zum Beispiel eine Testdatenbank einmal aufzusetzen und nach jedem Test wieder in einen bekannten Zustand zu bringen. Die Testpyramide bleibt dabei die Leitplanke: viele schnelle Unit-Tests an der Basis, weniger, aber aussagekräftige Integrationstests in der Mitte, eine Handvoll Ende-zu-Ende-Tests an der Spitze. Wer diese Verteilung umdreht, merkt es meistens erst an der Laufzeit der Build-Pipeline, wenn es zu spät ist, um sie günstig zu ändern.
Das Übungsrepo ist selbst schon Architektur
Ein Punkt, den ich Teilnehmern gerne erst am Ende erkläre, weil er vorher zu abstrakt wäre: introduction-nodejs ist nicht einfach ein Ordner voller Markdown-Dateien, sondern selbst als Yarn-Workspaces-Monorepo aufgebaut. Im Wurzelverzeichnis steht eine package.json mit "private": true und einem workspaces-Feld, das auf einen packages-Ordner zeigt, und jedes Beispiel liegt als eigenständiges Paket darunter: ARCHITECTURE, BASICS, ECMA, EVENTEMITTER, EXPRESSJS, FUNCTIONALJS, HTTP2, RXJS, STREAMS, TESTING, TYPESCRIPT. Jedes dieser Pakete hat seine eigenen Abhängigkeiten und lässt sich einzeln installieren und ausführen, ohne dass die Beispiele sich gegenseitig verunreinigen.
Das ist kein Zufall und auch kein reines Ordnungsprinzip. Es ist dieselbe Lektion, die vorher schon bei Streams und bei Express auftauchte, nur eine Ebene höher: klare Grenzen zwischen Bausteinen, jeder mit seiner eigenen Verantwortung, verbunden über einen expliziten, sichtbaren Mechanismus statt über stille Abhängigkeiten. Ich zeige Teilnehmern bewusst diese Struktur des Übungsrepos selbst, weil ein Monorepo mit sauberen Workspace-Grenzen ziemlich genau dasselbe Architekturproblem löst wie ein Backend, das aus mehreren Services besteht: Wo darf etwas geteilt werden, und wo muss es getrennt bleiben, damit eine Änderung an einer Stelle nicht überraschend woanders etwas kaputt macht?
Wie ich die Schulung tatsächlich gebe
Ich zeige selten eine Folie, ohne direkt danach den Code aufzumachen. Die Folienübersicht zum Training gibt es zum Nachschlagen und für alle, die zwischen den Terminen noch einmal die Landkarte sehen wollen, hier: Introduction to Node.js - Folien. Aber die Folie ist die Landkarte, nicht das Gelände. Das eigentliche Lernen passiert im Repository, gemeinsam: Ich tippe, ein Teilnehmer tippt, wir bauen einen Fehler absichtlich ein und schauen uns gemeinsam an, wie die Fehlermeldung aussieht, dann reparieren wir ihn zusammen. Wer nur zuschaut, merkt sich die Syntax für den Moment. Wer selbst tippt und selbst den Callback vergisst zurückzugeben oder das await an der falschen Stelle setzt, merkt sich die Grenze - und Grenzen sind es, die ein Jahr später noch abrufbar sind, nicht Syntax.
Deshalb ist das Repo introduction-nodejs für mich kein Foliensatz in Textform, sondern das eigentliche Werkzeug der Schulung. Die 23 Kapitel, von der Einführung über REPL, npm und Yarn, Event Loop, Buffer, Streams, File System, Fehler und globale Objekte bis zu Express, Testing, Skalierung, Promises, funktionaler Programmierung, modernem ECMAScript, RxJS und TypeScript, sind als eigenständige Markdown-Dateien angelegt, die man in beliebiger Reihenfolge nachschlagen kann, aber die als Ganzes eine Lernkurve vom ersten Skript zur Betriebsreife ergeben. Die dazugehörigen EXAMPLES sind lauffähiger Code, kein Pseudocode - genau der Code, den wir im Raum gemeinsam verändern.
Was am Ende bleiben soll
Am letzten Schulungstag frage ich Teilnehmer manchmal, was sie mitnehmen. Die Antwort, die mich am meisten freut, ist selten "ich kenne jetzt Express". Sie lautet eher so etwas wie: "Ich weiß jetzt, wann ich zu einem Callback greife und wann zu einem Stream, und ich weiß, warum ein await trotzdem noch etwas ist, worüber ich nachdenken muss." Genau das ist der Unterschied zwischen einer Werkzeugliste und einem Architekturverständnis. Ein Framework kann man in einem Wochenende nachlernen. Ein Gefühl dafür, welche Entscheidung sich später rächt, braucht die Übung an echtem, manchmal absichtlich kaputtem Code.
Node.js ist genau deshalb ein gutes Trainingsobjekt, nicht weil es besonders neu oder besonders angesagt ist, sondern weil seine zentrale Designentscheidung - nicht-blockierendes I/O in einem einzigen Thread - so kompromisslos ist, dass sie ihre eigenen Konsequenzen erzwingt. Man kann sie nicht ignorieren, ohne dass der Server sich irgendwann merkwürdig verhält. Und genau an diesen merkwürdigen Momenten lernt man am meisten.
Selbst Ryan Dahl, der Node vor zehn Jahren erfunden hat, hat vor einiger Zeit in einem vielbeachteten Vortrag öffentlich einige seiner eigenen Designentscheidungen infrage gestellt und experimentiert seitdem an einem möglichen Nachfolger. Das finde ich, ganz unabhängig davon, wohin dieses Experiment führt, bemerkenswert gesund für ein Ökosystem: dass der Erfinder selbst bereit ist zu sagen, was er heute anders machen würde. Genau diese Haltung versuche ich auch in der Schulung zu vermitteln. Node.js ist kein fertiges, geschlossenes Weltbild, das man auswendig lernt, sondern eine Reihe von Entscheidungen, jede mit ihrem Preis, jede diskutierbar. Wer das einmal verinnerlicht hat, liest auch die nächste Technologie, die in fünf Jahren angesagt sein wird, nicht mehr als Wahrheit, sondern als eine weitere Menge von Entscheidungen, die man verstehen und gegebenenfalls widerlegen kann.
Weiterführende Quellen
- Übungsrepository introduction-nodejs (Basis für die gemeinsamen Übungen): https://github.com/mikebild/introduction-nodejs
- Folienübersicht zum Training: https://supabase.mikebild.dev/storage/v1/object/public/slides/introduction-nodejs.html
- Node.js, Event Loop und Timers (offizielle Dokumentation): https://nodejs.org/learn/asynchronous-work/event-loop-timers-and-nexttick
- Node.js Streams API: https://nodejs.org/api/stream.html
- Node.js 12 Release-Ankündigung: https://nodejs.org/en/blog/release/v12.0.0/
- Richardson Maturity Model, Übersicht: https://martinfowler.com/articles/richardsonMaturityModel.html
Kommentare
Kommentar schreiben