Node.js ist eine JavaScript-Laufzeitumgebung außerhalb des Browsers, die auf V8 basiert und mit APIs für Networking, Dateien, Prozesse, Streams und den Zugriff auf das Betriebssystem erweitert wurde. Es wird häufig für REST APIs, GraphQL Services, Echtzeit WebSocket Server, Microservices, CLI Tools, Proxys und API Gateways verwendet. Seine größte Stärke ist die nicht-blockierende I/O: Ein Prozess kann viele Netzwerkverbindungen effizient verwalten. Der Kompromiss besteht darin, dass langwierige CPU-intensive Berechnungen den Event Loop blockieren können, es sei denn, sie werden in Worker, Prozesse oder einen anderen Dienst ausgelagert.
JavaScript läuft in V8, während die APIs von Node.js asynchrone Aufgaben an das Betriebssystem oder an den Thread-Pool von libuv delegieren. Der Haupt-JavaScript-Thread wartet nicht darauf, dass eine I/O-Operation abgeschlossen wird; er setzt mit der Ausführung anderer Code fort. Wenn die Operation abgeschlossen ist, wird sein Callback oder Promise Continuation für den Event Loop bereit. So erreicht Node.js eine hohe Konkurrenzfähigkeit, obwohl das Benutzer-JavaScript normalerweise auf einem Hauptthread läuft.
V8 kompiliert und führt JavaScript aus und verwaltet JavaScript-Objekte und den Speicher. libuv bietet den plattformübergreifenden Event Loop, die asynchrone I/O-Integration und einen Worker Thread Pool für Operationen wie bestimmte Dateisystemarbeiten, DNS und Kryptografie. Der Event Loop entscheidet, wann Callbacks für abgeschlossene Operationen ausgeführt werden sollen. Node.js selbst verbindet JavaScript APIs mit nativen Fähigkeiten; V8 allein bietet keinen HTTP-Server, keinen Dateizugriff oder Prozess-APIs.
4Warum wird Node.js oft als Single-Thread bezeichnet?
Ein einzelner Node.js-Prozess führt normalerweise das Benutzer-JavaScript auf einem Hauptthread aus, sodass zwei normale Request-Handler nicht gleichzeitig JavaScript in diesem Thread ausführen. Die Plattform ist jedoch nicht buchstäblich ein einziger Thread: libuv verfügt über einen Thread Pool, das Betriebssystem führt asynchrone I/O durch, V8 hat Garbage-Collection-Arbeit und Node.js unterstützt auch worker_threads und Child Processes. Der Ausdruck beschreibt hauptsächlich das Standard-JavaScript-Ausführungsmodell.
5Wie kann Node.js viele Anfragen gleichzeitig verarbeiten?
Node.js startet I/O-Operationen und kehrt dann zum Event Loop zurück, anstatt für jede Anfrage einen Thread zu blockieren. Zum Beispiel können 1.000 Datenbankaufrufe im Flug sein, während der Hauptthread weiterhin Verbindungen annimmt und bereitstehende Callbacks verarbeitet. Dies eignet sich gut für I/O-gebundene Workloads wie HTTP-Aufrufe, Datenbankabfragen, Dateien und Sockets. Es funktioniert schlecht, wenn Request Handler schwere synchrone CPU-Arbeit leisten, da dies den Event Loop blockiert und jede andere Anfrage verzögert.
6Was sind die wichtigsten Phasen der Ereignisschleife von Node.js?
Node.js verwendet die libuv-Phasen pending callbacks, idle/prepare, poll, check und close callbacks sowie die Timer-Verarbeitung. poll verarbeitet viele Ein-/Ausgabe-Rückruffunktionen, check führt setImmediate-Rückruffunktionen aus, und die Timer-Verarbeitung führt setTimeout-/setInterval-Rückruffunktionen aus, sobald ihre Mindestverzögerungen und die Ablaufplanung es zulassen. Ab libuv 1.45 und Node.js 20 werden Timer in jedem Durchlauf nach poll verarbeitet; vor dem Eintritt in die Schleife findet aus Kompatibilitätsgründen ein erster Timer-Durchlauf statt. process.nextTick nutzt eine separate Node-Warteschlange; Promise-Reaktionen und queueMicrotask nutzen die Microtask-Warteschlange von V8. Keine dieser Warteschlangen ist eine libuv-Phase. Sie werden an Kontrollpunkten zwischen Rückruffunktionen verarbeitet. Wenn sie sich rekursiv immer wieder füllen, können sie andere Arbeit dauerhaft verdrängen.
7Wie unterscheiden sich synchrone, asynchrone und nicht blockierende Operationen?
Synchron beschreibt den Kontrollfluss: Ein Aufruf liefert sein Ergebnis oder löst eine Ausnahme aus, bevor die aufrufende Funktion fortfährt. Asynchron bedeutet, dass ein Ergebnis später über eine Rückruffunktion, ein Promise oder eine andere Benachrichtigung geliefert wird. Blockierend bedeutet, dass beim Warten auf Arbeit oder eine Ressource der aktuelle Thread keine andere Arbeit ausführen kann. Nicht blockierende Schnittstellen vermeiden dieses Warten, etwa indem sie «würde blockieren» zurückgeben oder eine Benachrichtigung veranlassen. Das sind verschiedene Dimensionen: Synchron bedeutet für sich genommen nicht, dass eine Schnittstelle auf Ein-/Ausgabebereitschaft wartet; die Rückgabe eines Promise macht aufwendige synchrone Berechnungen nicht nichtblockierend. In Node.js ist fs.readFileSync eine blockierende synchrone Schnittstelle; fs.promises.readFile liefert sein Ergebnis asynchron, während der Hauptthread andere Arbeit verarbeiten kann. Bevorzuge geeignete asynchrone Ein-/Ausgabe-Schnittstellen bei der Verarbeitung von Anfragen.
8Was bedeutet das Verhungern der Ereignisschleife?
Die Ereignisschleife verhungert, wenn synchrones JavaScript oder ständig nachgefüllte Warteschlangen an Kontrollpunkten die rechtzeitige Verarbeitung von Ein-/Ausgabe-Rückruffunktionen, Timern und Anfragen verhindern. Ursachen sind aufwendige Berechnungen, große JSON-Operationen, rekursiv eingeplante Microtasks und übermäßige Nutzung von process.nextTick. Begrenze Eingabegrößen, untersuche teure Arbeit mit einem Profiler und verlagere rechenintensive Aufgaben in worker_threads oder einen anderen Dienst. Wenn du Arbeit in Teilstücke zerlegst, gib die Kontrolle an einen späteren Durchlauf der Ereignisschleife ab, etwa mit setImmediate. Wiederholtes await auf bereits aufgelösten Promises oder das Einplanen von queueMicrotask kann die Ein-/Ausgabe weiterhin verdrängen. Vermeide blockierende synchrone Schnittstellen in häufig ausgeführten Anfragepfaden.
9Was ist eine Rückruffunktion, und was versteht man unter Callback Hell?
Eine Rückruffunktion wird einer anderen Operation übergeben, damit diese sie aufruft. Sie kann synchron ausgeführt werden, wie bei Array.map, oder asynchron, wie bei fs.readFile. Klassische asynchrone Node.js-Rückruffunktionen verwenden häufig die Konvention mit dem Fehler als erstem Argument: callback(error, result). Callback Hell bezeichnet tief verschachtelte, voneinander abhängige Rückruffunktionen, die Kontrollfluss und Fehlerbehandlung unübersichtlich machen. Benannte Funktionen, kleinere Operationen, Promises und async/await können die Lesbarkeit verbessern. Starte unabhängige Operationen gegebenenfalls gemeinsam, etwa mit Promise.all, und berücksichtige dabei Parallelitätsgrenzen und Fehlerbehandlung.
import fs from 'node:fs';
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
10Was ist ein Promise und wie funktioniert async/await?
Ein Promise repräsentiert das zukünftige Ergebnis einer asynchronen Operation und kann im Zustand pending, fulfilled oder rejected sein. Eine async-Funktion gibt immer ein Promise zurück. await pausiert nur die aktuelle async-Funktion, bis das Promise aufgelöst wird; es blockiert nicht den gesamten Node.js-Prozess. Eine gute Antwort sollte Fehlerbehandlung mit try/catch erwähnen, das Vermeiden von fliegenden Promises und die Verwendung von Promise.all für unabhängige Aufgaben, damit Operationen nicht unnötigerweise sequenziell ablaufen.
11Wie unterscheiden sich process.nextTick, queueMicrotask, setImmediate und setTimeout?
process.nextTick plant Arbeit in der Next-Tick-Warteschlange von Node ein; queueMicrotask verwendet die Microtask-Warteschlange von V8, die auch Promise-Reaktionen nutzt. An einem normalen Kontrollpunkt auf oberster Ebene in CommonJS laufen nextTick-Rückruffunktionen vor eingeplanten Promise-/queueMicrotask-Rückruffunktionen. Auf oberster Ebene eines ES-Moduls erfolgt die Auswertung bereits in einem Microtask-Kontext, sodass eingeplante Microtasks vor nextTick laufen können. Das hängt vom Kontext ab und ist keine universelle Prioritätsregel, die laufendes JavaScript unterbricht. setImmediate läuft in check. setTimeout(fn, 0) wartet auf eine Mindestverzögerung und wird nicht sofort ausgeführt. Ihre relative Reihenfolge hängt vom Ort der Einplanung ab; werden beide innerhalb einer Ein-/Ausgabe-Rückruffunktion eingeplant, läuft setImmediate zuerst. Bevorzuge queueMicrotask für portables Aufschieben als Microtask, sofern das besondere Verhalten von nextTick nicht benötigt wird.
// CommonJS file (.cjs): nextTick precedes promise at this top level.
// ES module file (.mjs): promise precedes nextTick at this top level.
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// timeout vs immediate is context-dependent here.
Verwende try/catch für synchron ausgelöste Ausnahmen. Bei Promise-basierten Operationen nutze await innerhalb von try/catch oder füge einen Handler für Ablehnungen hinzu. Ein äußeres synchrones try/catch fängt keine Ausnahme ab, die eine spätere Rückruffunktion auslöst. Rückruffunktionen mit Fehlerargument an erster Stelle müssen dieses prüfen; error-Ereignisse eines EventEmitter benötigen passende Handler. Server sollten die Fehlerbehandlung für Anfragen zentralisieren, hilfreichen Kontext protokollieren und interne Details nicht an Clients weitergeben. Nach einer uncaughtException darf nicht davon ausgegangen werden, dass der Prozess sicher weiterlaufen kann: Führe nur notwendige synchrone Notfallbereinigung aus, beende den Prozess und lasse ihn durch eine Prozessüberwachung neu starten. Ein geordnetes Herunterfahren ist bei normaler Beendigung sinnvoll; beliebige asynchrone Wiederherstellungsversuche nach einer unbehandelten Ausnahme sind keine zuverlässige Strategie.
Ein Modul ist eine isolierte Code-Einheit mit einer explizit exportierten Schnittstelle. Node.js unterstützt eingebaute Module wie node:fs und node:http, lokale Projektmodule, npm-Pakete, CommonJS-Module und ECMAScript-Module. Module machen Verantwortungsgrenzen klarer und ermöglichen Wiederverwendung. Der Produktionskompromiss ist die Kopplung (Coupling): Wenn Module sich zu tief importieren oder Nebenwirkungen zum Importzeitpunkt verbergen, wird das Testen und das Startverhalten schwerer nachvollziehbar.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
14Wie unterscheiden sich require in CommonJS und import in ES-Modulen?
CommonJS verwendet require und module.exports, ES-Module verwenden import und export. require lädt normalerweise synchron und kann bedingt aufgerufen werden. Statische import-Deklarationen machen den Abhängigkeitsgraphen für Analysen sichtbar; dynamisches import() gibt ein Promise zurück. Moderne Node.js-Versionen können nach den unterstützten Interoperabilitätsregeln auch einen ES-Modulgraphen ohne await auf oberster Ebene mit require laden. ES-Module bedeuten daher nicht grundsätzlich asynchrone Auswertung. Die Endungen .cjs/.mjs, type in package.json und die jeweils geltenden Regeln zur Syntaxerkennung bestimmen den Modulmodus. ES-Module unterstützen await auf oberster Ebene. CommonJS stellt __dirname/__filename bereit; aktuelle Node.js-Versionen bieten für dateibasierte ES-Module import.meta.dirname/filename. import.meta.url eignet sich für die URL-basierte Auflösung und für Ansätze, die auch mit älteren Versionen funktionieren.
// File: math.cjs (CommonJS)
function calculate(a, b) { return a + b; }
module.exports = { calculate };
// Separate file: math.mjs (ES module)
export function calculate(a, b) { return a + b; }
// These are two separate files, not one combined script.
15Was sind npm, package.json und package-lock.json?
npm ist die Befehlszeilenschnittstelle zur Paketverwaltung. package.json beschreibt Metadaten, Skripte und Anforderungen an direkte Abhängigkeiten, häufig als Versionsbereiche. package-lock.json hält einen aufgelösten Abhängigkeitsbaum mit Paketversionen und Integritätsinformationen fest. Nimm die Lockdatei einer Anwendung in die Versionsverwaltung auf. npm ci eignet sich für die kontinuierliche Integration: Es installiert anhand der Lockdatei, entfernt ein vorhandenes node_modules-Verzeichnis und schlägt fehl, wenn package.json und Lockdatei nicht übereinstimmen. Eine Lockdatei verringert unbeabsichtigte Änderungen an Abhängigkeiten; sie vereinheitlicht weder Betriebssysteme noch native Builds, Lebenszyklusskripte, Node-Versionen oder Installationsoptionen. Reproduzierbare Builds benötigen zusätzlich eine kontrollierte Werkzeugkette und Umgebung.
16Wie sollten Konfiguration und Umgebungsvariablen verwaltet werden?
Lies Umgebungsvariablen über process.env: Vorhandene Werte sind Zeichenketten, eine fehlende Eigenschaft ergibt undefined. Prüfe erforderliche Werte und wandle sie beim Start gezielt um. Die Umwandlung von PORT allein mit Number weist weder eine leere Zeichenkette noch NaN oder einen Port außerhalb des zulässigen Bereichs zurück. Brich mit einer klaren Fehlermeldung ab, wenn erforderliche Konfiguration ungültig ist. Halte echte Geheimnisse aus der Versionsverwaltung heraus und stelle Produktionsgeheimnisse über die Bereitstellungsplattform oder eine Geheimnisverwaltung bereit; eine Beispielkonfiguration ohne Geheimnisse darf eingecheckt werden. NODE_ENV ist eine Anwendungskonvention und keine vertrauenswürdige Sicherheitsgrenze.
const rawPort = process.env.PORT ?? '3000';
if (!/^\d+$/.test(rawPort)) throw new Error('PORT must be an integer');
const port = Number(rawPort);
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
throw new Error('PORT must be between 1 and 65535');
}
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl?.trim()) throw new Error('DATABASE_URL is required');
EventEmitter implementiert ein Publish-Subscribe-Muster innerhalb eines Node.js-Prozesses. on registriert einen Listener, once registriert einen Listener für ein Ereignis, off entfernt einen Listener und emit ruft die Listener synchron in der Reihenfolge ihrer Registrierung auf. Die synchrone Natur ist wichtig: Ein langsamer Listener blockiert den Emitter. Das Error-Ereignis ist besonders, da ein unbehandelter Error-Event den Prozess abstürzen lassen kann. Langfristig laufende Systeme sollten auch nicht verwendete Listener entfernen, um Speicherlecks zu vermeiden.
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
emitter.on('orderCreated', order => {
console.log(`Order ${order.id} created`);
});
emitter.emit('orderCreated', { id: 42 });
Buffer repräsentiert eine feste Folge von Bytes. Es wird für Dateien, TCP-Sockets, HTTP-Bodies, Bilder, Kryptographie und binäre Protokolle verwendet. Buffer.from erstellt einen Puffer aus vorhandenen Daten, Buffer.alloc erstellt speicher mit Nullen, und Buffer.allocUnsafe ist schneller, kann aber alte Speicher enthalten, bis er überschrieben wird. Das macht allocUnsafe nur dann geeignet, wenn der Code den Puffer sofort füllt und niemals veraltete Daten preisgibt.
const buffer = Buffer.from('Hello', 'utf8');
console.log(buffer.toString('utf8'));
const safe = Buffer.alloc(1024);
// Prefer alloc for general use. Never expose allocUnsafe bytes before filling them.
const fast = Buffer.allocUnsafe(1024);
fast.fill(0); // All bytes initialized before use.
Streams verarbeiten Daten in Blöcken (Chunks), anstatt alles in den Speicher zu laden. Die Haupttypen sind Readable, Writable, Duplex und Transform. Häufige Beispiele sind Dateien, HTTP-Anfrage- und Antwortobjekte, TCP-Sockets und gzip-Streams. Streams sind für große Dateien und Netzwerkverkehr wertvoll, da sie den Speicherverbrauch und die Latenz reduzieren. Eine professionelle Antwort sollte erwähnen, dass Stream-Fehler über die gesamte Kette korrekt behandelt werden müssen.
20Was ist Backpressure und warum ist eine Pipeline nützlich?
Backpressure tritt auf, wenn eine Datenquelle Chunks schneller erzeugt, als das Ziel sie verarbeiten kann. Ohne Backpressure kann der Speicher wachsen, bis der Prozess langsamer wird oder abstürzt. Streams und Pipes können den Fluss koordinieren, aber `pipeline` aus `node:stream/promises` ist sicherer für mehrstufige Ketten, weil es Fehler weitergibt, verwandte Streams schließt und ein Promise zurückgibt. Es ist ein guter Standardwert für Dateikomprimierung, Uploads, Downloads und Stream-Transformationen.
import fs from 'node:fs';
import zlib from 'node:zlib';
import { pipeline } from 'node:stream/promises';
await pipeline(
fs.createReadStream('input.txt'),
zlib.createGzip(),
fs.createWriteStream('input.txt.gz')
);