Een gerichte set Node.js-interviewvragen voor backend-ontwikkelaars. De antwoorden zijn gestructureerd om hardop te oefenen: leg de architectuur uit, geef een voorbeeld en benoem afwegingen voor productie.
Node.js is een uitvoeringsomgeving voor JavaScript buiten de browser, gebouwd op V8 en uitgebreid met API's voor netwerkcommunicatie, bestanden, processen, gegevensstromen en toegang tot het besturingssysteem. Het wordt veelal gebruikt voor REST (Representational State Transfer) API's, GraphQL-services, realtime WebSocket-servers, microservices, CLI (Command-Line Interface)-tools, proxy's en API-gateways. De belangrijkste kracht ervan is niet-blokkerende I/O (Input/Output): één proces kan efficiënt veel netwerkverbindingen afhandelen. De keerzijde is dat langdurige, CPU (Central Processing Unit)-intensieve berekeningen de eventloop kunnen blokkeren, tenzij deze worden verplaatst naar workers, afzonderlijke processen of een andere service.
JavaScript draait in V8, terwijl de API's van Node.js asynchroon werk delegeren aan het besturingssysteem of aan de threadpool van libuv. De hoofdthread van JavaScript wacht niet tot een I/O-bewerking is voltooid; deze gaat door met het uitvoeren van andere code. Wanneer de bewerking is voltooid, wordt de bijbehorende callbackfunctie of `Promise`-voortzetting klaargezet voor de eventloop. Op deze manier bereikt Node.js een hoge mate van gelijktijdigheid, hoewel de JavaScript-code van de gebruiker doorgaans op één hoofdthread wordt uitgevoerd.
V8 compileert en voert JavaScript uit en beheert JavaScript-objecten en het geheugen. libuv biedt de platformonafhankelijke eventloop, asynchrone I/O-integratie en een worker-threadpool voor bewerkingen zoals bepaald bestandssysteemwerk, DNS en cryptografie. De eventloop bepaalt wanneer callbackfuncties van voltooide bewerkingen moeten worden uitgevoerd. Node.js zelf verbindt JavaScript-API's met systeemeigen functionaliteiten; V8 op zichzelf biedt geen HTTP-server, toegang tot het bestandssysteem of proces-API's.
4Waarom wordt Node.js vaak single-threaded genoemd?
Een enkel Node.js-proces voert JavaScript-code van de gebruiker doorgaans uit op één hoofdthread, wat betekent dat twee normale afhandelaren van requests niet tegelijkertijd JavaScript uitvoeren in die thread. Het platform bestaat echter niet letterlijk uit één thread: libuv heeft een threadpool, het besturingssysteem voert asynchrone I/O uit, V8 voert garbagecollection-taken uit en Node.js ondersteunt ook `worker_threads` en sub-processen (child processes). De term beschrijft voornamelijk het standaard uitvoeringsmodel voor JavaScript.
5Hoe kan Node.js veel gelijktijdige aanvragen afhandelen?
Node.js start I/O-operaties en keert daarna terug naar de eventloop in plaats van een thread per aanvraag te blokkeren. Er kunnen bijvoorbeeld 1.000 database-aanroepen in behandeling zijn, terwijl de hoofdthread doorgaat met het accepteren van verbindingen en het verwerken van voltooide callbacks. Dit werkt goed voor I/O-intensieve taken, zoals HTTP-aanroepen, database-queries, bestanden en sockets. Het werkt slecht als handlers voor aanvragen zwaar synchroon CPU-werk verrichten, omdat dit de eventloop blokkeert en alle andere aanvragen vertraagt.
6Wat zijn de belangrijkste fasen van de Node.js-eventloop?
Node.js gebruikt libuv-fasen zoals pending callbacks, idle/prepare, poll, check en close callbacks, samen met timerverwerking. poll verwerkt veel I/O-callbacks (invoer/uitvoer), check voert setImmediate uit en timers voeren setTimeout/setInterval uit zodra de minimale vertraging en planning dat toelaten. In libuv 1.45 en Node.js 20+ worden timers elke iteratie na poll verwerkt, met een eerste compatibiliteitsronde vóór de loop. process.nextTick gebruikt een aparte Node-wachtrij; Promise-reacties en queueMicrotask gebruiken V8's microtask-wachtrij. Geen van beide is een libuv-fase. Deze wachtrijen draaien op controlepunten tussen callbacks. Recursief aanvullen kan ander werk uithongeren.
7Hoe verschillen synchrone, asynchrone en niet-blokkerende bewerkingen?
Synchroon beschrijft de besturingsstroom: een aanroep geeft zijn resultaat of werpt een uitzondering voordat de aanroeper verdergaat. Asynchroon beschrijft een resultaat dat later via een callback, Promise of andere melding komt. Blokkeren betekent wachten op werk of een resource terwijl de huidige thread niets anders kan doen. Niet-blokkerende interfaces vermijden dit wachten, bijvoorbeeld door ‘zou blokkeren’ terug te geven of een melding te regelen. Dit zijn verschillende dimensies: synchroon betekent op zichzelf niet wachten op I/O-gereedheid (invoer/uitvoer), en een Promise teruggeven maakt zware synchrone berekeningen niet niet-blokkerend. fs.readFileSync is een blokkerende synchrone interface; fs.promises.readFile levert asynchroon resultaat terwijl de hoofdthread ander werk kan verwerken. Geef in verzoekafhandeling de voorkeur aan geschikte asynchrone I/O-interfaces.
Dit ontstaat wanneer synchroon JavaScript of voortdurend aangevulde controlepuntwachtrijen tijdige verwerking van I/O-callbacks (invoer/uitvoer), timers en verzoeken verhinderen. Oorzaken zijn zware berekeningen, grote JSON-bewerkingen, recursief geplande microtasks en overmatig process.nextTick-gebruik. Beperk invoergroottes, profileer duur werk en verplaats rekenintensieve taken naar worker_threads of een andere dienst. Geef bij het opdelen van werk de controle aan een latere eventloopronde, bijvoorbeeld met setImmediate; herhaald wachten op reeds opgeloste Promises of queueMicrotask plannen kan I/O nog steeds uithongeren. Vermijd blokkerende synchrone interfaces in veelgebruikte verzoekpaden.
Een callback is een functie die aan een andere bewerking wordt gegeven om door die bewerking te worden aangeroepen. Hij kan synchroon draaien, zoals bij Array.map, of asynchroon, zoals bij fs.readFile. Klassieke asynchrone Node.js-callbacks zetten de fout doorgaans als eerste argument: callback(error, result). Callback hell betekent diep geneste afhankelijke callbacks die besturingsstroom en foutafhandeling onduidelijk maken. Benoemde functies, kleinere bewerkingen, Promises en async/await kunnen de leesbaarheid verbeteren. Start onafhankelijke bewerkingen waar passend samen, bijvoorbeeld met Promise.all, met inachtneming van gelijktijdigheidslimieten en foutafhandeling.
import fs from 'node:fs';
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
Een Promise vertegenwoordigt het toekomstige resultaat van een asynchrone bewerking en kan de status pending, fulfilled of rejected hebben. Een async-functie retourneert altijd een Promise. Het sleutelwoord await pauzeert alleen de huidige async-functie totdat de Promise is afgehandeld; het blokkeert niet het volledige Node.js-proces. Een goed antwoord noemt foutafhandeling met try/catch, het vermijden van floating Promises en het gebruik van Promise.all voor onafhankelijke taken, zodat bewerkingen niet onnodig na elkaar worden uitgevoerd.
11Hoe verschillen process.nextTick, queueMicrotask, setImmediate en setTimeout?
process.nextTick plant werk in Nodes next-tick-wachtrij; queueMicrotask gebruikt V8's microtask-wachtrij, net als Promise-reacties. Op een normaal controlepunt op het hoogste CommonJS-niveau draaien nextTick-callbacks vóór ingeplande Promise/queueMicrotask-callbacks. Op het hoogste ES-moduleniveau vindt evaluatie al in een microtask-context plaats, zodat ingeplande microtasks vóór nextTick kunnen draaien. Dit hangt af van de context en is geen universele prioriteitsregel die lopend JavaScript onderbreekt. setImmediate draait in check. setTimeout(fn, 0) wacht op een minimale timerdrempel en is niet onmiddellijk. Hun volgorde hangt af van waar ze zijn gepland; worden beide binnen een I/O-callback (invoer/uitvoer) gepland, dan draait setImmediate eerst. Gebruik bij voorkeur queueMicrotask voor overdraagbaar uitstel als microtask, tenzij nextTick-specifiek gedrag nodig is.
// 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.
12Hoe moeten fouten in Node.js worden afgehandeld?
Gebruik try/catch voor synchrone uitzonderingen. Gebruik bij Promise-bewerkingen await binnen try/catch of voeg een afwijzingshandler toe; een buitenste synchrone try/catch vangt geen uitzondering van een latere callback. Callbacks met een fout als eerste argument moeten dat argument controleren, en EventEmitter-errorgebeurtenissen vereisen passende handlers. Servers moeten verzoekfouten centraal afhandelen, nuttige context loggen en interne details niet aan clients tonen. Neem na uncaughtException niet aan dat het proces veilig kan doorgaan: voer alleen noodzakelijke synchrone noodopruiming uit, beëindig het proces en laat een supervisor het herstarten. Geplande nette afsluiting is nuttig bij normale beëindiging, maar willekeurig asynchroon herstel na een niet-opgevangen uitzondering is geen betrouwbare strategie.
Een module is een geïsoleerde eenheid code met een expliciete geëxporteerde interface. Node.js ondersteunt ingebouwde modules zoals `node:fs` en `node:http`, lokale projectmodules, npm-pakketten, CommonJS-modules en ECMAScript-modules. Modules maken verantwoordelijkheidsgrenzen duidelijker en maken hergebruik mogelijk. De afweging in productieomgevingen is koppeling: als modules elkaar te diep importeren of neveneffecten verbergen tijdens het importeren, worden het testen en het opstartgedrag moeilijker te doorgronden.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
14Hoe verschillen CommonJS require en import van ES-modules?
CommonJS gebruikt require en module.exports; ES-modules gebruiken import en export. require laadt normaal synchroon en kan voorwaardelijk worden aangeroepen. Statische import-declaraties maken de afhankelijkheidsgraaf analyseerbaar; dynamische import() retourneert een Promise. Moderne Node.js-versies kunnen via require ook een ES-modulengraaf zonder await op het hoogste niveau laden volgens de ondersteunde interoperabiliteitsregels. ES-modules betekenen dus niet altijd asynchrone evaluatie. .cjs/.mjs, type in package.json en toepasselijke syntaxdetectieregels bepalen de modulemodus. ES-modules ondersteunen await op het hoogste niveau. CommonJS biedt __dirname/__filename; huidig Node.js biedt import.meta.dirname/filename voor bestandsgebaseerde ES-modules. import.meta.url is bruikbaar voor URL-gebaseerde resolutie en oplossingen die oudere versies ondersteunen.
// 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.
15Wat zijn npm, package.json en package-lock.json?
npm is de opdrachtregelinterface voor pakketbeheer. package.json beschrijft metadata, scripts en eisen aan directe afhankelijkheden, vaak met versiebereiken. package-lock.json legt een opgeloste afhankelijkheidsboom vast met pakketversies en integriteitsgegevens. Neem het lockbestand van een applicatie op in versiebeheer. npm ci is nuttig voor continue integratie: het installeert vanuit het lockbestand, verwijdert een bestaande node_modules-map en faalt als package.json en het lockbestand verschillen. Een lockbestand beperkt verschuivingen in afhankelijkheden, maar maakt besturingssystemen, native builds, levenscyclusscripts, Node-versies of installatieopties niet identiek. Reproduceerbare builds vereisen ook een beheerste toolchain en omgeving.
16Hoe moeten configuratie en omgevingsvariabelen worden beheerd?
Lees omgevingsvariabelen via process.env: aanwezige waarden zijn strings, een ontbrekende eigenschap is undefined. Valideer verplichte waarden en converteer ze bewust bij het opstarten; PORT alleen met Number omzetten verwerpt geen lege string, NaN of poort buiten het toegestane bereik. Stop met een duidelijke fout bij ongeldige verplichte configuratie. Houd echte geheimen buiten versiebeheer en lever productiegeheimen via het uitrolplatform of een geheimenbeheerder; voorbeeldconfiguratie zonder geheimen mag worden vastgelegd. NODE_ENV is een applicatieconventie, geen vertrouwde beveiligingsgrens.
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 implementeert een publish-subscribe-patroon binnen een Node.js-proces. on registreert een listener, once registreert een listener voor één event, off verwijdert een listener en emit roept listeners synchroon aan in de volgorde van registratie. Het synchrone karakter is belangrijk: een trage listener blokkeert de emitter. Het error-event is speciaal omdat een onafgehandeld error-event het proces kan laten crashen. Langlopende systemen moeten ook ongebruikte listeners verwijderen om geheugenlekken te voorkomen.
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 vertegenwoordigt een reeks bytes met een vaste grootte. Het wordt gebruikt voor bestanden, TCP-sockets, HTTP-body's, afbeeldingen, cryptografie en binaire protocollen. Buffer.from creëert een buffer op basis van bestaande gegevens, Buffer.alloc reserveert geheugen dat volledig met nullen is gevuld, en Buffer.allocUnsafe is sneller maar kan oud geheugen bevatten totdat het wordt overschreven. Dat maakt allocUnsafe alleen geschikt wanneer de code de buffer onmiddellijk vult en nooit verouderde gegevens blootstelt.
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 verwerken gegevens in kleine delen (chunks) in plaats van alles in het geheugen te laden. De belangrijkste typen zijn `Readable`, `Writable`, `Duplex` en `Transform`. Bestanden, objecten voor HTTP-aanvragen en -antwoorden, TCP-sockets en gzip-streams zijn veelvoorkomende voorbeelden. Streams zijn waardevol voor grote bestanden en netwerkverkeer omdat ze het geheugengebruik en de vertraging verminderen. Een goed antwoord voor een productieomgeving moet benoemen dat fouten in streams correct door de hele keten heen afgehandeld moeten worden.
20Wat is backpressure en waarom is `pipeline` nuttig?
Backpressure treedt op wanneer een gegevensbron sneller brokken (chunks) produceert dan de bestemming ze kan verwerken. Zonder backpressure kan het geheugengebruik toenemen totdat het proces trager wordt of crasht. Streams en `pipe` kunnen de datastroom coördineren, maar `pipeline` uit `node:stream/promises` is veiliger voor ketens met meerdere stappen, omdat het fouten doorgeeft, gerelateerde streams sluit en een Promise retourneert. Het is een goede standaardkeuze voor bestandscompressie, uploads, downloads en streamtransformaties.
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')
);