Node.js Interview Questions 2026
If you're interviewing for a backend or full stack role, Node.js questions are almost guaranteed to come up, even at companies that also use Python or Java elsewhere in the stack. It's the default choice for a lot of APIs, and interviewers use it as a way to check whether you actually understand asynchronous programming, not just whether you can write app.get() a few times.
This page covers the questions that come up most in real interviews, from "what is the event loop" (which everyone asks but not everyone can explain well) to the messier stuff around streams, backpressure, error handling across async code, and how you'd actually scale a Node app past a single core.
Why Interviewers Lean on Node.js Questions
Node has a few properties that make it a good interview topic: it's single-threaded but handles massive concurrency, which forces you to actually understand what's happening under the hood instead of just trusting the runtime. Interviewers use Node questions to check:
- Whether you understand non-blocking I/O, or just know the buzzwords
- Whether you can reason about what blocks the event loop and what doesn't
- Whether you've actually built something with Express beyond a tutorial (middleware order, error handling, structuring routes)
- Whether you know how to keep a Node app alive and fast once it has to handle a real amount of traffic
Who This Page Is For
- Backend and full stack developers interviewing for Node/Express or NestJS roles
- MERN stack developers who've used Node but never had to explain the internals
- Anyone prepping for a role where the JD lists "Node.js" as a core requirement, not a nice-to-have
Related Resources
Node.js Interview Questions and Answers (2026)
Node.js Basics and Runtime
1. What is Node.js, and why does it let JavaScript run outside a browser at all?
Node.js is a JavaScript runtime built on Chrome's V8 engine, but running outside the browser as a standalone process on your machine or a server. Browsers only give JavaScript access to browser-specific APIs (DOM, window, fetch), Node instead gives it access to operating system level stuff, the file system, network sockets, processes, which is what lets you write servers, CLIs, and scripts in JavaScript instead of just browser code.
2. Is Node.js single-threaded? What does that actually mean in practice?
Your actual JavaScript code runs on a single thread, yes, so two pieces of your JS code never run at the exact same instant. But Node itself isn't single-threaded under the hood, I/O operations like reading a file or querying a database get handed off to the operating system or a background thread pool, and your single JS thread just gets notified with a callback once they're done. That's the whole trick, one thread runs your code, but a lot of the actual waiting happens elsewhere.
3. What is libuv, and what job does it actually do?
libuv is the C library underneath Node that implements the event loop and handles the async, non-blocking I/O, things like file system access, DNS lookups, and some networking operations that don't have a native async OS API. It maintains a thread pool (4 threads by default) for operations that would otherwise block, so your main JS thread never has to wait on them directly.
4. What's the actual difference between Node.js and a browser's JavaScript engine like V8?
V8 is just the engine that compiles and executes JavaScript, both Node and Chrome use it. The difference is everything built around it: browsers wrap V8 with DOM APIs, window, fetch, and a security sandbox meant for running untrusted code from random websites. Node wraps the same V8 engine with OS-level APIs instead, fs, http, process, and none of the browser sandboxing, since it's meant to run trusted code you wrote yourself.
5. Are __dirname and __filename available in ES modules the same way they are in CommonJS?
No, and this trips people up when they switch a project to "type": "module". __dirname and __filename are CommonJS-only globals. In ES modules you get import.meta.url instead, and you'd typically derive the equivalent path using fileURLToPath(import.meta.url) from the url module if you actually need a directory path.
6. What's the difference between npm and npx, and when do you actually reach for npx?
npm installs and manages packages. npx executes a package's binary, either one you already have installed locally, or a one-off download it runs without permanently installing it. You reach for npx when you want to run a CLI tool once without adding it as a project dependency, like scaffolding a new project with npx create-react-app or running a code generator you won't need again.
The Event Loop and Asynchronous Model
1. Walk through what actually happens during one pass of the event loop.
The event loop cycles through a series of phases: timers (running callbacks scheduled by setTimeout/setInterval whose time has elapsed), pending callbacks (some system-level callbacks deferred from the previous loop), poll (retrieving new I/O events and running their callbacks), check (running setImmediate callbacks), and close callbacks (like a socket's close event). Between almost every phase, Node drains the microtask queue, Promises and process.nextTick, before moving on, which is why microtasks tend to run sooner than you'd expect relative to timers.
2. What's the difference between process.nextTick(), setImmediate(), and setTimeout(fn, 0)?
process.nextTick() runs before the event loop continues at all, it jumps the queue ahead of even Promise microtasks. setImmediate() runs in the "check" phase, after the current poll phase completes. setTimeout(fn, 0) schedules the callback for the timers phase, and even with a 0ms delay it's not guaranteed to run before setImmediate inside I/O callbacks, the actual order between the two can vary depending on context. If you genuinely need something to run "as soon as possible but after the current operation," process.nextTick is the strongest guarantee, though overusing it can starve the event loop entirely if you keep scheduling more nextTick calls recursively.
3. Why does a long, synchronous CPU-heavy function block everything else, even other users' requests?
Because there's only one thread running your JavaScript. If that thread is busy crunching a big synchronous loop, sorting a huge array, doing heavy computation inline, nothing else can happen until it finishes: no other request gets handled, no timer fires, no I/O callback runs, even for completely unrelated users hitting your server at the same time. This is the single biggest gotcha people miss when they assume Node "handles everything async by default," async only helps with I/O, not with CPU-bound work you wrote synchronously.
4. What's the difference between microtasks and macrotasks, and where do Promises fit?
Macrotasks are things like setTimeout, setInterval, and I/O callbacks, each phase of the event loop processes one batch of macrotasks. Microtasks are Promise callbacks (.then, .catch, .finally) and queueMicrotask, and the microtask queue gets fully drained after every single macrotask completes, before the loop moves on to the next phase. That's why a resolved Promise's .then() callback consistently runs before a setTimeout(fn, 0) callback, even though both look like they should run "immediately."
5. How would you handle a genuinely CPU-intensive task without blocking the event loop?
You offload it, either to a worker_threads worker, which runs on a separate thread and communicates back via message passing, or to a completely separate process (a queue-based background job using something like Bull/BullMQ backed by Redis). Breaking the work into smaller chunks and yielding back to the event loop between chunks (using setImmediate to schedule the next chunk) is another option for moderately heavy work, but for genuinely expensive computation, a worker thread or separate process is the real fix.
6. What was "callback hell," and how did Promises and async/await actually fix it under the hood?
Callback hell is what happens when you chain several async operations that each depend on the previous one's result, using nested callbacks, and the code drifts rightward with every additional step, becoming hard to read and reason about, especially for error handling since every level needs its own error check. Promises didn't remove the async nature of the work, they just gave you a flatter way to chain .then() calls instead of nesting. async/await goes a step further and lets you write asynchronous code that reads top to bottom like synchronous code, but it's still Promises underneath, await is really just syntax sugar for chaining .then() and pausing the function's execution until it resolves.
Modules, require vs import, and npm
1. What's the actual difference between CommonJS (require) and ES modules (import) in Node?
CommonJS loads modules synchronously, at the exact line require() is called, and it's what Node used exclusively for years. ES modules are loaded asynchronously and statically analyzed before execution, which is what lets bundlers do tree-shaking, and imports are hoisted to the top of the file regardless of where you wrote them. Node supports both today, CommonJS by default, ES modules if you set "type": "module" in package.json or use a .mjs extension.
2. Can you mix require and import in the same project? What breaks?
You can, but not freely in the same file. A CommonJS file can't use import syntax directly, and an ES module file can't use require() directly (though there's a workaround using createRequire). The common friction point is that not every npm package publishes both formats, so if you switch your project to ES modules, you might hit an older CommonJS-only package that doesn't play nicely with a default import the way you'd expect, and vice versa.
3. What does module.exports vs exports actually do, and why doesn't reassigning exports directly work the way people expect?
Both start out pointing to the same object. exports is really just a shorthand reference to module.exports. Adding properties to exports (like exports.foo = ...) works fine because you're mutating the same object module.exports also points to. But if you reassign exports directly, like exports = { foo: ... }, you've just pointed that local variable at a brand new object, while module.exports (the thing Node actually returns from require()) still points to the original, so your new object is silently thrown away. If you want to export a single value instead of a set of named properties, you have to reassign module.exports itself.
4. How does Node resolve a module when you require('some-package'), what's the actual lookup order?
Node first checks if it's a core module (like fs or path). If not, and the path starts with ./ or ../, it resolves it relative to the current file. Otherwise, it treats it as a package name and walks up the directory tree checking each node_modules folder along the way, starting from the current directory and going up toward the filesystem root, until it finds a match or runs out of directories to check.
5. What's the difference between dependencies, devDependencies, and peerDependencies in package.json?
dependencies are packages your code needs at runtime in production, like Express or a database driver. devDependencies are only needed during development, testing frameworks, linters, build tools, they don't get installed in a production-only install (npm install --omit=dev). peerDependencies are used mainly by libraries, declaring "this package expects the consuming project to already have a compatible version of X installed," commonly used by plugins that need to match the host app's version of something like React.
6. What does package-lock.json actually guarantee that package.json alone doesn't?
package.json typically specifies version ranges (like ^4.18.0), which means two different installs, on your machine and a teammate's, or in CI, could resolve to slightly different actual versions if new releases came out in between. package-lock.json pins the exact resolved version (and the full dependency tree) that was installed, so everyone who runs npm ci gets byte-for-byte the same dependency versions, which matters a lot for avoiding "works on my machine" bugs caused by a transitive dependency quietly bumping a version.
Streams and Buffers
1. What is a stream in Node, and why would you use one instead of just reading a whole file into memory?
A stream lets you process data piece by piece as it arrives, instead of waiting for the entire thing to load into memory first. If you're reading a 2GB file, loading it all with fs.readFile means holding all 2GB in memory at once, which can crash a process with limited memory. A stream reads it in small chunks, processes each chunk, and moves on, so memory usage stays roughly constant regardless of file size.
2. What are the four types of streams in Node?
Readable (you read data from it, like fs.createReadStream), Writable (you write data to it, like fs.createWriteStream or an HTTP response), Duplex (both readable and writable, like a TCP socket), and Transform (a duplex stream that modifies data as it passes through, like a gzip compression stream).
3. What is backpressure, and why does it matter when piping streams?
Backpressure happens when a writable stream can't keep up with the rate a readable stream is producing data, its internal buffer starts filling up. If you ignore this and keep writing anyway, memory usage climbs unbounded. stream.pipe() handles backpressure for you automatically, pausing the readable side when the writable side's buffer is full and resuming once it drains, which is one of the big reasons to prefer pipe() over manually reading and writing chunks yourself.
const fs = require('fs');
const readStream = fs.createReadStream('bigfile.txt');
const writeStream = fs.createWriteStream('copy.txt');
readStream.pipe(writeStream); // backpressure handled automatically
4. What is a Buffer, and why does Node need it separate from a regular JS array or string?
A Buffer is a fixed-size chunk of raw binary data sitting outside the regular V8 heap. Node needs it because a lot of I/O, reading files, network data, TCP sockets, deals in raw bytes, not text, and JavaScript strings are always encoded as UTF-16 internally, which isn't a good fit for arbitrary binary data. Buffers give you a way to work with that raw byte data directly, and to convert it to a string in whatever encoding actually applies (utf8, base64, and so on) only when you need to.
5. How would you stream a large file to an HTTP response instead of loading it all into memory first?
Instead of reading the whole file with fs.readFile and sending it in one shot, you'd create a read stream and pipe it directly into the response object, which is itself a writable stream. This way the file gets sent to the client in chunks as it's read from disk, keeping memory usage low regardless of file size.
app.get('/download', (req, res) => {
const stream = fs.createReadStream('large-video.mp4');
stream.pipe(res);
});
Express.js and Middleware
1. What is middleware in Express, and what does calling next() actually do?
Middleware is a function that sits in the request-response cycle with access to req, res, and a next function, and it can inspect or modify the request, end the response early, or pass control along to the next matching middleware or route handler. Calling next() is what hands control off to whatever comes next in the chain, if you forget to call it (and don't send a response yourself), the request just hangs forever with no response.
2. What's the difference between application-level, router-level, and error-handling middleware?
Application-level middleware is bound directly to the app instance with app.use() and runs for every matching request across the whole app. Router-level middleware is the same idea but scoped to a specific express.Router() instance, so it only applies to routes registered on that router. Error-handling middleware is a special case, it's defined with four parameters instead of three ((err, req, res, next)), and Express specifically looks for that four-argument signature to know a middleware function is meant to handle errors rather than regular requests.
3. How does Express know a middleware function is meant for error handling?
Purely by its arity, the number of parameters it declares. A normal middleware function has three parameters, (req, res, next). An error handler has four, (err, req, res, next). Express checks the function's length property under the hood to decide which bucket it falls into, so if you accidentally leave out the next parameter on a regular middleware or add an extra unused one on an error handler, Express can misclassify it.
4. How would you structure a large Express app so routes, controllers, and middleware don't turn into one giant file?
A common pattern splits things into a routes/ folder (just defining URL paths and which controller handles them), a controllers/ folder (the actual request-handling logic), a middleware/ folder (auth checks, validation, error handling), and a services/ or models/ layer for business logic and database access. Each route file exports an express.Router() instance, and the main app file mounts each router under its base path, so app.use('/users', userRoutes) instead of defining every single route directly in one massive app.js.
5. What's the difference between app.use() and app.get()/app.post()?
app.use() mounts middleware (or a router) for all HTTP methods on a given path, and by default it matches any path that starts with the one you gave it, not just an exact match. app.get(), app.post(), and the other HTTP-verb methods register a route handler for that specific method and an exact path match (unless you're using path patterns). In short, app.use is for middleware that applies broadly, the verb methods are for defining actual endpoints.
Error Handling and Debugging
1. What's the difference between an operational error and a programmer error, and why does that distinction matter for how you handle it?
Operational errors are expected failures that can happen during normal operation, a database connection timing out, a file not existing, invalid user input, and your app should handle these gracefully, log them, return a proper error response, maybe retry. Programmer errors are actual bugs, calling a function with the wrong arguments, a typo, referencing something undefined, and these usually shouldn't be "handled" quietly, since the state of the program is now unpredictable, the safer move is often to log it, alert someone, and let the process restart (which is exactly what tools like PM2 or Kubernetes are built to do).
2. Why can't you catch an error thrown inside a callback with a regular try/catch wrapped around the function that registered it?
Because by the time the callback actually runs, the original function has already finished executing and its try/catch block is off the call stack entirely. The callback runs later, asynchronously, in a completely separate turn of the event loop, so there's no active try/catch around it anymore. This is exactly the kind of problem Promises and async/await solve, since await keeps you inside the same logical function context, letting a surrounding try/catch actually catch a rejected promise.
3. What happens if an unhandled promise rejection occurs in a Node app, and how has this behavior changed over versions?
In older Node versions, an unhandled rejection just logged a warning and the process kept running, which was risky because your app could be silently in a broken state. In modern Node (since v15), an unhandled promise rejection crashes the process by default, the same as an uncaught exception, which is a deliberate choice to surface these bugs loudly instead of letting them fail silently in production.
4. How do you handle errors in async/await code cleanly across many functions without repeating try/catch everywhere?
In Express specifically, a common pattern is a small wrapper function that catches any rejected promise from an async route handler and forwards it to next(err), so your centralized error-handling middleware deals with it instead of every route needing its own try/catch block.
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
app.get(
'/users/:id',
asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user);
})
);
5. What is process.on('uncaughtException'), and why is it usually a bad idea to just catch and continue?
It's a global event handler that fires when an error is thrown and nothing else caught it, your last line of defense before the process would otherwise crash. The problem with just logging it and letting the app keep running is that the error left your program in an unknown state, memory could be inconsistent, a lock might never get released, something might be half-initialized, so continuing to serve requests afterward is genuinely risky. The recommended pattern is to log the error, do any necessary cleanup, and then exit the process deliberately, letting a process manager restart it fresh.
Working with Databases
1. Why is Node particularly well suited to I/O-heavy work like handling many concurrent database queries?
Because a database query is I/O, not CPU work, your Node process fires off the query and immediately moves on to handle other requests while waiting for the response, instead of a thread sitting idle blocked on that query. This means a single Node process can have hundreds of database queries "in flight" at once without needing hundreds of threads, which is a very different resource model than a traditional thread-per-request server.
2. What's the difference between using a connection pool and opening a new database connection per request?
Opening a fresh connection per request is expensive, the handshake, authentication, and setup all add real latency, and under load you can exhaust the database's max connection limit fast. A connection pool keeps a set of already-open connections ready to be reused, a request borrows one, uses it, and returns it to the pool instead of tearing it down, which is dramatically more efficient and is what virtually every Node database library (Mongoose, Sequelize, the pg driver) does by default.
3. What's a practical difference in how you'd query MongoDB with Mongoose versus a SQL database with something like Sequelize or Prisma?
With Mongoose, you're working with a schema-flexible document model, queries return nested objects naturally, and relationships are usually either embedded documents or manual populate() calls resolving references. With a SQL ORM like Sequelize or Prisma, you're working with a fixed relational schema, joins are a first-class concept, and you generally get stronger guarantees around data structure since the database itself enforces the schema, not just your application code.
4. How do you avoid N+1 query problems in a Node/Express API?
The N+1 problem happens when you fetch a list of items with one query, then loop over that list and fire a separate query for each item's related data, for example fetching 50 posts, then querying each post's author individually, 51 queries total instead of 2. The fix is to batch the related fetch into a single query, using a SQL join, Prisma's include, or Mongoose's populate(), instead of querying inside a loop.
Security and Authentication
1. How would you implement authentication in a Node API, sessions vs JWTs, and what's the actual tradeoff?
Sessions store the auth state server-side, the client just holds a session ID cookie, which means you can instantly revoke a session by deleting it server-side, but it requires shared session storage (like Redis) if you're running multiple server instances. JWTs are stateless, the token itself carries the user's claims and is signed, so any server instance can verify it without a shared store, but you can't easily revoke a single token before it expires without adding extra infrastructure like a blocklist, which somewhat defeats the "stateless" benefit.
2. What is CORS, and why does a Node API need to explicitly configure it?
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that blocks a web page from making requests to a different origin (different domain, port, or protocol) unless the server explicitly allows it via response headers. Your Node API needs to configure it because by default, a frontend running on localhost:3000 trying to hit an API on localhost:5000 (different origin) will get blocked by the browser unless your server sends back the right Access-Control-Allow-Origin headers, which is what a package like the cors middleware handles for you.
3. What are common security mistakes in Express apps that actually show up in real code reviews?
Not validating or sanitizing user input before it hits a database query (opening the door to injection attacks), storing passwords in plain text or with weak hashing instead of something like bcrypt, leaving default error handlers on in production that leak stack traces to clients, not rate-limiting login or password-reset endpoints, and trusting client-supplied data (like a user role sent in the request body) instead of deriving it from the authenticated session.
4. How do you securely store and compare passwords in Node?
You never store the plain password, you hash it with a slow, purpose-built algorithm like bcrypt (or argon2), which includes a per-password salt automatically. To check a login, you don't decrypt anything, you hash the submitted password the same way and compare it to the stored hash, which bcrypt gives you a built-in compare function for.
const bcrypt = require('bcrypt');
const hashed = await bcrypt.hash(plainPassword, 10);
const isMatch = await bcrypt.compare(plainPassword, hashed);
5. What's the point of Helmet, and what does it actually protect against?
Helmet is a middleware that sets a bunch of security-related HTTP response headers for you, things like Content-Security-Policy, X-Frame-Options (protecting against clickjacking), and Strict-Transport-Security. None of it is exotic, you could set these headers manually yourself, but Helmet bundles sane defaults so you don't forget one, and it's usually one of the first things added to a production Express app.
Performance, Clustering and Scaling
1. Node is single-threaded, so how do you actually take advantage of a multi-core server?
You run multiple Node processes, one per CPU core, and load balance requests across them, since a single Node process can only use one core no matter how much traffic it gets. The built-in cluster module does exactly this, forking worker processes that all share the same server port, with the OS (or Node's internal round-robin) distributing incoming connections between them. In production, this is often handled by a process manager like PM2's cluster mode, or just running multiple container replicas behind a load balancer.
2. What's the difference between the cluster module and worker_threads?
cluster spins up entirely separate processes, each with its own memory space and event loop, mainly to scale handling of many concurrent requests across CPU cores. worker_threads spins up threads within the same process that can share memory (via SharedArrayBuffer) and are meant more for offloading a specific CPU-heavy computation without blocking the main thread, not for general request handling. Use cluster (or just run more instances) to scale a web server, use worker_threads when a specific task needs real parallel computation.
3. What is PM2, and what does it give you beyond just running node server.js?
PM2 is a process manager for Node apps that handles things you'd otherwise have to build yourself: automatically restarting the app if it crashes, cluster mode to run one instance per CPU core with load balancing built in, log management, and zero-downtime reloads when you deploy a new version. Running node server.js directly works fine for local dev, but in production you want something watching the process and restarting it if it dies, which is exactly what PM2 (or an equivalent, like relying on Kubernetes restarts) is for.
4. How would you profile a Node app to find out why it's slow?
Node has a built-in --prof flag that generates a V8 profiling log you can process into a readable report, and --inspect lets you attach Chrome DevTools directly to a running Node process for CPU profiling and heap snapshots. For production, you'd typically reach for an APM tool (like New Relic, Datadog, or Clinic.js) that tracks request latency, event loop lag, and memory usage over time, since reproducing a production-only slowdown locally isn't always possible.
5. What's the difference between horizontal and vertical scaling for a Node app, and which is generally preferred?
Vertical scaling means giving your existing server more resources, more CPU, more RAM, on the same machine. Horizontal scaling means running more instances of your app across multiple machines (or containers) and distributing traffic between them with a load balancer. Horizontal scaling is generally preferred for Node apps because a single Node process can only use one CPU core no matter how powerful the machine is, so past a certain point, just throwing more hardware at one instance stops helping, you get far more benefit from running multiple instances instead.
Struggling to Find a Job? Get Specific Batch Wise job Updates ✅ Check now