Node.js Interview Questions
Commonly asked Node.js interview questions with clear, practical answers.
Common Node.js interview questions, from the event loop through practical Express and API-building concerns.
Q: How can Node.js handle many concurrent connections on a single thread? Node runs your JavaScript on one main thread, but hands off slow I/O operations (file access, network requests, database queries) to libuv, which uses the operating system's asynchronous I/O facilities and a background thread pool for certain operations under the hood. The main thread stays free to keep processing other work while I/O is in flight, and only runs your callback (or resolves your Promise) once the operation actually completes — so Node avoids dedicating a full OS thread to each connection, which is expensive at scale.
Q: What's the difference between blocking and non-blocking I/O in Node.js?
Blocking I/O (like fs.readFileSync) halts the entire single JavaScript thread until the operation finishes, so nothing else — including handling other incoming requests — can happen in the meantime. Non-blocking I/O (fs.readFile, or any Promise-based/async equivalent) starts the operation, immediately returns control to continue running other code, and only invokes a callback or resolves a Promise once the result is ready. Production Node servers should almost always use the non-blocking versions of I/O APIs.
Q: What's the difference between CommonJS and ES modules in Node.js?
CommonJS (require()/module.exports) is Node's original, synchronous module system. ES modules (import/export) are the standardized JavaScript module system also used in browsers, and support features CommonJS doesn't, like top-level await. Whether a .js file is treated as one or the other depends on the "type" field in the nearest package.json (or explicit .cjs/.mjs extensions) — the two systems can't be freely mixed within the same file.
Q: Why does the order of middleware registration matter in Express?
Express processes middleware and routes in exactly the order they were registered with app.use() and the routing methods, passing control forward only when a middleware calls next(). A middleware registered after the route it's meant to protect (like an auth check) or after the routes it's meant to observe (like a logger) simply never runs for those requests, since Express has already moved past that point in the chain by the time the request arrives.
Q: Why do streams matter for handling large files or large HTTP payloads? Reading an entire large file (or request body) into memory at once requires enough memory to hold the whole thing, which doesn't scale well for very large files or high-concurrency workloads. Streams process data in small chunks as they arrive, keeping memory usage small and roughly constant regardless of the total size — this is also how Node handles incoming HTTP request bodies internally, which is why frameworks need body-parsing middleware to assemble the chunks before your route handler can use them.
Q: What is the difference between a microtask and a macrotask in Node's event loop, and why does it matter?
Microtasks (Promise callbacks, queueMicrotask) are drained completely after every synchronous operation finishes, before the event loop moves on to the next macrotask (like a setTimeout callback or an I/O completion). This means a resolved Promise's .then() always runs before a setTimeout(fn, 0) callback scheduled around the same time, even though both look like they're scheduled "immediately" — a common source of confusing ordering bugs if you don't know the distinction exists.
Q: What is backpressure, and why does it matter when piping one stream into another?
Backpressure happens when a writable destination can't consume data as fast as a readable source produces it — without handling it, unconsumed chunks pile up in memory rather than being flushed as fast as they arrive. writable.write() returns false once its internal buffer fills up, and both .pipe() and the more modern stream.pipeline() use that signal to automatically pause the source until a 'drain' event says it's safe to resume, keeping memory usage roughly constant regardless of any speed mismatch between the two ends.
Q: Why should an uncaughtException handler exit the process instead of logging the error and continuing?
Once an error has escaped every try/catch in the code, the process's internal state is no longer verified — some in-progress operation could be left half-done. Continuing to serve requests from that unknown state risks silently corrupting data in ways that are far harder to diagnose later than a clean restart, which is why the standard pattern logs the error and exits, relying on a process manager to bring up a fresh, known-good instance right away.
Q: How does the cluster module let a Node.js app use more than one CPU core, and what's the catch?
Node runs JavaScript on a single thread per process, so one process is limited to one CPU core no matter how well it handles concurrent I/O. cluster forks multiple independent OS processes — typically one per core — with a primary process load-balancing connections across them. The catch is that each forked worker has completely separate memory, so any state meant to be shared (an in-memory cache, a rate limiter) needs an external store like Redis instead of a plain in-process object, which would otherwise be silently duplicated per worker.
Q: What's the practical difference between a unit test and an integration test for an Express route? A unit test exercises one function in isolation — pure logic, no HTTP layer, no dependencies — and catches bugs in that function's own logic. An integration test (typically via Supertest) drives a real HTTP request through the actual Express app, exercising routing, middleware, and a route handler's interaction with its (often mocked) dependencies together, catching wiring mistakes — a wrong status code, a middleware registered in the wrong order — that a pure unit test on the underlying logic alone wouldn't reveal.