Startseite / Artikel

Vorbereitung auf Interviews zu Node.js und Express: 70 Fragen mit präzisen Antworten.

Eine strukturierte Übersicht über 70 Interviewfragen zu Node.js und Express, von Event Loops und Streams bis hin zu Middleware, Sicherheit und Skalierung, inklusive Korrekturen für gängige Antworten.

Verlag: REACTAPP.TOP21 Min. Lesezeit4861 Wörter

Quellen: Node.js & Express.js Interview Guide

Bei Backend-Interviews für Node.js-Stellen dreht sich alles meist um dieselben Themen: wie ein eindrahtiger Laufzeitumgebung Tausende von Verbindungen bedienen kann, was der Event-Loop tatsächlich bewirkt und wie Express einen rohen HTTP-Server in eine Anwendung umwandelt. Auswendig gelernte Antworten funktionieren bei nachfolgenden Fragen nicht mehr, deshalb gruppiert diese Übersicht 70 häufige Fragen nach Themen und liefert zu jeder Antwort ausreichende Begründungen, damit man sie verteidigen kann. Wenn die gängige Antwort veraltet oder ungenau ist, wird dies angegeben – denn das Erkennen solcher Details unterscheidet oft einen starken Kandidaten von einem nur vorbereiteten.

Grundlagen von Node.js

Frage 1: Was ist Node.js?

Es handelt sich um eine Laufzeitumgebung, die JavaScript außerhalb des Browsers ausführt. Sie enthält Google’s V8-Engine sowie APIs für Dateien, Netzwerkanfragen und Prozesse. Ihr Modell ist ereignisgesteuert mit nicht blockierender Eingabe/Ausgabe, wodurch sie leicht bleibt und sich hervorragend für datenintensive sowie Echtzeit-Dienste eignet.

Frage 2: Ist Node.js einheitsträgerbasiert?

Ihr JavaScript läuft auf einem Hauptthread, demjenigen, der den Ereigniszyklus antreibt. Im Hintergrund hält libuv einen Threadpool für Aufgaben bereit, die keine geeignete asynchrone OS-Schnittstelle haben, wie beispielsweise Dateisystemaufrufe, dns.lookup, einige crypto-Funktionen und zlib. Die genaue Antwort lautet daher: einheitsträgerbasiert für Ihren Code, mehrheitsträgerbasiert auf Systemebene.

