Node.js Backend Developer Top 100 Interview Questions
100 Node.js backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers the event loop, async patterns, streams, HTTP servers, databases, caching, testing, security, performance and system design, from the basics to production scenarios.
Questions 1-20 of 100
Q1.What does a Node.js backend developer own beyond writing routes?
A Node.js backend developer owns the service end to end: how requests are handled, how data is stored, and how the process behaves under load, failure and change. Routes are just the entry point.
- Asynchronous design: structuring code so the event loop never blocks, and handling errors in every promise and callback.
- Data and integrations: queries, transactions, caching and calls to other services with timeouts and retries.
- Performance: knowing when work belongs in the event loop, the thread pool, a worker thread or a separate service.
- Security: validating input, protecting secrets, managing a large dependency tree and avoiding JavaScript-specific risks such as prototype pollution.
- Operations: structured logs, metrics such as event loop delay and heap use, graceful shutdown and safe deployments.
- Code health: clear module boundaries, tests, static type checking and dependency upgrades.
A strong Node.js engineer understands the runtime model deeply, because most production problems, from latency spikes to memory leaks, come from misunderstanding how the event loop and asynchronous code really work.
Q2.What is Node.js, and why is it well suited to I/O-heavy services?
Node.js is a runtime that executes JavaScript outside the browser, built on a high-performance JavaScript engine and an event loop with non-blocking I/O. Application code runs on a single main thread, while network and file operations complete in the background.
- Non-blocking I/O: starting a database query or HTTP call does not wait; the code continues, and a callback or promise resumes when the result arrives.
- Cheap concurrency: one process can hold thousands of open connections, because waiting connections cost memory, not threads.
- One language across the stack: teams share code, types and validation logic between browser and server.
- Large package ecosystem: libraries exist for most protocols and services.
Where it is a weaker fit:
- CPU-heavy work such as image processing or large computations blocks the main thread and every request with it, unless moved to worker threads or other services.
- Numerical precision: JavaScript numbers are floating point, so money and large integers need care.
Node.js shines for APIs, real-time services, proxies and integration layers, where most time is spent waiting on other systems.
Q3.How does the Node.js event loop work, and what are its phases?
The event loop repeatedly checks for completed work and runs the matching callbacks, in a fixed order of phases. Between every callback, Node.js also drains its
process.nextTickqueue and the promise microtask queue.flowchart TD A["Timers: expired setTimeout and setInterval callbacks"] --> B["Pending callbacks: deferred system errors"] B --> C["Poll: wait for and run I/O callbacks"] C --> D["Check: setImmediate callbacks"] D --> E["Close callbacks: closed sockets and handles"] E --> F{"Any work left?"} F -->|yes| A F -->|no| G["Process exits"]- Timers: runs callbacks whose delay has passed; a delay is a minimum, not a guarantee.
- Poll: retrieves completed I/O events and runs their callbacks, and waits here when nothing else is ready.
- Check: runs
setImmediatecallbacks right after the poll phase. - Close: handles close events such as a socket ending.
What it means in practice:
- Only one callback runs at a time, so a long synchronous task delays everything queued behind it.
- The process stays alive while timers, sockets or servers remain active, which matters for clean shutdown.
Q4.How do `process.nextTick`, promise callbacks, `setImmediate` and `setTimeout` differ in ordering?
They go into different queues, which run at different moments. Knowing the order explains many "why did this run first" bugs.
process.nextTickPromise callbacks setImmediatesetTimeout(fn, 0)Runs as soon as the current operation finishes Runs once the nextTick queue is empty, before the loop moves on Runs in the check phase, right after I/O polling Runs in the next timers phase, after at least 1 ms Runs before any promise callback Also called microtasks Runs before timers when called inside an I/O callback Order against setImmediatevaries when called from the main scriptPractical consequences:
- Recursive
process.nextTickor endless promise chains can starve I/O, because the loop never gets to poll. - Use
setImmediateto break up long work, so I/O callbacks and timers can run between chunks instead of waiting for the whole job. - Prefer promises for ordinary async flow, and keep
process.nextTickfor rare cases such as emitting an event after a constructor returns.
Example
Inside an I/O callback, the order is fully predictable.
const fs = require('node:fs'); fs.readFile(__filename, () => { setTimeout(() => console.log('timeout'), 0); setImmediate(() => console.log('immediate')); Promise.resolve().then(() => console.log('promise')); process.nextTick(() => console.log('nextTick')); console.log('sync'); }); // Output: // sync // nextTick // promise // immediate // timeout- Recursive
Q5.What does it mean to block the event loop, and how do you detect it?
Blocking the event loop means running synchronous code long enough that no other callback can run. Since all requests share one main thread, a 200-millisecond loop in one request adds 200 milliseconds of waiting to every other request in flight.
Common causes:
- Synchronous APIs such as
readFileSync,pbkdf2SyncorexecSyncon the request path. - Parsing or stringifying very large JSON documents.
- CPU-heavy loops: sorting huge arrays, building reports, image work.
- Regular expressions with catastrophic backtracking on hostile input.
flowchart LR A["Request A runs a long synchronous task"] --> B["Event loop cannot run other callbacks"] B --> C["Timers fire late, sockets wait"] C --> D["Latency rises for every request"]How to detect it:
- Measure event loop delay with
perf_hooks.monitorEventLoopDelayand alert when its 99th percentile climbs. - Watch the symptoms: latency rising across all endpoints while one CPU core sits at 100%.
- Profile CPU to see which function holds the main thread.
- Ban synchronous APIs in request code with a lint rule.
- Synchronous APIs such as
Q6.Which Node.js operations use the thread pool, and why can that become a bottleneck?
Some operations cannot be done with non-blocking operating system calls, so Node.js runs them on a small internal thread pool. It has four threads by default, shared by every such operation in the process.
Runs on the thread pool Uses non-blocking operating system I/O Most file system operations Network sockets: TCP, HTTP and TLS connections dns.lookup, used by default for host namesDNS queries made with the resolver functions Asynchronous cryptowork such aspbkdf2,scryptandrandomBytesTimers and child process pipes Asynchronous compression in zlibMost database drivers' network traffic Why it becomes a bottleneck:
- Four slow password hashes occupy all four threads, so file reads and host name lookups queue behind them.
- Slow file systems or network storage can tie up threads for long periods.
How to manage it:
- Raise the pool size with the
UV_THREADPOOL_SIZEenvironment variable, set before the process starts, usually to around the number of cores. - Move heavy hashing or compression to worker threads or a separate service.
- Cache DNS results, or connect by address where appropriate.
Q7.How do callbacks, promises and async/await relate in Node.js?
They are three generations of the same idea: run code when an asynchronous operation finishes. Modern code uses async/await, which is syntax built on promises.
- Error-first callbacks: the original Node.js style, where the callback receives
(error, result). Deep nesting and easy-to-miss error handling earned the name "callback hell". - Promises: objects representing a future result, chainable with
.thenand.catch, with errors flowing to the nearest handler. - async/await: an
asyncfunction always returns a promise, andawaitpauses that function until a promise settles, so asynchronous code reads like synchronous code and works withtry/catch.
Bridging the styles:
util.promisifyturns an error-first callback function into a promise-returning one.- Most core modules also offer promise versions, such as
fs/promisesandtimers/promises.
Common mistakes:
- Forgetting
await, so errors escape as unhandled rejections and work continues out of order. - Awaiting independent calls one after another instead of starting them together.
- Mixing callbacks and promises in one function, which can call back twice.
Example
util.promisifywraps a callback API so both success and failure work withawait.const util = require('node:util'); function lookupUser(id, callback) { setImmediate(() => { if (id <= 0) return callback(new Error('invalid id')); callback(null, { id, name: 'Asha' }); }); } const lookupUserAsync = util.promisify(lookupUser); async function main() { const user = await lookupUserAsync(7); console.log(user.name); // Asha try { await lookupUserAsync(0); } catch (error) { console.log(error.message); // invalid id } } main();- Error-first callbacks: the original Node.js style, where the callback receives
Q8.How do you handle errors correctly in asynchronous Node.js code?
Make sure every asynchronous failure reaches a handler:
awaitpromises insidetry/catch, attach.catchto promises you do not await, check the error argument in callbacks, and listen forerrorevents on emitters and streams.- Unhandled rejections crash the process by default in modern Node.js, which is safer than silently continuing in an unknown state.
- Throwing inside a callback that runs later, such as in a timer, cannot be caught by a
try/catcharound the code that scheduled it. - Streams and emitters report failures through
errorevents; anerrorevent with no listener throws and usually crashes the process. - Fire-and-forget work still needs a
.catchthat logs the failure.
At the service level:
- Use an error-handling middleware that turns thrown errors into consistent responses.
- Use
Errorobjects, never thrown strings, so stack traces exist. - Add context with the
causeoption when wrapping errors. - Log and exit on
uncaughtException, letting the process manager restart a clean process, instead of trying to carry on.
Q9.How do `Promise.all`, `Promise.allSettled`, `Promise.race` and `Promise.any` differ?
All four take several promises and return one, but they differ in when they finish and how they treat failures. Choosing the right one avoids both lost errors and needless waiting.
Promise.allPromise.allSettledPromise.racePromise.anyWaits for every promise to succeed Waits for every promise to finish, either way Settles as soon as the first promise settles Resolves with the first promise that succeeds Rejects on the first failure Never rejects; reports each outcome Copies the first result or failure Rejects only if every promise fails Use when you need all results Use when partial results are useful Use for a deadline against a timer Use for redundant sources, such as replicas Important details:
- None of them cancel the remaining work; a rejected
Promise.allleaves the other operations running. Pass anAbortSignalif they should stop. Promise.anyrejects with anAggregateErrorthat lists every failure.- For many items, combine these with a concurrency limit instead of starting everything at once.
Example
Each combinator handles the same mix of fast, slow and failing calls differently.
const ok = (value, ms) => new Promise((resolve) => setTimeout(() => resolve(value), ms)); const fail = (message, ms) => new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms)); async function main() { console.log((await Promise.all([ok('a', 20), ok('b', 10)])).join(',')); // a,b const settled = await Promise.allSettled([ok('a', 10), fail('down', 5)]); console.log(settled.map((result) => result.status).join(',')); // fulfilled,rejected console.log(await Promise.race([ok('slow', 30), ok('fast', 5)])); // fast console.log(await Promise.any([fail('x', 5), ok('backup', 15)])); // backup try { await Promise.all([ok('a', 20), fail('down', 5)]); } catch (error) { console.log(error.message); // down } } main();- None of them cancel the remaining work; a rejected
Q10.How do closures work in JavaScript, and how can they cause memory leaks?
A closure is a function together with the variables from the scope where it was created. The function can use those variables later, even after the outer function has returned, because the closure keeps them alive.
Why closures are useful in backend code:
- Factories that capture configuration, such as a logger bound to a module name.
- Middleware and handlers that capture shared dependencies.
- Private state, such as a counter that only one function can change.
How they leak memory:
- Long-lived callbacks: an event listener or timer registered for each request captures that request's data, and if it is never removed, the data is never freed.
- Large captured objects: a small closure that captures a huge buffer or result set keeps all of it alive.
- Caches of functions: storing closures in a module-level map without eviction grows memory forever.
How to avoid it:
- Remove listeners and clear timers when their purpose ends.
- Capture only the values you need, not whole request or response objects.
- Bound every cache, and compare heap snapshots when memory keeps growing.
Q11.How does prototypal inheritance work, and how do classes relate to it?
Every JavaScript object has a hidden link to another object, its prototype. When you read a property the object does not have, the engine follows the prototype chain until it finds the property or reaches the end.
- Methods live on prototypes: all instances of a class share one copy of each method through the class's prototype object, rather than each instance holding its own.
classsyntax is mostly a cleaner way to set up constructor functions and prototype chains;extendslinks one prototype to another.supercalls the parent's constructor or methods along that chain.- Own versus inherited properties:
Object.hasOwn(obj, key)checks only the object itself, which matters when processing untrusted input.
Practical points for backend code:
- Prefer composition, passing collaborators in, over deep inheritance hierarchies.
- Do not modify built-in prototypes such as
Object.prototype; every object in the process would change. - Objects created with
Object.create(null)have no prototype, which makes them safe dictionaries for keys supplied by users.
Private fields declared with
#are truly private to the class, unlike properties starting with an underscore by convention.Q12.What is the difference between `==` and `===`, and how does type coercion cause bugs?
===compares value and type without conversion.==converts the operands to a common type first, following rules that produce surprising results, so backend code should use===almost everywhere.=====Object.isConverts operands to a common type, then compares Compares value and type with no conversion Like ===, butNaNequals itself and0differs from-00 == ''is true0 === ''is falseObject.is(NaN, NaN)is trueUse only as the x == nullshorthand, if the team agreesDefault for every comparison Rarely needed outside exact value checks Coercion surprises that cause real bugs:
0 == ''and'0' == falseare both true, so a quantity of 0 can look like a missing value.- Comparing strings with
<or>compares character by character, so'10' < '9'is true when numbers arrive from query strings. typeof nullis'object', a long-standing quirk that breaks naive checks for objects.NaNis not equal to itself; useNumber.isNaN.+concatenates if either side is a string, so'5' + 1is'51'.
Defensive habits:
- Convert input explicitly at the boundary, such as
Number(value)followed by a validity check. - Use
===and!==, and turn on a lint rule that enforces them. - Use
x == nullonly as a deliberate shorthand for "null or undefined", if your team agrees on it. - Validate request data with a schema, so types are known before business logic runs.
Example
Each line shows a coercion rule that has caused production bugs.
console.log(0 == ''); // true console.log('0' == false); // true console.log(null == undefined); // true console.log(null === undefined); // false console.log('10' > '9'); // false console.log('5' + 1); // 51 console.log(typeof null); // object console.log(Number.isNaN(Number('12px'))); // trueQ13.How do CommonJS modules and ES modules differ in Node.js?
Node.js supports two module systems. CommonJS is the original one, using
requireandmodule.exports; ES modules are the JavaScript standard, usingimportandexport.CommonJS ES modules Loads with require, a normal function callLoads with importstatements, resolved before code runsLoads synchronously Loads asynchronously, allowing top-level awaitExports are a plain object assigned at run time Exports are live bindings that static tools can analyze Default for .jsfiles without a module type settingUsed for .mjsfiles, or.jswhen the package declares module typeProvides __dirnameand__filenameUses import.meta.dirnameandimport.meta.urlinsteadInteroperability:
- ES modules can import CommonJS packages.
- Requiring an ES module from CommonJS was historically impossible; recent versions support it for modules without top-level
await.
When to use which: choose ES modules for new projects, because they are the standard and enable better tooling, such as unused code removal. Keep a package consistent, since mixing systems causes confusing loading and default-export issues.
Q14.How does module loading and caching work in Node.js?
The first time a module is loaded, Node.js resolves its path, runs its top-level code once, and caches the result. Every later import of the same resolved file returns the cached exports.
flowchart TD A["import or require"] --> B["Resolve to a file path"] B --> C{"Path already in module cache?"} C -->|Yes| D["Return cached exports"] C -->|No| E["Run top-level code once"] E --> F["Store exports under the resolved path"] F --> DRelative paths point to files, while bare names such as
date-utilsare looked up in the nearestnode_modulesfolders, honoring the package's declared entry points.Consequences for backend code:
- Modules act as singletons: a connection pool created at a module's top level is shared by every importer in that process.
- Cache keys are paths: two copies of the same package in different folders are two separate modules, so
instanceofchecks and shared state can break. - Top-level side effects run at import time, so slow work there delays startup and makes tests harder.
- Circular imports can return partially initialized exports.
Good practice: keep modules free of heavy side effects, create shared resources in an explicit startup function, and pass them in where they are needed.
Q15.How do you manage dependencies and lock files in a Node.js project?
Declare direct dependencies in
package.json, commit the lock file, and install exactly what the lock file records in every build and deployment. That makes builds reproducible even though version ranges allow newer releases.- Version ranges: a caret, such as
^2.4.0, accepts newer minor and patch versions; a tilde accepts only patch versions. Ranges are resolved when the lock file is created. - Lock file: records the exact version, source and integrity hash of every package in the tree, direct and indirect.
- Clean installs in builds: use the package manager's clean-install mode, which installs exactly the lock file and fails if it does not match
package.json. - Dev versus production: keep test and build tools in development dependencies, and omit them from production images.
- Updates: update deliberately on a schedule, with automated pull requests and tests, rather than letting each fresh install pick new versions.
Watch out for:
- Packages that run install scripts, which execute code on your build machines.
- Duplicate versions of the same package bloating the bundle and breaking shared state.
- Unused dependencies, which still add attack surface.
- Version ranges: a caret, such as
Q16.What are streams in Node.js, and what are the four stream types?
Streams process data piece by piece as it arrives, instead of loading it all into memory first. They power files, HTTP requests and responses, sockets and compression in Node.js.
Readable Writable Duplex Transform A source you read data from A destination you write data to Both readable and writable, independently A duplex stream whose output is computed from its input An incoming HTTP request or a file being read An HTTP response or a file being written A network socket Compression, encryption or parsing Why streams matter:
- Constant memory: a 5 GB file can be copied using only a few kilobytes of buffer.
- Lower latency: the first bytes reach the client before the whole result exists.
- Backpressure: a slow consumer automatically slows the producer, instead of letting data pile up in memory.
Modern usage:
- Connect streams with
stream.pipeline, which handles errors and cleanup on every stage. - Readable streams are async iterable, so
for await (const chunk of stream)works. - Object mode lets streams carry JavaScript objects, such as parsed rows, instead of bytes.
Q17.What is backpressure in Node.js streams, and how does `pipeline` handle it?
Backpressure is the signal a slow consumer sends to a fast producer: "stop, I am full". Without it, a fast source such as a file read would push data into memory faster than a slow destination such as a network client can take it.
- How it works:
writable.write()returnsfalsewhen the writable's internal buffer passes its high-water mark. The producer should pause until thedrainevent fires. pipelineandpipehandle this automatically, pausing and resuming the source.- Manual code that ignores the return value of
writeand keeps writing is a classic cause of memory blowups.
Why
pipelineoverpipe:- It forwards errors from any stage and destroys every stream in the chain, so failures do not leak file handles or sockets.
- The promise version in
stream/promisesworks naturally withawait. - Async generator functions can be used as transform stages, which keeps parsing code simple.
flowchart LR A["Fast readable source"] --> B["Transform stage"] B --> C["Slow writable destination"] C -->|buffer full: pause| A C -->|drained: resume| AExample
An async generator parses CSV lines inside a pipeline, and the last stage sums the amounts.
const { Readable } = require('node:stream'); const { pipeline } = require('node:stream/promises'); async function* parseAmounts(source) { for await (const line of source) { const [id, amount] = line.trim().split(','); if (id !== 'id') yield Number(amount); } } async function main() { let total = 0; await pipeline( Readable.from(['id,amount\n', '1,250\n', '2,100\n', '3,75\n']), parseAmounts, async function (source) { for await (const amount of source) total += amount; }, ); console.log(total); // 425 } main();- How it works:
Q18.How do Buffers work in Node.js, and how do they differ from strings?
A
Bufferis a fixed-size chunk of raw bytes, stored outside the JavaScript heap. Strings are sequences of characters; converting between the two always needs an encoding such as UTF-8.BufferString Fixed-size raw bytes, stored outside the JavaScript heap Immutable sequence of characters, stored on the heap lengthcounts bytes:Buffer.byteLength('₹')is 3lengthcounts UTF-16 code units:'₹'.lengthis 1Best for files, sockets, hashes and images Best for text, after decoding the bytes with an encoding - Encodings:
buffer.toString('utf8'),'base64'or'hex'decode bytes;Buffer.from(text, encoding)encodes them. - Splitting multi-byte characters: decoding chunk by chunk can cut a character in half; set an encoding on the stream, or use a string decoder, which handles boundaries.
Safety points:
Buffer.alloc(size)returns zero-filled memory;Buffer.allocUnsafeis faster but may contain old data, so fill it completely before exposing it.- Large Buffers count toward process memory but not the JavaScript heap limit, so watch the external memory figure.
- Compare secrets with
crypto.timingSafeEqualon equal-length Buffers, not===on strings.
- Encodings:
Q19.How do EventEmitters work, and what are the common pitfalls?
An
EventEmitterlets objects publish named events that any number of listeners subscribe to. Servers, streams and sockets in Node.js are all emitters.onandonceregister listeners;offremoves one.emitis synchronous: all listeners run immediately, in registration order, beforeemitreturns. Slow listeners block the code that emitted the event.- The
errorevent is special: emitting it with no listener throws the error, which usually crashes the process.
Common pitfalls:
- Listener leaks: adding a listener per request to a long-lived emitter, without removing it, grows memory. Node.js warns when more than ten listeners are added for one event.
- Errors in listeners: an exception thrown in a listener propagates out of
emit, into the code that emitted. - Async listeners: an
asynclistener's rejected promise is not caught byemit; handle errors inside it. - Assuming delivery order across emitters, which is not guaranteed.
For waiting on a single event with
await,events.once(emitter, name)returns a promise. Eachfor awaitoverevents.onturns an event stream into an async iterator.Example
Listeners run synchronously during
emit,oncefires only the first time, and an unhandlederrorevent throws.const { EventEmitter } = require('node:events'); const orders = new EventEmitter(); orders.on('created', (order) => console.log(`email for ${order.id}`)); orders.once('created', () => console.log('first order bonus')); console.log('before emit'); orders.emit('created', { id: 1 }); orders.emit('created', { id: 2 }); console.log('after emit'); try { orders.emit('error', new Error('payment provider down')); } catch (error) { console.log(`unhandled error event: ${error.message}`); } // Output: // before emit // email for 1 // first order bonus // email for 2 // after emit // unhandled error event: payment provider downQ20.How does JSON handling affect a Node.js service's performance and safety?
JSON.parseandJSON.stringifyare synchronous and run on the main thread, so very large payloads block the event loop. JSON also has gaps that cause subtle bugs with numbers, dates and special keys.Performance:
- Parsing or stringifying a multi-megabyte document can take tens of milliseconds, during which no other request progresses.
- Set a request body size limit, paginate large responses and stream big exports instead of building one giant string.
- For truly large documents, use a streaming parser or move the work to a worker thread.
Correctness and safety:
- Large integers: numbers above 2 to the power of 53 lose precision, so send big identifiers as strings.
- Dates become strings and do not come back as dates; parse them explicitly.
- Unsupported values:
BigIntmakesJSON.stringifythrow, andundefinedproperties are dropped silently. - Special keys: a parsed object can contain a
__proto__key, which is harmless on its own but dangerous when naively merged into other objects. - Circular references throw; log objects with a safe serializer.
A
toJSONmethod on a class controls exactly what is serialized, which also prevents accidentally leaking internal fields.