Frage 3: Warum ist Node.js schnell?

  • V8 kompiliert JavaScript in Maschinencode anstelle dessen, es zu interpretieren.
  • Eingabe und Ausgabe sind asynchron und nicht blockierend.
  • Der Event-Loop bietet Konkurrenz ohne einen Thread pro Anfrage.
  • Das Modell ist besonders geeignet für arbeitsschweres Netzwerk- und Festplattenarbeiten; arbeitsschweres CPU-Arbeiten ist seine Schwachstelle.
  • Frage 4: Synchroner versus asynchroner Code

    Eine synchrone Operation wird vor dem Ausführen der nächsten Zeile abgeschlossen und blockiert alles danach. Eine asynchrone Operation, wie zum Beispiel das Lesen einer Datei oder eine Datenbankabfrage, wird gestartet und weitergeleitet; die Ausführung setzt fort, und ein Callback oder eine Promise liefert das Ergebnis später. In der untenstehenden synchronen Version wartet das Protokoll auf das gesamte Lesen (und gibt einen Buffer aus, da keine Kodierung übergeben wird):

    // Synchronous
    const data = fs.readFileSync('file.txt');
    console.log(data);
    

    In der asynchronen Version wird die letzte Protokollzeile bereits vor dem Inhalt der Datei ausgegeben, weil das Lesen in einem späteren Schritt des Event-Loops abgeschlossen wird:

    // Asynchronous
    fs.readFile('file.txt', (err, data) => {
      console.log(data);
    });
    console.log("This runs first!");
    

    Frage 5: Was ist npm?

    npm ist der Paketmanager, der mit Node.js geliefert wird. Er dient dazu, Abhängigkeiten zu installieren, zu veröffentlichen und zu verwalten, und er speichert diese zusammen mit Skripten in package.json.

    Frage 6: package.json gegenüber package-lock.json

    package.json beschreibt das Projekt: seinen Namen, die Version, den Einstiegspunkt, die Skripte sowie die zulässigen Abhängigkeitsbereiche. package-lock.json legt die genaue, festgelegte Version jedes installierten Pakets fest, einschließlich der transitiven Abhängigkeiten, sodass auf jedem Rechner und bei jedem CI-Run derselbe Paketbaum installiert wird. Committen Sie beides.

    Frage 7: Welche Kernmodule sollten Sie kennen?

    Zu den häufig genannten eingebauten Modulen gehören fs, path, http und https, os, events, stream, buffer, crypto, util, url, querystring (das heute als veraltet gilt und durch URLSearchParams ersetzt wurde), child_process, cluster und zlib.

    Q8. require() versus import

    Die Unterschiede zwischen CommonJS und ES-Modulen tauchen ständig auf:

    • require lädt synchron; ES-Module werden asynchron geladen und bieten außerdem eine dynamische import()-Funktion, die ein Promise zurückgibt.
    • CommonJS ist die Standardlösung für .js-Dateien; ES-Module benötigen den Eintrag "type": "module" in package.json oder die Erweiterung .mjs.
  • CommonJS exportiert mit module.exports; ES-Module verwenden export und export default.
  • require kann überall aufgerufen werden, auch innerhalb von Bedingungen; statische import-Deklarationen werden hochgehoben und müssen auf der obersten Ebene stehen.
  • Q9. module.exports versus exports

    exports beginnt als Referenz auf dasselbe Objekt wie module.exports. Das Hinzufügen von Eigenschaften funktioniert, doch das Zuweisen eines neuen Werts (exports = {...}) bindet lediglich die lokale Variable neu, und das Modul exportiert weiterhin das ursprüngliche Objekt. Um den gesamten Export zu ersetzen, muss man module.exports zuweisen.

    Q10. Was ist das REPL?

    REPL steht für Read, Eval, Print, Loop. Wenn man node ohne Argumente ausführt, wird eine interaktive Shell geöffnet, die jede eingegebene Ausdruckserstellung auswertet und das Ergebnis anzeigt – was für schnelle Experimente sehr praktisch ist.

    Der Event-Loop und asynchrone Muster

    F11. Was ist der Event-Loop?

    Es handelt sich um den Mechanismus, der es einem einzigen JavaScript-Thread ermöglicht, blockierende Eingabe/Ausgabe-Aufgaben auszuführen. Wenn der Aufrufstapel leer ist, nimmt der Loop den nächsten bereiten Callback aus seinen Warteschlangen und führt ihn aus. Eine ausführlichere Erklärung dazu, wie er mit libuv zusammenwirkt, finden Sie in unserem Artikel über libuv, den Event-Loop und den Thread-Pool.

    F12. Welche Phasen gibt es beim Event-Loop?

    1. Timer: Callbacks von setTimeout und setInterval, deren Zeit abgelaufen ist.
  • Ausstehende Callbacks: bestimmte Callbacks für Systemoperationen, die aus der vorherigen Iteration verschoben wurden.
  • Inaktiv und Vorbereitung: interne Aufgabenbereinigung.
  • Pollen: Sammlung neu abgeschlossener Eingabe/Ausgabe-Ereignisse und Ausführung ihrer Callbacks.
  • Überprüfung: setImmediate()-Callbacks.
  • Schließungs-Callbacks: Handler wie socket.on('close').
  • Die Microtask-Queues (process.nextTick zuerst, anschließend Promise-Reaktionen) werden nach jedem einzelnen Callback geleert, nicht nur zwischen Phasen. Dieses Verhalten besteht bereits seit Node 11 und ist eine häufig gestellte Nachfrage.

    Frage 13: nextTick, Promises, setTimeout und setImmediate

    • process.nextTick()-Callbacks werden so schnell wie möglich ausgeführt, sobald die aktuelle Operation abgeschlossen ist, vor den Microtasks der Promises.
  • Promise-Reaktionen wie .then werden ausgeführt, sobald der nextTick-Warteschlange nichts mehr hinzugefügt werden kann, aber noch bevor der Loop fortfährt.
  • setTimeout(fn, 0) wird in der Timers-Phase ausgelöst, sobald die minimale Verzögerung vergangen ist.
  • setImmediate() wird in der Check-Phase ausgelöst, unmittelbar nach der Poll-Phase derselben Iteration.
  • Dieses Skript nutzt alle vier Mechanismen:

    console.log('start');
    setTimeout(() => console.log('setTimeout'), 0);
    setImmediate(() => console.log('setImmediate'));
    process.nextTick(() => console.log('nextTick'));
    Promise.resolve().then(() => console.log('promise'));
    console.log('end');
    

    Die erwartete Ausgabe zeigt zunächst die synchronen Protokolleanmeldungen und danach die Microtasks. Die beiden Macrotasks können in beliebiger Reihenfolge angezeigt werden, wenn sie vom Hauptmodul aus geplant werden, da dies davon abhängt, ob zum Start des Loops bereits die Timerschwelle überschritten wurde; innerhalb einer Datei- oder Socket-Callback-Funktion hat setImmediate immer Vorrang:

    // Output:
    // start
    // end
    // nextTick
    // promise
    // setTimeout (or setImmediate — order can vary if not inside I/O)
    // setImmediate (or setTimeout)
    

    Q14. Callbacks und Callback-Hell

    Ein Callback ist eine Funktion, die man an eine Operation weitergibt, damit sie aufgerufen werden kann, sobald diese abgeschlossen ist. Wenn jeder Schritt vom vorherigen abhängt, stapeln sich die Callbacks immer tiefer und erzeugen so die als „Callback-Hell“ bekannte Pyramide, bei der es schwierig ist, den Code zu lesen und Fehler zu verarbeiten:

    getUser(id, (user) => {
      getPosts(user, (posts) => {
        getComments(posts, (comments) => {
          // deeply nested - hard to manage
        });
      });
    });
    

    Promises sowie async/await glätten diese Struktur.

    Q15. Was ist ein Promise?

    Ein Promise ist ein Objekt, das den zukünftigen Ergebniswert einer asynchronen Operation darstellt. Es befindet sich entweder im Wartezustand, wurde mit einem Wert erfüllt oder mit einem Grund abgelehnt, und sobald sein Zustand festgelegt ist, ändert er sich nicht mehr. Mit .then und .catch fügt man Handlungsregeln hinzu:

    const promise = new Promise((resolve, reject) => {
      if (success) resolve(data);
      else reject(error);
    });
    promise.then(data => console.log(data)).catch(err => console.error(err));
    

    Q16. Was ist async/await?

    Es handelt sich um eine auf Promises basierende Syntax, die es asynchronem Code ermöglicht, von oben nach unten gelesen zu werden. Eine async-Funktion gibt immer ein Promise zurück, und await pausiert diese Funktion, bis das erwartete Promise abgeschlossen ist; Ablehnungen treten als Ausnahmen auf, die gefangen werden können:

    async function getUser() {
      try {
        const res = await fetch('/api/user');
        const data = await res.json();
        return data;
      } catch (err) {
        console.error(err);
      }
    }
    

    Beachten Sie, dass dieses Beispiel den Fehler protokolliert und implizit undefined zurückgibt, sodass Aufrufer keinen Unterschied zwischen einem Fehler und einem leeren Ergebnis erkennen können. Das Wiederwerfen der Ausnahme ist oft die bessere Wahl.

    Q17. Promise-Combinatoren

    • Promise.all() wird erfüllt, wenn alle Eingaben erfüllt sind, und wird abgelehnt, sobald eine der Eingaben abgelehnt wird.
    • Promise.race() wird mit der ersten abgeschlossenen Eingabe erfüllt oder abgelehnt, unabhängig davon, ob sie erfüllt oder abgelehnt wurde.
    • Promise.allSettled() wartet auf alle Eingaben und gibt jedes Ergebnis an.
  • Promise.any() erfüllt sich mit dem ersten erfüllten Eingang und lehnt ab (mit einem AggregateError) nur dann ab, wenn alle Eingänge abgelehnt werden.
  • Frage 18: Blockierender versus nicht-blockierender Code

    Blockierender Code stoppt alles andere, bis er abgeschlossen ist, wie es fs.readFileSync tut. Nicht-blockierender Code startet die Arbeit und lässt das Programm weiterlaufen, wie es fs.readFile tut. Auf einem Server blockiert ein solcher blockierender Aufruf alle laufenden Anfragen.

    Ströme, Puffer und Ereignisse

    Frage 19: Was sind Ströme?

    Ströme verarbeiten Daten schrittweise, Stück für Stück, anstatt sie alle im Speicher zu halten. Sie sind das richtige Werkzeug für große Dateien, Medieninhalte und Netzwerkverkehr. Es gibt vier Arten:

    • Lesebar, wie z. B. fs.createReadStream
    • Schreibbar, wie z. B. fs.createWriteStream
    • Duplex, gleichzeitig les- und schreibbar, wie z. B. ein TCP-Socket
    • Transform – ein Duplex-Stream, der die Daten während des Durchlaufs verändert, wie z. B. zlib.createGzip

    Q20. Was ist ein Buffer?

    Ein Buffer speichert rohe binäre Daten in einem außerhalb des V8-Heaps zugewiesenen Speicherbereich. Man trifft auf ihn überall dort, wo es sich um Bytes handelt und nicht um Strings oder Objekte: Streams, Dateien, Sockets. Die Erstellung eines Buffers aus einem String zeigt die zugrunde liegenden Bytewerte im Hexadezimalformat an:

    const buf = Buffer.from('Hello');
    console.log(buf); // <Buffer 48 65 6c 6c 6f>
    

    Q21. Was ist EventEmitter?

    EventEmitter aus dem events-Modul implementiert das Observer (Pub/Sub)-Muster, auf dem ein Großteil der Node-API basiert. Man erstellt eine Instanz davon:

    const EventEmitter = require('events');
    const emitter = new EventEmitter();
    

    danach registriert man Zuhörer mit on und triggert sie mit emit, wobei alle Argumente mit übergeben werden:

    emitter.on('greet', (name) => console.log(`Hello, ${name}`));
    emitter.emit('greet', 'John');
    

    Listener werden synchron und in der Reihenfolge ihrer Registrierung ausgeführt, wenn emit aufgerufen wird.

    Q22. Wie verarbeitet man große Datenmengen, ohne an Speicher zu scheitern?

    Nutzen Sie Streams: Lesen Sie kleine Datenblöcke und leiten Sie einen lesbaren Stream in einen schreibbaren um, sodass der Speicherverbrauch unabhängig von der Dateigröße in etwa konstant bleibt:

    fs.createReadStream('input.txt').pipe(fs.createWriteStream('output.txt'));
    

    Für Produktionscode ist stream.pipeline vorzuziehen gegenüber .pipe(), da es Fehler weiterleitet und jeden Stream in der Kette bereinigt.

    Erläutertere Laufzeitkonzepte

    Q23. process.nextTick() versus setImmediate(), im Detail

    nextTick-Callback-Funktionen werden ausgeführt, bevor der Loop fortfahren darf – sogar vor den Promise-Microtasks. setImmediate-Callback-Funktionen warten auf die Überprüfungsphase, nach der Abfragephase. Da jede nextTick-Funktion eine weitere anfordern kann, kann eine rekursive Kette den Loop lahmlegen, sodass ausstehende Lese- und Schreibvorgänge niemals bearbeitet werden. Dieser Fehlerfall wird in wie process.nextTick den Event-Loop stillschweigend lahmlegt erläutert.

    Q24. Was ist Clustering und warum wird es verwendet?

    Ein einzelner Node-Prozess verwendet einen Kern für JavaScript. Das cluster-Modul erstellt mehrere Worker-Prozesse, von denen jeder seinen eigenen Event-Loop und Speicher hat, und ermöglicht es ihnen, einen Server-Port zu teilen, damit alle Kerne des Rechners Traffic abwickeln können. Sie importieren die Module wie folgt:

    const cluster = require('cluster');
    const os = require('os');
    

    Dann wird für jede CPU ein Worker vom Hauptprozess abgespalten, während die Worker den Server laden:

    if (cluster.isMaster) {
      os.cpus().forEach(() => cluster.fork());
    } else {
      require('./server');
    }
    

    cluster.isMaster funktioniert zwar noch, ist aber inzwischen veraltet und wird durch cluster.isPrimary ersetzt; verwenden Sie in neuem Code letzteres.

    Q25. cluster versus worker_threads

    • cluster erstellt separate Prozesse mit isolierter Speicherung, was sich gut für die Skalierung von HTTP-Servern eignet.
    • worker_threads erstellt Threads innerhalb eines Prozesses. Sie können über SharedArrayBuffer oder durch den Transfer von Buffern Speicher teilen, was sich für rechenintensive Aufgaben wie Bildverarbeitung eignet.

    Q26. Wie werden rechenintensive Aufgaben abgewickelt?

    Codeteile, die stark von der CPU beansprucht werden, blockieren den Event-Loop – daher sollten sie an einen anderen Ort verlegt werden: in worker_threads, in einen Kindprozess oder ganz aus dem Service heraus über eine Warteschlange und Hintergrundarbeiter. Siehe die Methode zum Auslagern von CPU-intensiver Arbeit mit Worker-Threads und Pools für entsprechende Muster.

    Q27. Was ist Middleware im Allgemeinen?

    Middleware sind Codeblöcke, die zwischen der Empfangnahme einer Anfrage und dem Senden der Antwort liegen und dort Inspektionen, Transformationen, Validierungen oder Protokollierungen durchführen können. Express basiert ganz auf diesem Konzept, wie die folgenden Abschnitte zeigen.

    Q28. Was sind Kindprozesse?

    Die child_process-Modul führt externe Programme oder andere Node-Skripte in separaten Prozessen aus. Vier Funktionen sind dabei wichtig:

    • spawn() streamt die Ausgabe, wodurch große Mengen an Daten gut verarbeitet werden können.
  • exec() führt einen Shell aus und puffert die Ausgabe, was für kleine Ergebnisse geeignet ist.
  • execFile() führt eine Datei direkt ohne Shell aus, was bei unzuverlässigen Argumenten sicherer ist.
  • fork() ist eine spezialisierte Version von spawn für Node-Module, die ein IPC-Kanal für Nachrichten einrichtet.
  • Q29. Was ist libuv?

    libuv ist die C-Bibliothek, die den Event-Loop von Node, asynchrone Eingabe/Ausgabe sowie einen Thread-Pool bereitstellt und die Unterschiede zwischen den Event-Benachrichtigungsmechanismen von Linux, macOS und Windows versteckt.

    Q30. setTimeout versus setInterval

    setTimeout(fn, delay) führt fn mindestens nach delay Millisekunden einmal aus. setInterval(fn, delay) führt sie in diesem Intervall wiederholt aus, bis clearInterval() aufgerufen wird.

    Q31. Umgang mit nicht gefangenen Ausnahmen und unverarbeiteten Ablehnungen

    Es existieren Prozessebene-Handler für beide Fälle. Bei synchronen Ausnahmen, die niemand gefangen hat, sollte man protokollieren und den Prozess beenden, da dieser sich in einem inkonsistenten Zustand befinden könnte:

    process.on('uncaughtException', (err) => {
      console.error('Uncaught Exception:', err);
      process.exit(1); // recommended: exit after cleanup
    });
    

    Bei Promises, die ohne Handler abgelehnt werden:

    process.on('unhandledRejection', (reason, promise) => {
      console.error('Unhandled Rejection:', reason);
    });
    

    Seit Node 15 führt eine unverarbeitete Ablehnung standardmäßig zum Absturz des Prozesses, wodurch die Registrierung dieses Listeners dieses Verhalten ändert. Behandeln Sie beide Fälle als letztes Mittel zur Protokollierung und nicht als Teil des Kontrollflusses; verwenden Sie stattdessen try/catch sowie .catch() direkt im Code.

    Q32. Was ist ein Speicherverlust und wie findet man ihn?

    Ein Leck ist Speicher, der weiterhin erreichbar bleibt, obwohl er nicht mehr benötigt wird: wachsende globale Caches, nie entfernte Timer oder Zuhörer, Schließfunktionen, die große Objekte speichern. Diagnostizieren Sie das Problem mit dem --inspect-Flag und den Heap-Snapsshots der Chrome DevTools, oder mit Tools wie heapdump und clinic.js, und vergleichen Sie die Snapsshots im Laufe der Zeit.

    Q33. __dirname versus __filename

    __dirname ist der absolute Pfad zum Verzeichnis, das den aktuellen Modul enthält; __filename ist der absolute Pfad zum Datei selbst. Beide existieren nur in CommonJS. In ES-Modulen werden sie aus import.meta.url abgeleitet, oder in neueren Node-Versionen werden import.meta.dirname und import.meta.filename verwendet.

    Q34. Warum sollte man das Path-Modul verwenden?

    path erstellt und analysiert Dateipfade portabel mithilfe von Funktionen wie path.join, path.resolve, path.basename und path.extname, sodass Sie niemals Strings manuell zusammenfügen müssen und aufgrund von Windows-Backslashes im Vergleich zu Unix-Slashes Probleme bekommen.

    Q35. fs.readFile versus fs.createReadStream

    fs.readFile lädt das gesamte Dateiinhalt in den Speicher, bevor es zurückruft, was bei großen Dateien riskant ist. fs.createReadStream liefert die Datei in Blöcken, hält den Speicherverbrauch niedrig und ermöglicht ein sofortiges Verarbeiten.

    Essentielle Aspekte von Express

    Q36. Was ist Express?

    Express ist ein minimalistisches, unpräferenziertes Web-Framework auf Basis von Node’s http-Modul. Es fügt Routing, eine Middleware-Pipeline sowie die Integration eines Template-Engines hinzu und beseitigt den größten Teil der Standardprozeduren bei der Verarbeitung von Anfragen.

    Q37. Ein einfacher Express-Server

    Erstellen Sie ein Anwendungsobjekt:

    const express = require('express');
    const app = express();
    

    Dann registrieren Sie eine Route und beginnen Sie mit dem Zuhören:

    app.get('/', (req, res) => {
      res.send('Hello World');
    });app.listen(3000, () => console.log('Server running on port 3000'));
    

    Q38. Was ist Middleware in Express?

    Express-Middleware ist eine Funktion, die req, res und next entgegennimmt. Sie kann beliebigen Code ausführen, die Anfrage oder Antwort ändern, den Zyklus durch eine Antwort beenden oder next() aufrufen, um die Steuerung an die nächste Funktion weiterzugeben. Ein Anfrage-Logger ist ein klassisches Beispiel:

    app.use((req, res, next) => {
      console.log(`${req.method} ${req.url}`);
      next();
    });
    

    Falls eine Middleware weder antwortet noch next() aufruft, hängt die Anfrage fest.

    Q39. Arten von Express-Middleware

    1. Anwendungs-Ebene, angehängt mit app.use() oder app.METHOD().
    2. Router-Ebene, angehängt an eine express.Router()-Instanz.
  • Fehlerbehandlung, die an ihren vier Parametern (err, req, res, next) erkannt wird.
  • Integrierte Funktionen wie express.json() und express.static().
  • Drittanbieter-Module wie cors, morgan und helmet.
  • Q40. Wie funktioniert das Routing?

    Eine Route verbindet eine HTTP-Methode mit einem Pfad und einem Handler. Eine typische CRUD-Ressource sieht so aus:

    app.get('/users', (req, res) => { /* get all users */ });
    app.post('/users', (req, res) => { /* create user */ });
    app.put('/users/:id', (req, res) => { /* update user */ });
    app.delete('/users/:id', (req, res) => { /* delete user */ });
    

    Q41. Was ist express.Router()?

    Ein Router ist eine eigenständige Mini-Anwendung mit eigenen Routen und Middleware, die man an einen Pfad anschließen kann. Er hilft dabei, große Anwendungen nach Funktionen zu organisieren. Definieren Sie die Routen in einem eigenen Modul:

    // userRoutes.js
    const router = require('express').Router();
    router.get('/', (req, res) => res.send('All users'));
    module.exports = router;
    

    und schließen Sie ihn in der Hauptanwendung an, wobei sein / zu /users wird:

    // app.js
    app.use('/users', require('./userRoutes'));
    

    Q42. app.use() im Vergleich zu app.get() und ähnlichen Funktionen

    app.use() passt auf jede HTTP-Methode sowie jeden Pfad, der mit dem angegebenen Präfix beginnt, weshalb damit Middleware und Router eingebunden werden. app.get(), app.post() und ähnliche passen auf eine bestimmte Methode sowie das vollständige Route-Muster.

    Q43. req.params, req.query und req.body

    • req.params enthält benannte Route-Abschnitte: /users/:id liefert req.params.id.
    • req.query enthält Werte der Abfrageseite: /users?age=25 liefert req.query.age, immer als Zeichenkette.
    • req.body enthält die parsierte Anfrage-Payload für Methoden wie POST und PUT und bleibt undefiniert, es sei denn, zuerst läuft eine Body-Parsing-Middleware wie express.json().

    Q44. Parsen von JSON- und Formdaten

    Zwei eingebaute Parser decken die meisten Fälle ab, einer für JSON-Inhalte und einer für URL-kodierte HTML-Formulare:

    app.use(express.json());               // parses application/json
    app.use(express.urlencoded({ extended: true })); // parses form data
    

    Q45. Was ist express.static()?

    Es handelt sich um ein eingebautes Middleware, das Dateien wie Bilder, Stylesheets, Skripte und HTML aus einem Verzeichnis bereitstellt:

    app.use(express.static('public'));
    

    Intermediäres Express

    Q46. Fehlerbehandlung

    Fehler werden durch Middleware mit vier Parametern behandelt, die nach allen Routen registriert werden:

    app.use((err, req, res, next) => {
      console.error(err.stack);
      res.status(500).json({ message: 'Something went wrong' });
    });
    

    Innerhalb von Routen und Middleware werden Fehler mit next(err) an dieses Middleware weitergeleitet:

    app.get('/user/:id', async (req, res, next) => {
      try {
        const user = await getUser(req.params.id);
        res.json(user);
      } catch (err) {
        next(err); // passed to error-handling middleware
      }
    });
    

    Vermeiden Sie in der Produktion das Senden von err.stack oder internen Nachrichten an die Clients.

    Q47. Behandlung von 404-Fehlern

    Express bietet kein eingebautes JSON-Format für 404-Fehler. Fügen Sie nach jeder Route ein Catch-all-Middleware hinzu; wenn eine Anfrage dort ankommt, passt nichts anderes mehr:

    app.use((req, res) => {
      res.status(404).json({ message: 'Route not found' });
    });
    

    Q48. CORS und das cors-Paket

    Cross-Origin Resource Sharing ist der Browser-Mechanismus, der entscheidet, ob eine Seite aus einem bestimmten Ursprung Antworten von einer anderen Seite lesen darf. Der Browser setzt dies durch; der Server aktiviert es über Antwort-Header, die das cors-Paket für Sie festlegt:

    const cors = require('cors');
    app.use(cors()); // allow all origins
    app.use(cors({ origin: 'https://example.com' })); // restrict origin
    

    Der Auszug zeigt zwei Alternativen: Die Ermöglichung für jeden Ursprung oder nur für einen einzigen; verwenden Sie entweder die eine oder die andere Variante, nicht beide, da die permissive Einstellung zuerst angewendet wird.

    Q49. Sessions und Cookies

    Laden Sie das Cookie- und Session-Middleware:

    const cookieParser = require('cookie-parser');
    const session = require('express-session');
    

    und registrieren Sie es anschließend mit einem Signiergeheimnis sowie entsprechenden Optionen:

    app.use(cookieParser());
    app.use(session({
      secret: 'mySecretKey',
      resave: false,
      saveUninitialized: true
    }));
    

    Legen Sie das Geheimnis in einer Umgebungsvariable statt im Quellcode ab und berücksichtigen Sie saveUninitialized: false, um zu vermeiden, dass für anonyme Besucher Sessions erstellt werden. Der standardmäßige In-Memory-Speicher ist nicht für die Produktion geeignet.

    Q50. JWT-Authentifizierung

    Importieren Sie die Bibliothek:

    const jwt = require('jsonwebtoken');
    

    Der folgende Code signiert beim Anmelden ein Token, das die Benutzer-ID enthält, schützt die Routen mit einem Middleware-Modul, das das Token aus dem Authorization-Header extrahiert und überprüft, sowie fügt die decodierte Nutzdatenmenge zu req.user hinzu:

    // Generate token on login
    const token = jwt.sign({ id: user.id }, 'secretKey', { expiresIn: '1h' });// Middleware to protect routes
    function auth(req, res, next) {
      const token = req.headers.authorization?.split(' ')[1];
      if (!token) return res.status(401).json({ message: 'No token' });
      try {
        req.user = jwt.verify(token, 'secretKey');
        next();
      } catch (err) {
        res.status(403).json({ message: 'Invalid token' });
      }
    }app.get('/protected', auth, (req, res) => res.json({ data: 'secret data' }));
    

    Ebenso wie bei Sessions sollte der fest codierte 'secretKey' aus der Konfiguration stammen. Eine gängige Praxis ist es, 401 bei fehlendem Token und 403 bei ungültigem Token zurückzugeben, obwohl viele APIs für beides 401 zurückgeben.

    Q51. Anfragenvalidierung

    Bibliotheken wie express-validator oder joi definieren Regeln und sammeln Fehler. Mit express-validator importieren Sie die Hilfsfunktionen:

    const { body, validationResult } = require('express-validator');
    

    danach schalten Sie die Regel-Middleware vor den Handler ein und überprüfen das Ergebnis:

    app.post('/user',
      body('email').isEmail(),
      body('password').isLength({ min: 6 }),
      (req, res) => {
        const errors = validationResult(req);
        if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
        // proceed
      }
    );
    

    Für einen detaillierteren Vergleich der Validierungsmethoden siehe Zod versus express-validator.

    Q52. Dateiuploads

    multer parsiert multipart/form-data. Konfigurieren Sie ein Ziel:

    const multer = require('multer');
    const upload = multer({ dest: 'uploads/' });
    

    und wenden Sie es pro Route an; upload.single('file') erwartet eine Datei im Feld mit dem Namen file und stellt sie als req.file zur Verfügung:

    app.post('/upload', upload.single('file'), (req, res) => {
      res.json({ file: req.file });
    });
    

    In echten Systemen sollten auch die Dateigröße und -art begrenzt werden, und man sollte niemals dem vom Client bereitgestellten Dateinamen trauen.

    Q53. Verbindung zu MongoDB mit Mongoose

    Erstellen Sie eine Verbindung:

    const mongoose = require('mongoose');
    mongoose.connect('mongodb://localhost:27017/mydb');
    

    dann definieren Sie ein Modell und verwenden Sie es in einer Route:

    const User = mongoose.model('User', new mongoose.Schema({
      name: String,
      email: String
    }));app.post('/users', async (req, res) => {
      const user = await User.create(req.body);
      res.status(201).json(user);
    });
    

    Das Direktübergeben von req.body an create ermöglicht es den Clients, beliebige Felder des Schemas zu setzen; daher sollten die Felder zunächst validiert oder nur die erlaubten ausgewählt werden.

    Q54. Ausführungsreihenfolge der Middleware

    Express führt die Middleware in der Reihenfolge aus, in der sie registriert wurde – von oben nach unten – und die Kette stoppt bei jeder Funktion, die nicht next() aufruft. Aus diesem Grund werden 404-Handler und Fehlerhandler zuletzt ausgeführt.

    Q55. Template-Engines

    Template-Engines rendern HTML anhand von Daten vom Server. Beliebte Optionen sind EJS, Pug (früher Jade) und Handlebars. Die Einrichtung erfordert eine Einstellung für den View-Engine sowie einen Aufruf von res.render:

    app.set('view engine', 'ejs');
    app.get('/', (req, res) => res.render('index', { name: 'John' }));
    

    Fortgeschrittenes Express und Node

    Q56. Asynchrone Fehler in Express 4 im Vergleich zu Express 5

    In Express 4 wird eine abgelehnte Promise aus einem async-Handler nicht vom Framework gefangen; ohne try/catch zusammen mit next(err) oder einem Wrapper wird es zu einer unverarbeiteten Abweisung. Express 5 leitet abgelehnte Promises von Handlern und Middleware automatisch an den Fehlerhandler weiter. Die übliche Workaround-Lösung in Express 4 ist eine kleine höherer-Ordnungsfunktion:

    // Express 4 workaround
    const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
    

    Ein Handler mit dieser Funktion zu umhüllen leitet jede Abweisung an next weiter:

    app.get('/data', asyncHandler(async (req, res) => {
      const data = await fetchData(); // if this throws, asyncHandler catches it
      res.json(data);
    }));
    

    Dieses Muster wird ausführlich in removing try/catch boilerplate with asyncHandler behandelt.

    Q57. Strukturierung einer großen Express-Anwendung

    Ein gängiges MVC-basiertes Layout trennt die Aufgaben in Ordnern voneinander:

    project/
    ├── controllers/     # business logic
    ├── routes/          # route definitions
    ├── models/          # DB schemas
    ├── middlewares/      # custom middleware
    ├── config/          # DB, env configs
    ├── utils/           # helper functions
    ├── app.js
    └── server.js
    

    Routen deklarieren Endpunkte und leiten an Controller weiter, Controller enthalten die Anfragenlogik, während Modelle den Datenzugriff kapseln. Die Trennung von app.js (Konfiguration der Anwendung) und server.js (Starten der Anwendung) erleichtert außerdem die Importierung in Tests.

    Q58. Rate Limiting

    express-rate-limit zählt die Anfragen pro Client innerhalb eines Zeitraums. Importieren Sie es:

    const rateLimit = require('express-rate-limit');
    

    und wenden Sie einen Limitierer an – hier 100 Anfragen pro IP alle 15 Minuten:

    const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100 // limit each IP to 100 requests per window
    });app.use(limiter);
    

    Der Standard-Speicher befindet sich im Arbeitsspeicher, sodass jeder Prozess getrennt gezählt wird; verwenden Sie einen gemeinsamen Speicher wie Redis, wenn Sie skalieren. Hinter einem Proxy konfigurieren Sie trust proxy, damit die tatsächliche Client-IP verwendet wird.

    Q59. Absicherung einer Express-Anwendung

    • Setzen Sie schützende HTTP-Header mit helmet.
  • Überprüfen und bereinigen Sie alle Eingaben, um SQL- oder NoSQL-Injektionen sowie XSS-Angriffe zu verhindern.
  • Dienst über HTTPS anbieten.
  • Eine Rate-Limiting-Strategie anwenden.
  • Geheime Informationen aus der Umgebung laden, beispielsweise mit dotenv, anstatt sie zu speichern.
  • Sicherstellen, dass alle Abhängigkeiten aktualisiert sind, und npm audit ausführen.
  • Für auf Cookies basierende, formbasierte Anwendungen eine CSRF-Schutzmaßnahme hinzufügen.
  • Q60. res.send() versus res.json() versus res.end()

    • res.send() akzeptiert Zeichenketten, Objekte oder Buffers und wählt einen geeigneten Content-Type aus.
    • res.json() serialisiert seinen Parameter als JSON und setzt Content-Type: application/json.
    • res.end() beendet die Antwort ohne weitere Daten – es handelt sich dabei um eine einfache Umhüllung des eigenen Methods von Node.

    Q61. Was macht next('route')?

    Es überspringt die verbleibenden Callbacks der aktuellen Route und wechselt zur nächsten passenden Route. Dies gilt nur für Handler, die über app.METHOD() oder router.METHOD() mit mehreren Callbacks registriert wurden.

    Q62. Leistungssteigerung

    • Führen Sie mit Clustering oder PM2 einen Prozess pro Kern aus.
    • Cachieren Sie Antworten in Redis oder im Arbeitsspeicher.
    • Komprimieren Sie Antworten mit dem compression-Middleware oder lassen Sie einen Proxy dies übernehmen.
    • Setzen Sie Nginx oder einen anderen Reverse-Proxy bzw. Load-Balancer davor.
    • Lassen Sie rechenintensive Aufgaben außerhalb des Event-Loops stattfinden.
    • Poolen Sie Datenbankverbindungen.
    • Bedenken Sie HTTP/2.
    • Verwenden Sie überall asynchrone Eingabe/Ausgabe und vermeiden Sie die Sync-Varianten in den Anfragen.

    Q63. REST gegenüber GraphQL mit Express

    REST stellt viele Endpunkte mit festen Antwortstrukturen bereit, wodurch Clients oft zu viel oder zu wenig Daten abrufen. GraphQL bietet einen einzigen Endpunkt, bei dem der Client genau die Daten angeben kann, die er benötigt. In der Regel wird es mit express-graphql oder apollo-server-express in Express integriert; beide Pakete sind mittlerweile veraltet und werden durch neuere Integrationen ersetzt, daher sollten Sie die aktuelle Dokumentation von Apollo oder GraphQL Yoga konsultieren.

    Q64. Wie handhabt Express gleichzeitige Anfragen?

    Jede Anfrage ist lediglich eine Reihe von Callbacks im Event-Loop. Da Datenbank- und Netzwerkaufrufe nicht blockierend sind, kann ein Thread viele Anfragen abwechselnd bearbeiten: Der Netzwerkverkehr wird den asynchronen Funktionen des Betriebssystems übertragen, die Dateiarbeit dem libuv-Thread-Pool, und die Callbacks werden ausgeführt, sobald die Ergebnisse eintreffen. Im Gegensatz zu klassischen mehrthreadigen Servern gibt es keinen Thread pro Anfrage, weshalb auch ein langsamer synchroner Handler alle beeinflusst.

    Q65. PM2 im Vergleich zum Cluster-Modul

    cluster ist das eingebaute Werkzeug für die Skalierung über mehrere Prozesse. PM2 ist ein Prozessmanager, der auf derselben Idee basiert und automatische Neustarts nach Abstürzen, Lastverteilung, Protokollierung, fehlerfreie Neu laden sowie Überwachung bietet – weshalb er in so vielen Produktivumgebungen verwendet wird.

    Q66. Warum einen Reverse-Proxy vor Node platzieren?

    Ein Reverse-Proxy wie Nginx beendet den TLS-Verkehr, verteilt die Last auf verschiedene Instanzen, speichert in Cache, komprimiert Daten und liefert statische Dateien – dadurch entlastet er Node, sodass der Anwendungsprozess sich auf die Geschäftslogik konzentrieren kann.

    Q67. Konfiguration spezifisch für das Umfeld

    Das dotenv-Paket lädt Variablen aus einer .env-Datei in process.env hoch, und die Anwendung greift bei fehlenden Werten auf Standardwerte zurück:

    require('dotenv').config();
    const PORT = process.env.PORT || 3000;
    

    Neueere Node-Versionen können Env-Dateien außerdem nativ mit dem Flag --env-file laden. Commitzen Sie niemals .env-Dateien, die Geheimnisse enthalten.

    Q68. app.all() versus app.use()

    app.all() passt auf jede HTTP-Methode an einem bestimmten Route-Pfad. app.use() passt ebenfalls auf jede Methode, aber auf den gesamten Pfad und alles darunter – weshalb es das Werkzeug zum Einbinden von Middleware ist.

    Q69. Paginierung mit Express und MongoDB

    Lesen Sie page und limit aus der Abfragesuche mit sinnvollen Standardwerten ab und kombinieren Sie anschließend skip mit limit:

    app.get('/posts', async (req, res) => {
      const page = parseInt(req.query.page) || 1;
      const limit = parseInt(req.query.limit) || 10;
      const posts = await Post.find()
        .skip((page - 1) * limit)
        .limit(limit);
      res.json(posts);
    });
    

    Verschärfen Sie die Implementierung vor der Produktion: Übergeben Sie eine Basiszahl an parseInt, begrenzen Sie page auf mindestens 1 und limit auf einen maximalen Wert, und fügen Sie eine stabile sort-Anweisung hinzu. Bei sehr großen Sammlungen wird skip mit zunehmendem Offset langsamer, während paginieren auf Basis eines indizierten Feldes besser skaliert.

    Q70. Was ist Idempotenz?

    Eine Operation ist idempotent, wenn ihre Wiederholung den Server im selben Zustand lässt wie bei einer einzigen Ausführung. GET, PUT und DELETE sind als idempotent definiert; POST ist in der Regel nicht idempotent, da jede Aufrufung eine weitere Ressource erstellen kann. Wiederholte Anfragen nach Ablauf der Zeitlimits sind nur für idempotente Anfragen sicher, weshalb Zahlungs-APIs oft Idempotenzschlüssel zu POST-Anfragen hinzufügen.

    Hauptpunkte

    • Die meisten Fragen zu Node lassen sich auf ein Modell reduzieren: einen einzigen JavaScript-Thread, einen Event-Loop mit definierten Phasen, Microtasks, die nach jedem Callback abgearbeitet werden, sowie libuv, das den Rest übernimmt.
    • Die meisten Fragen zu Express lassen sich auf ein weiteres Modell reduzieren: eine geordnete Kette von Middleware-Funktionen, bei denen jede Funktion entweder antwortet oder next() aufruft, wobei Fehler an vierargumentige Handler weitergeleitet werden.
  • Die Interviewer schätzen Präzision bei den Details, die im Laufe der Zeit geändert wurden, wie zum Beispiel cluster.isPrimary, Abstürze bei unverarbeiteten Ablehnungen, asynchrone Fehler in Express 5 sowie ESM-Pfadhilfen.
  • Verbinden Sie jede Antwort mit einer Hinweisnote für die Produktivumgebung (Geheimnisse in der Konfiguration, Fehlerverbreitung, gemeinsame Rate-Limit-Speicher), denn das ist in der Regel die nächste Frage.