Node.js backend interviews usually start with the event loop and non-blocking I/O, then move into streams, production operations, authentication, and scaling choices such as cluster mode versus worker threads. Knowing Express routes is baseline; interviewers probe what blocks the loop, how you handle backpressure, and how you run Node reliably in production.
Below are 40+ Node.js developer interview questions grouped from runtime fundamentals through senior architecture and production scenarios. Junior sections build core runtime knowledge; later sections assume you have shipped Node services before. For TypeScript interview questions (Express handlers, ESM, runtime validation), see TypeScript interview questions. For MongoDB interview questions (indexes, aggregation, Mongoose), see MongoDB interview questions. For HTTP API and full-stack depth, see full stack developer interview questions.
Interview context and how to prepare
What Node.js developer interviews actually test
Node.js interviews usually test whether you can build and debug real backend applications, not just recite syntax.
| Level | Typical focus |
|---|---|
| Junior | JavaScript basics, Node core modules, callbacks, Promises, simple Express routes |
| Mid | Event loop, async/await, middleware flow, database calls, error handling, testing |
| Senior | Performance, scaling, worker threads, auth, observability, API design, production debugging |
Live coding is often based on:
- Array/object manipulation in JavaScript or TypeScript
- Fixing async bugs
- Writing or debugging an API endpoint
- Explaining why a Node.js service became slow
The best preparation is not memorizing 100 answers. You should be able to explain how Node handles I/O, why blocking code is dangerous, and how you would debug a production issue.
What interview formats should you expect
A typical Node.js developer interview may include:
- Recruiter or hiring manager screen — background, projects, stack, communication
- Online assessment — JavaScript, async behavior, small API or debugging task
- Technical deep dive — event loop, Promises, Express, database usage, error handling
- Live coding — usually 45–60 minutes in HackerRank, CoderPad, or a shared IDE
- System design — more common for senior roles
For take-home assignments, common tasks include:
- Build a REST API
- Add validation and error handling
- Connect to a database
- Write tests
- Provide a clean README
Do not treat take-home assignments as only “working code.” Interviewers also check structure, naming, edge cases, and whether another developer can understand your solution.
A realistic 4–5 week prep plan
| Week | Focus |
|---|---|
| 1 | Node runtime, modules, blocking vs non-blocking I/O |
| 2 | Event loop, Promises, async/await, error handling |
| 3 | Express routes, middleware, REST APIs, validation, HTTP requests |
| 4 | Streams, files, testing, database integration, authentication basics |
| 5 | Performance, worker threads, clustering, logging, monitoring, mock interviews |
For each topic, prepare in this order:
- Explain the concept in simple words
- Write a small example
- Explain one common mistake
- Explain how it appears in production
For example, do not only say “Node is non-blocking.” Explain what happens when a request handler uses readFileSync() or performs a large CPU loop.
TypeScript and NestJS in 2026
You do not always need TypeScript or NestJS, but many backend teams now prefer them for larger Node.js applications.
Still, interviewers usually expect you to understand Node.js runtime concepts first:
- Event loop behavior
- Blocking vs non-blocking code
- Promise and async/await flow
- Stream backpressure
- Error handling
- API design
If the job description mentions TypeScript, prepare:
- Types vs interfaces
- Generics
- DTOs
- Type narrowing
- Error typing
- Basic project structure
If the job description mentions NestJS, prepare:
- Modules
- Controllers
- Providers
- Dependency injection
- Guards
- Pipes
- Interceptors
NestJS is built on top of Node.js concepts. If you understand Node, Express-style request handling, middleware flow, and dependency management, NestJS becomes easier to explain.
Node.js fundamentals
What is Node.js and how is it different from the browser?
What interviewers are testing: Whether you distinguish the JavaScript language from its host environment and know which runtime APIs Node provides that browsers do not.
Node.js is a JavaScript runtime used to run JavaScript outside the browser, commonly for servers, CLIs, APIs, build tools, and automation.
The browser and Node.js both run JavaScript, but they provide different APIs.
| Area | Browser | Node.js |
|---|---|---|
| Main use | User interface | Server-side and tooling |
| Host globals | window, document |
process, Buffer |
| APIs | DOM, Web APIs, browser storage | fs, http, crypto, stream, path |
| File access | Restricted | Available through Node modules |
| Modules | ES modules, bundlers | CommonJS and ES modules |
Both environments provide the standard globalThis. Older Node.js code may also use the legacy global alias.
The key difference is the host environment. JavaScript is the language, but the browser and Node.js provide different capabilities around it. See Node.js global objects explained for process, Buffer, and other runtime globals.
A simple way to answer:
- Browser JavaScript interacts with pages and users
- Node.js JavaScript interacts with files, networks, processes, servers, and backend systems
A strong answer is:
Node.js is a JavaScript runtime for running JavaScript outside the browser. The language is the same, but the environment is different: browsers provide DOM APIs, while Node provides server-side APIs like file system, HTTP, streams, crypto, and process control.
What roles do V8 and libuv play?
What interviewers are testing: Whether you understand that V8 executes JavaScript while libuv provides the event loop and asynchronous I/O machinery—and that not all Node work happens on the JavaScript thread.
Node.js is built from multiple parts. Two important ones are V8 and libuv.
- V8 executes JavaScript code
- libuv provides the event loop, async I/O support, and a thread pool for some operations
A common interview mistake is saying “Node is single-threaded, so everything runs on one thread.” That is only partly correct.
Your JavaScript code usually runs on the main thread, but Node can use the operating system and libuv thread pool for background work. Network sockets are commonly handled through OS event notification rather than consuming a libuv worker thread; the thread pool is used by APIs such as filesystem operations, dns.lookup(), selected crypto work, and zlib.
Common thread-pool examples:
- File system operations
- Some crypto operations
dns.lookup()(not the same asdns.resolve*()APIs)- Compression
So the better explanation is:
JavaScript execution is single-threaded by default, but Node.js uses libuv and the operating system to handle asynchronous work efficiently.
Example interview flow:
- Request reaches Node.js
- JavaScript code runs on the main thread
- Async I/O is delegated
- Event loop receives completion callbacks
- Callback or Promise continuation runs when the main thread is free
A strong answer is:
V8 runs JavaScript, while libuv gives Node.js its event loop and async I/O capabilities. My JavaScript runs on the main thread, but libuv and the operating system help Node wait for I/O without blocking every request.
Why is Node.js called single-threaded?
What interviewers are testing: Whether you can explain why one JavaScript thread can still serve many concurrent I/O-bound requests and where CPU-bound work breaks that model.
Node.js is called single-threaded because your JavaScript application code normally runs on one main thread.
This means only one piece of JavaScript executes at a time in a single Node.js process.
However, Node.js can still handle many concurrent requests because it does not create one JavaScript thread per request. Instead, it uses:
- Event loop
- Non-blocking I/O
- OS-level async operations
- libuv thread pool for some background tasks
This is why Node.js works well for I/O-heavy applications such as APIs, chat apps, dashboards, and proxy services.
But CPU-heavy JavaScript is different. If one request performs a large calculation, image processing, huge JSON parsing, or a long loop, it can block the event loop and delay other requests.
For CPU-heavy work, use:
- Worker threads
- Child processes
- Job queues
- Separate services
- Native/database-side processing where appropriate
See is Node.js single-threaded? for how the main thread, libuv, and the thread pool interact.
A strong answer is:
Node.js is single-threaded for JavaScript execution, not for every internal operation. It handles concurrency through the event loop and async I/O, but CPU-heavy JavaScript can block the main thread, so I would move that work to worker threads, another process, or a background queue.
CommonJS vs ES modules — when does each matter?
What interviewers are testing: Whether you understand CommonJS vs ESM syntax, interop pitfalls, and when each appears in real Node projects.
Node.js supports two module systems: CommonJS and ES modules.
| Feature | CommonJS | ES modules |
|---|---|---|
| Syntax | require() / module.exports |
import / export |
| Module model | require() / module.exports; synchronous require() semantics |
Standard import / export module graph; supports top-level await |
| Common in | Older Node.js projects | Modern JavaScript and TypeScript projects |
| File indicators | .cjs or default in many older projects |
.mjs or "type": "module" |
CommonJS example:
const fs = require('node:fs');
module.exports = {
readConfig
};ES module example:
import fs from 'node:fs';
export function readConfig() {}Important interview points:
- CommonJS is still common in older Node.js projects
- ES modules are the modern JavaScript standard
- Mixing both can create interop issues
- CommonJS exposes
__dirnameand__filename; in modern Node.js ES modules, equivalent path information is available throughimport.meta.dirnameandimport.meta.filenameon supported current releases, withimport.meta.urlremaining the standard module URL - Many TypeScript projects compile to either CommonJS or ESM depending on configuration
In real projects, the best choice depends on the existing codebase, tooling, Node.js version, and deployment environment.
A strong answer is:
CommonJS uses
require()and is common in older Node.js projects. ES modules useimportandexportand are the modern standard. I would avoid mixing both unless necessary, because interop, default exports, and path handling can create confusion.
What is blocking vs non-blocking code?
What interviewers are testing: Whether you recognize blocking calls on the main thread and how they hurt concurrent request handling in production.
Blocking code stops the main JavaScript thread until the operation finishes.
Non-blocking code starts an operation and lets Node.js continue handling other work while waiting for the result.
Example of blocking code:
// Bad inside a request handler
const data = fs.readFileSync('/large-file.json');Example of non-blocking code:
// Better inside a request handler
const data = await fs.promises.readFile('/large-file.json');Blocking code is dangerous in Node.js because one slow operation can delay unrelated requests.
Common blocking examples:
fs.readFileSync()inside an API route- Large
JSON.parse()on big payloads - CPU-heavy loops
- Synchronous crypto operations
- Image/file processing in the request thread
Non-blocking does not mean “faster for one request.” It means Node can keep serving other requests while waiting for I/O.
In interviews, connect this to production:
- Blocking code increases latency
- Event loop delay goes up
- Requests queue behind slow work
- Health checks may fail under load
- Users see timeouts
A strong answer is:
Blocking code holds the main thread and prevents Node.js from serving other requests. Non-blocking code delegates I/O and resumes when the result is ready. In a server, I would avoid synchronous file, crypto, or CPU-heavy work inside request handlers.
What is the difference between npm and npx?
What interviewers are testing: Whether you distinguish dependency management from running package binaries and understand lockfile discipline in CI.
npm is the package manager for Node.js. It is used to install, update, remove, and manage dependencies.
Common npm uses:
npm install express
npm install
npm run test
npm run buildnpx runs package binaries—often from the local project or package cache—without requiring a separate global install.
Common npx uses:
npx eslint .
npx create-next-app my-app
npx prisma migrate devSimple difference:
| Tool | Purpose |
|---|---|
npm |
Install and manage packages |
npx |
Run a package command or binary |
Important interview points:
- Prefer local project binaries over global installs
- Use
package-lock.jsonfor repeatable dependency resolution - Use
npm ciin CI/CD for clean, lockfile-based installs - Be careful when running unknown packages with
npx
npx first resolves suitable local or package-cache binaries and may fetch a package when it is not already available, so package identity and version still matter.
Example:
If eslint is installed in your project, this command can run the local binary:
npx eslint .For production teams, dependency management is not only about installing packages. It also includes version pinning, lockfiles, security audits, and repeatable builds.
A strong answer is:
npm manages dependencies, while npx runs package binaries, often from the local project or temporarily from the registry. In production projects, I would rely on lockfiles, use
npm ciin CI, avoid unnecessary global packages, and audit dependencies regularly.
Event loop and async scheduling
What are the phases of the Node.js event loop?
What interviewers are testing: Whether you understand enough event-loop ordering to diagnose scheduling and latency problems rather than merely memorizing phase names.
The Node.js event loop decides when async callbacks run. It is provided by libuv and runs work in phases.
The main libuv phases interviewers usually discuss are pending callbacks, poll, check, close callbacks, and timers, plus internal idle/prepare phases. The important part is what each queue represents rather than memorizing a permanently fixed circular order.
| Phase | Role |
|---|---|
| Pending callbacks | Some deferred system-level callbacks |
| Poll | Receives new I/O events and runs I/O callbacks |
| Check | Runs setImmediate() callbacks |
| Close callbacks | Runs close events like socket.on('close') |
| Timers | Callbacks from setTimeout() and setInterval() |
Microtasks are not a separate libuv phase. Node also drains:
process.nextTick()queue- Promise microtasks /
queueMicrotask()
These run between normal callback execution points, so they can affect ordering.
Since libuv 1.45 / Node 20, timers are processed after poll rather than both before and after it, which changes some setImmediate()/setTimeout() edge cases.
A strong answer is:
I focus on what each phase queue represents—poll for I/O, check for
setImmediate(), timers forsetTimeout/setInterval—and how microtasks andnextTickinterleave. Since Node 20, timer ordering relative to poll changed, so I reason about scheduling rather than memorizing one fixed mnemonic.
Difference between process.nextTick(), setImmediate(), and setTimeout(fn, 0)?
What interviewers are testing: Whether you know the scheduling priority of nextTick, microtasks, timers, and setImmediate—and the starvation risk of overusing nextTick.
These APIs schedule callbacks at different points.
| API | When it runs |
|---|---|
process.nextTick() |
After current JavaScript finishes, before the event loop continues |
queueMicrotask() / Promise jobs |
Microtask queue, after nextTick in typical CommonJS execution |
setTimeout(fn, 0) |
Timers phase, after the timer threshold is reached |
setImmediate() |
Check phase, usually after poll/I/O callbacks |
process.nextTick() has very high priority. If you recursively schedule too many nextTick callbacks, you can starve I/O and timers.
For modern userland code, prefer queueMicrotask() unless you specifically need process.nextTick() behavior. Node.js now marks process.nextTick() as legacy.
Important interview nuance:
- At top level,
setTimeout(fn, 0)andsetImmediate()ordering can be context-dependent. - Inside an I/O callback,
setImmediate()usually runs beforesetTimeout(fn, 0).
A strong answer is:
process.nextTick()drains before the event loop continues; Promise/queueMicrotask()jobs use the microtask queue. Timers andsetImmediate()belong to later event-loop phases. I would avoid overusingnextTickbecause it can starve I/O.
How do Promises interact with the event loop?
What interviewers are testing: Whether you understand Promise and await scheduling as microtasks and how they interact with the event loop.
Promise callbacks do not run in the normal timer or I/O phases. They run in the microtask queue.
Examples of microtasks:
Promise.then()Promise.catch()Promise.finally()awaitcontinuationqueueMicrotask()
When an await completes, the remaining part of the async function is scheduled as a microtask.
Example:
console.log('start');
Promise.resolve().then(() => console.log('promise'));
console.log('end');Output:
start
end
promiseThe Promise callback runs after the current synchronous code finishes.
Microtasks are useful, but too many chained microtasks can delay timers and I/O. This can happen with recursive Promise chains or repeated queueMicrotask() calls.
Exact ordering relative to process.nextTick() can depend on the execution context, so interview answers should focus on the separate queues rather than memorizing one universal output.
A strong answer is:
Promises run their
.then()andawaitcontinuations as microtasks. That means they run after the current JavaScript stack finishes, but before many normal event loop callbacks like timers or I/O callbacks.
What does blocking the event loop mean in production?
What interviewers are testing: Whether you can identify event-loop blocking causes and mitigation in production APIs.
Blocking the event loop means one piece of JavaScript keeps the main thread busy, so Node.js cannot quickly process other requests, timers, or I/O callbacks.
Common symptoms:
- Latency increases on many endpoints
- Requests time out
- Timers fire late
- Health checks fail under load
- CPU goes high but throughput does not improve
Common causes:
- Synchronous file operations inside request handlers
- Large
JSON.parse()orJSON.stringify() - CPU-heavy loops
- Synchronous crypto
- Large in-memory data processing
- Recursive
process.nextTick()or Promise chains
Better fixes:
- Use async I/O
- Stream large files or payloads
- Move CPU-heavy work to worker threads or background jobs
- Add request size limits
- Measure event loop delay using
perf_hooks.monitorEventLoopDelay()
A strong answer is:
Blocking the event loop means the main JavaScript thread is busy, so all requests suffer, not just one. I would look for sync file/crypto calls, large JSON work, CPU-heavy loops, or recursive microtasks, then move the work to async I/O, streams, workers, or background jobs.
What work uses the libuv thread pool?
What interviewers are testing: Whether you know which async APIs use the libuv thread pool versus OS event notification and how pool saturation shows up under load.
Node.js uses the libuv thread pool when an async API cannot be handled directly by the operating system event mechanism.
Common examples:
- File system APIs such as
fs.readFile() dns.lookup()specifically (uses OS name resolution and can use the libuv thread pool—not the same asdns.resolve*()APIs)- Crypto operations such as
crypto.pbkdf2()andcrypto.scrypt() - Compression APIs from
zlib
The default libuv thread pool size is 4. It can be changed using UV_THREADPOOL_SIZE before the Node.js process starts.
Example:
UV_THREADPOOL_SIZE=8 node server.jsA saturated thread pool can make unrelated operations slow. For example, many expensive crypto tasks can delay file system operations because both may use the same pool.
Do not blindly increase the pool size. First check:
- Which operation is slow
- Whether the thread pool is saturated
- CPU usage
- Request latency
- Whether work should move to workers or a queue
A strong answer is:
The libuv thread pool is used by filesystem APIs,
dns.lookup(), selected crypto operations, and zlib. Network sockets and APIs such asdns.resolve*()use different mechanisms, so I don't assume every asynchronous Node API consumes a worker-pool thread. The default pool size is 4, and increasing it can help some workloads, but I would profile first because a larger pool can also increase CPU and contention.
Predict output: setTimeout vs setImmediate vs Promise.
What interviewers are testing: Whether you reason about scheduling order without memorizing a single fixed output for all contexts.
Classic interview example:
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));Typical CommonJS top-level output starts with:
nextTick
promiseAfter that, the order of timeout and immediate can be context-dependent:
timeout
immediateor:
immediate
timeoutWhy?
process.nextTick()runs before Promise microtasks in typical CommonJS execution- Promise callbacks run as microtasks
setTimeout(0)runs in the timers phasesetImmediate()runs in the check phase- Top-level timer vs immediate ordering can vary based on timing and runtime context
Inside an I/O callback, setImmediate() usually runs before setTimeout(0).
A strong answer is:
I would not memorize only one full output. I know
nextTickruns first, then Promise microtasks. The order betweensetTimeout(0)andsetImmediate()can depend on context, but inside an I/O callbacksetImmediate()usually runs first.
Async patterns and error handling
How do Promises and async/await improve callback code?
What interviewers are testing: Whether you explain how Promises and async/await improve composition and error routing over nested callbacks.
Callbacks work, but deeply nested callbacks become hard to read, test, and handle errors in. This is often called callback hell.
Promises improve this by making async work composable:
.then()for success.catch()for errorsPromise.all()for parallel workPromise.race()for timeout-like patterns
async/await makes Promise-based code look more like synchronous code.
async function loadUser(id) {
const user = await db.findUser(id);
if (!user) {
throw new Error('User not found');
}
return user;
}The important interview point is not only readability. Promises and async/await also make it easier to compose async operations and route errors to centralized handlers—catch locally only when adding useful context, recovering, translating the error, or performing cleanup.
See try-catch in Node.js.
A strong answer is:
Promises and async/await make asynchronous code easier to read, compose, and handle errors in. Instead of nesting callbacks, I can write sequential-looking code with
awaitand usetry/catchfor errors.
Promise.all vs Promise.allSettled vs Promise.race?
What interviewers are testing: Whether you pick the right Promise combinator and understand that race does not cancel losing work.
These methods are used to coordinate multiple Promises.
| Method | Behavior | Use when |
|---|---|---|
Promise.all() |
Rejects as soon as one Promise rejects | All results are required |
Promise.allSettled() |
Waits for every Promise and returns success/failure status | Partial failure is acceptable |
Promise.race() |
Returns the first settled Promise, success or failure | You need the first result or timeout pattern |
Promise.any() |
Returns first fulfilled Promise; rejects only if all fail | Any successful result is enough |
Examples:
- Use
Promise.all()when loading user, orders, and permissions where all are required - Use
Promise.allSettled()in batch jobs where some records may fail - Use
Promise.race()to apply timeout behavior—but a timeout Promise inPromise.race()only settles the race; it does not automatically cancel the underlying I/O. UseAbortControlleror an API-specific cancellation mechanism when abandoned work should actually stop. - Use
Promise.any()when calling multiple mirrors/providers and one success is enough
A strong answer is:
Promise.allis for all-or-nothing work,allSettledis for batch work with partial failure,racereturns the first settled Promise, andanyreturns the first successful Promise.
How do you handle unhandled promise rejections?
What interviewers are testing: Whether you treat unhandled rejections as defects with bounded shutdown rather than resuming after unknown failures.
An unhandled rejection happens when a Promise rejects but no .catch() or try/catch handles it in time.
Bad example:
doWork(); // returns a Promise, but nobody awaits or catches it
Better:
try {
await doWork();
} catch (err) {
logger.error({ err }, 'doWork failed');
}Treat an unhandled rejection as a serious programming defect. Modern Node's default --unhandled-rejections=throw behavior turns an unhandled rejection into an uncaught exception when no handler is installed.
In production:
- Always await, return, or otherwise handle Promises
- For intentional fire-and-forget work, attach
.catch()and log or handle failure - Express 4 needs explicit async-error forwarding; Express 5 forwards rejected handler Promises to error middleware
- Treat genuinely unhandled rejections as programming defects
- Log and monitor unhandled rejections
- Let a process supervisor restart the application after controlled shutdown—not
process.exit()in the handler itself
process.exit() terminates synchronously and can truncate pending logs/output and skip asynchronous cleanup. If you implement controlled shutdown, stop accepting new work, perform bounded cleanup, set a non-zero exit status, and then terminate.
process.on('unhandledRejection', (reason, promise) => {
logger.error({ reason, promise }, 'unhandled promise rejection');
shutdown('unhandledRejection'); // stop accepting work; supervisor restarts
});If you install unhandledRejection, do not use it to resume normal service after an unknown programming defect. Log the failure, initiate bounded shutdown, and rely on an external supervisor for restart.
A strong answer is:
I avoid unhandled rejections by always awaiting or catching Promises. At process level, I log them as defects, stop accepting new traffic, and let the supervisor restart—I do not treat immediate process.exit() as normal production handling.
What is EventEmitter and when do you use it?
What interviewers are testing: Whether you use in-process events appropriately and handle error listeners on EventEmitters that emit errors.
EventEmitter is Node.js's in-process publish/subscribe mechanism.
It allows one part of the application to emit an event and another part to listen for it.
const { EventEmitter } = require('node:events');
const bus = new EventEmitter();
bus.on('job:done', (id) => {
console.log('job completed', id);
});
bus.emit('job:done', 42);Many Node.js core APIs use event-based behavior, including:
- Streams
- HTTP servers
- Sockets
- Child processes
Use EventEmitter when you need in-process decoupling, such as:
- Notify when a job finishes
- React to stream events
- Trigger internal application hooks
- Implement simple plugin-style behavior
Do not use it as a replacement for distributed messaging. Across services or processes, use a real queue or broker such as Redis, RabbitMQ, Kafka, SQS, or another messaging system.
For EventEmitters that emit 'error', register appropriate error handling; an emitted 'error' event without a listener is thrown and normally terminates the process.
A strong answer is:
EventEmitter is Node’s in-process event system. I use it for decoupling components inside one process, but for communication across services I would use a message queue or broker.
How do you handle async errors in Express middleware?
What interviewers are testing: Whether you route async handler errors to centralized Express error middleware in Express 4 vs 5.
In Express, errors should reach centralized error-handling middleware.
Error middleware has four arguments:
app.use((err, req, res, next) => {
logger.error({ err }, 'request failed');
res.status(500).json({ error: 'Internal server error' });
});In Express 4, async route errors are commonly wrapped:
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 db.findUser(req.params.id);
if (!user) {
return res.status(404).json({ error: 'not found' });
}
res.json(user);
}));In Express 5, rejected Promises from async handlers are automatically forwarded to error middleware, so simple async handlers are easier to write.
Still, production-grade error handling should include:
- Clear HTTP status codes
- Safe user-facing messages
- Detailed internal logs
- Request ID / correlation ID
- No leaking stack traces in production responses
A strong answer is:
I route async errors to centralized Express error middleware. In Express 4 I wrap async handlers or call
next(err). In Express 5, rejected Promises are forwarded automatically, but I still keep consistent logging, status codes, and safe error responses.
Streams, buffers, and performance
What are the types of streams in Node.js?
What interviewers are testing: Whether you know stream types and when chunk-based processing beats loading entire payloads into memory.
Streams let Node.js process data piece by piece instead of loading everything into memory at once.
The four main stream types are:
| Stream type | Meaning | Example |
|---|---|---|
| Readable | Source of data | fs.createReadStream() |
| Writable | Destination for data | fs.createWriteStream() |
| Duplex | Both readable and writable | TCP socket |
| Transform | Duplex stream that modifies data | gzip, encryption, parser |
Streams are useful for large files, uploads, downloads, logs, network data, and compression.
Instead of this:
const data = await fs.promises.readFile('large.log');A stream can process chunks as they arrive:
fs.createReadStream('large.log').pipe(process.stdout);The interview point is: streams reduce memory usage and improve performance for large or continuous data.
A strong answer is:
Node.js has readable, writable, duplex, and transform streams. They process data in chunks, which is useful for large files, network traffic, uploads, downloads, and compression without loading everything into memory.
What is backpressure?
What interviewers are testing: Whether you know how Node prevents a fast producer from overwhelming a slower consumer and can connect write(), drain, pipe(), and pipeline().
Backpressure happens when data is being produced faster than it can be consumed.
Example:
- A readable stream is reading a large file quickly
- A writable stream is writing to a slow network or disk
- If Node keeps reading without control, memory usage can grow
In Node.js, a writable stream signals backpressure when write() returns false.
readable.on('data', (chunk) => {
if (!writable.write(chunk)) {
readable.pause();
writable.once('drain', () => {
readable.resume();
});
}
});This means:
write()returnedfalse→ writable buffer is full- Pause the readable stream
- Wait for
'drain' - Resume reading when the writable side catches up
In normal code, .pipe() handles this automatically:
readable.pipe(writable);For production stream composition, prefer stream.pipeline() / stream/promises.pipeline() because it propagates errors and coordinates teardown across the stream chain.
A strong answer is:
Backpressure means the destination cannot consume data as fast as the source produces it. In Node.js,
write()returningfalsetells me to pause reading until thedrainevent. For production, I preferpipeline()because it handles backpressure, errors, and cleanup across the chain.
What is a Buffer and why does it matter?
What interviewers are testing: Whether you understand binary data handling with Buffer and the cost of unnecessary conversions.
A Buffer is used to work with raw binary data in Node.js.
JavaScript strings are good for text, but many backend operations deal with bytes:
- File content
- TCP packets
- Images
- PDFs
- Crypto data
- Compressed data
- Binary API payloads
Example:
const buf = Buffer.from('hello');
console.log(buf);
console.log(buf.toString('utf8'));Important points:
- Buffer stores bytes, not normal JavaScript characters
- Converting large buffers to strings can be expensive
Buffer.alloc()creates a zero-filled bufferBuffer.allocUnsafe()skips zero-filling and must be completely overwritten before any data is read or exposed
In interviews, connect Buffer with streams: streams often deliver data as Buffer chunks.
A strong answer is:
A Buffer represents raw binary data in Node.js. It matters because files, sockets, crypto, images, and streams work with bytes, not just strings. I would avoid unnecessary conversion of huge buffers because it can increase memory usage and block the event loop.
Why is fs.readFileSync discouraged in API handlers?
What interviewers are testing: Whether you avoid synchronous file I/O on the request path and use async or streams instead.
fs.readFileSync() is discouraged in API handlers because it blocks the Node.js event loop.
While the file is being read:
- No other JavaScript can run in that process
- Other requests wait
- Timers may fire late
- p95/p99 latency can increase under load
Bad in request handlers:
app.get('/report', (req, res) => {
const data = fs.readFileSync('./report.json');
res.send(data);
});Better options:
app.get('/report', async (req, res, next) => {
try {
const data = await fs.promises.readFile('./report.json');
res.send(data);
} catch (err) {
next(err);
}
});For large files, streams are usually better:
app.get('/download', (req, res) => {
fs.createReadStream('./large-file.zip').pipe(res);
});readFileSync() is acceptable during startup for small config files, scripts, CLIs, or build tools. The main problem is using it inside high-traffic request paths.
A strong answer is:
fs.readFileSync()blocks the event loop, so one slow file read can delay all requests handled by that Node.js process. In API handlers, I would use async file APIs or streams, and reserve sync reads for startup scripts or small CLI tasks.
Express, REST, and authentication
How does Express middleware work?
What interviewers are testing: Whether you understand middleware ordering, next(), and four-argument error middleware.
Express middleware is a chain of functions that run during the request-response cycle.
A middleware function usually has this signature:
(req, res, next) => {}Middleware can:
- Read or modify
req - Modify
res - End the response
- Call
next()to continue to the next middleware
Order matters. Express runs middleware in the order it is registered.
Typical stack:
request
↓
logger
↓
body parser
↓
authentication
↓
routes
↓
404 handler
↓
error handlerError-handling middleware has four arguments:
(err, req, res, next) => {}A common mistake is forgetting next(). If middleware neither sends a response nor calls next(), the request hangs.
For a full Express request stack with routes and MongoDB, see Node.js CMS with Express and MongoDB.
A strong answer is:
Express middleware runs in registration order during the request-response cycle. Each middleware can modify the request, send a response, or call
next(). Error middleware is special because it has four arguments:err,req,res, andnext.
What makes a well-designed REST API in Node?
What interviewers are testing: Whether you design predictable REST APIs with correct methods, status codes, validation, and idempotency.
A well-designed REST API is predictable, consistent, and easy for clients to use.
Important qualities:
| Area | Good practice |
|---|---|
| Resource naming | Use nouns: GET /users/:id, not /getUser |
| HTTP methods | GET, POST, PUT, PATCH, DELETE correctly |
| Status codes | 201 create, 204 no content, 400 validation, 401 auth, 404 missing, 409 conflict |
| Validation | Validate input at the API boundary |
| Pagination | Use cursor or page / limit for lists |
| Filtering/sorting | Keep query parameters consistent |
| Errors | Return a consistent error shape |
| Idempotency | Make retry behavior clear for PUT, DELETE, and payment-like operations |
Example error shape:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found"
}
}For senior interviews, mention:
- Request validation using Zod, Joi, or similar
- API versioning strategy
- Rate limiting
- Idempotency keys for unsafe retries
- Observability with request IDs and structured logs
See full stack developer interview questions for REST design, status codes, and API trade-offs in depth.
A strong answer is:
A good REST API uses resource-based URLs, correct HTTP methods and status codes, validation at the boundary, pagination for lists, consistent errors, and clear idempotency rules so clients can safely retry requests.
How should you implement JWT authentication?
What interviewers are testing: Whether you decide if JWT fits the architecture and design token storage, validation, and XSS/CSRF trade-offs together.
JWT authentication should be implemented carefully because a JWT is usually a bearer token: whoever has it can use it until it expires.
JWT is not automatically preferable to server-side sessions. Choose it when stateless or verifiable tokens fit the architecture.
A common pattern is:
- User logs in with valid credentials
- Server issues a short-lived access token
- Client sends the access token with API requests
- Server verifies signature and claims
- Refresh token is used to get a new access token
When validating a JWT, check:
- Signature
- Expiry:
exp - Issuer:
iss - Audience:
aud - Algorithm allowlist
- User status or token version if revocation is required
Avoid storing sensitive data in the JWT payload. The payload is encoded, not encrypted.
For browser clients, avoid long-lived bearer tokens in JavaScript-accessible storage. HttpOnly cookies can reduce token theft through XSS, but cookie-based authentication requires appropriate CSRF defenses and SameSite policy.
For Passport.js patterns see Passport authenticate.
A strong answer is:
I first decide whether JWT is actually needed. If it is, I use short-lived access tokens, verify signature and allowed algorithm plus
exp,iss, andaud, keep sensitive data out of the payload, and design refresh-token storage, rotation, revocation, XSS, and CSRF controls together.
What security measures should Node APIs use?
What interviewers are testing: Whether you layer API security controls beyond a single helmet middleware.
A production Node.js API should include security at multiple layers, not just one middleware.
Baseline checklist:
| Area | What to do |
|---|---|
| Headers | Use helmet |
| Input | Validate input and use context-appropriate escaping or parameterized queries |
| Auth | Verify tokens, roles, and ownership |
| Rate limiting | Protect login and public APIs |
| CORS | Use explicit allowed origins |
| Secrets | Use environment variables or a vault, never commit secrets |
| Dependencies | Lockfiles, automated vulnerability/dependency scanning, timely updates, and supply-chain review (npm audit is one tool) |
| Transport | Use HTTPS/TLS |
| Errors | Do not leak stack traces to users |
| Logging | Log security events without exposing secrets |
Example CORS mistake:
Do not use wildcard CORS with credentials.For secrets, see read env vars. For TLS setup, see create an HTTPS server with Node.js.
Senior candidates should also mention:
- SQL/NoSQL injection prevention
- Password hashing with bcrypt/argon2
- Request body size limits
- CSRF protection if using cookie-based auth
- Secure cookie flags:
httpOnly,Secure,SameSite - Separate user-facing error messages from internal logs
A strong answer is:
I would secure a Node API with input validation, authentication and authorization checks, secure headers, rate limiting, strict CORS, HTTPS, safe secret handling, dependency audits, and centralized error handling that does not leak internals.
When would you choose GraphQL over REST?
What interviewers are testing: Whether you choose GraphQL vs REST based on client flexibility, caching, and operational trade-offs.
Choose GraphQL when clients need flexible data fetching from a graph of related resources.
GraphQL is useful when:
- Mobile/web clients need different fields
- REST endpoints cause over-fetching or under-fetching
- Data comes from multiple backend sources
- The frontend team benefits from a strongly typed schema
- You want clients to request exactly the shape of data they need
Choose REST when:
- The API is simple and resource-oriented
- HTTP caching/CDN behavior is important
- You are building a public API
- File downloads or static resources are involved
- The team wants simpler tooling and operations
Trade-offs:
| Area | GraphQL | REST |
|---|---|---|
| Data fetching | Flexible client-selected fields | Fixed endpoint responses |
| Caching | More complex | Easier with HTTP/CDN |
| Performance risk | N+1 resolvers, complex queries | Over-fetching or many round trips |
| Tooling | Strong schema and typed clients | Mature, simple HTTP tooling |
Senior answer should mention GraphQL risks:
- N+1 query problem
- Query depth and complexity limits
- Resolver-level authorization
- Batching/caching with DataLoader
- Monitoring slow fields and operations
A strong answer is:
I would choose GraphQL when clients need flexible, typed queries across related data. I would choose REST for simpler resource APIs, public APIs, file downloads, and CDN-friendly caching. With GraphQL, I would also plan for N+1 prevention, query limits, authorization, and monitoring.
Senior Node.js: scaling, reliability, and operations
Cluster module vs worker threads — when to use each?
What interviewers are testing: Whether you distinguish process-level traffic scaling from thread-based CPU offload and know when neither is needed because the platform already scales replicas.
Both cluster and worker_threads help Node.js use more CPU capacity, but they solve different problems.
| Area | cluster |
worker_threads |
|---|---|---|
| Execution model | Multiple Node.js processes | Multiple threads inside one process |
| Memory | Separate memory per process | Separate JS execution contexts; memory can be shared explicitly |
| Best for | Scaling HTTP servers across CPU cores | CPU-heavy JavaScript work |
| Failure isolation | Better, because one worker process can crash separately | Lower than separate processes |
| Communication | IPC between processes | Message passing between threads |
Use cluster when you want multiple Node.js processes listening for traffic and sharing load across CPU cores.
Use worker threads when the event loop is blocked by CPU-heavy work such as:
- Image processing
- Large JSON transformation
- PDF generation
- Data compression
- CPU-heavy calculations
Do not use worker threads just to make normal database or HTTP calls faster. Node's async I/O already handles that well.
In Kubernetes or container platforms, prefer horizontal scaling with multiple replicas for traffic scaling. cluster is less common when the platform already manages multiple app instances.
See Node.js multithreading with worker threads for CPU offload patterns.
A strong answer is:
I would use cluster to run multiple Node.js processes for serving traffic across CPU cores, and worker threads to offload CPU-heavy JavaScript from the main event loop. For large production traffic, I would still rely on horizontal scaling with multiple replicas.
What causes memory leaks in Node and how do you debug them?
What interviewers are testing: Whether you diagnose heap vs RSS growth and common retention patterns.
A memory leak happens when the application keeps references to objects that are no longer needed, so garbage collection cannot free them.
Common causes:
- Global arrays or maps growing without limits
- Caches without TTL or eviction
- Forgotten
setInterval()timers - Event listeners added repeatedly but never removed
- Closures holding large objects
- Request data stored accidentally in module-level variables
- Large Buffers or external memory not being released
Debugging approach:
- Track memory over time using
process.memoryUsage() - Compare heapUsed, rss, and external memory
- Reproduce the leak under load
- Take heap snapshots using
node --inspectand Chrome DevTools - Compare snapshots before and after load
- Look for growing object counts or retained references
- Check caches, listeners, timers, and queues
Useful tools:
- Chrome DevTools heap snapshots
node --inspect- Clinic.js
- APM memory graphs
- Container memory metrics
See Node.js debugging tips and tricks for heap inspection workflows.
Important distinction:
- Heap leak usually means JavaScript objects are retained
- RSS growth may include Buffers, native modules, memory fragmentation, or external memory
A strong answer is:
I would first confirm whether heap, RSS, or external memory is growing. Then I would reproduce the issue, take heap snapshots, compare retained objects, and check common sources like unbounded caches, global maps, timers, listeners, and closures.
How do you gracefully shut down a Node server?
What interviewers are testing: Whether you drain traffic and close dependencies cleanly under orchestrator termination signals.
Graceful shutdown means the server stops accepting new work, finishes current work safely, closes dependencies, and exits cleanly.
Conceptual sequence:
- Receive termination signal (
SIGTERM/SIGINT) - Make shutdown idempotent
- Mark instance not ready / begin traffic draining
- Stop accepting new work (
server.close()and stop consumers) - Allow bounded in-flight completion
- Close dependencies, telemetry, and pools
- Let handles drain or force exit at deadline
Do not assume readiness changes instantly stop traffic—load-balancer and endpoint propagation can take time.
Example:
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
function shutdown() {
const forceTimer = setTimeout(() => {
logger.error('forced shutdown after timeout');
process.exit(1);
}, 30000);
forceTimer.unref();
server.close(async () => {
try {
await db.close();
await redis.quit();
clearTimeout(forceTimer);
process.exitCode = 0;
} catch (err) {
logger.error({ err }, 'shutdown cleanup failed');
process.exitCode = 1;
}
});
}Prefer letting the event loop drain after closing handles rather than calling process.exit(0) on the normal path. Clear the force-exit timer after successful cleanup, and use unref() so the failsafe timer does not keep the process alive unnecessarily. Reserve process.exit() for timeout or fatal fallback when shutdown hangs.
In Kubernetes, this matters because pods normally receive SIGTERM before termination. If the app exits immediately, users may see failed requests during deployment.
Senior candidates should also mention:
- Readiness should fail before shutdown completes
- In-flight requests need a timeout
- Queue consumers should stop pulling new jobs
- Shutdown should be idempotent
- Logs should clearly show shutdown progress
A strong answer is:
I would handle SIGTERM and SIGINT, stop accepting new traffic, fail readiness, wait briefly for in-flight requests, close databases and queues, and force exit after a timeout so deployments do not hang.
How do you monitor Node.js in production?
What interviewers are testing: Whether you separate liveness from readiness and monitor runtime health signals beyond request counts.
Node.js monitoring should cover application health, performance, errors, and resource usage.
Important areas:
| Area | What to monitor |
|---|---|
| Health | Liveness and readiness endpoints |
| Traffic | Request rate, error rate, latency |
| Latency | p50, p95, p99 response times |
| Event loop | Event loop delay and utilization |
| Memory | Heap, RSS, external memory, GC behavior |
| CPU | CPU usage and saturation |
| Logs | Structured logs with request IDs |
| Traces | Distributed tracing across services |
| Dependencies | DB latency, Redis latency, queue depth |
Common tools:
- Prometheus + Grafana
- OpenTelemetry
- Datadog / New Relic / AppDynamics
- Pino or Winston for structured logs
perf_hooks.monitorEventLoopDelay()for event loop delay
Separate health checks clearly:
- Liveness: is the process alive?
- Readiness: is the app ready to serve traffic?
A common mistake is making /health return OK even when the database, queue, or critical dependency is unavailable. Readiness should fail only when the instance cannot meaningfully serve the traffic it is responsible for. Do not blindly make readiness depend on every downstream service—otherwise one shared dependency outage can remove every application replica simultaneously.
A strong answer is:
I would monitor request rate, errors, latency, event loop delay, memory, CPU, dependency latency, logs, and traces. I would also separate liveness from readiness so bad instances are removed from traffic before users are affected.
Where does caching fit in a Node API architecture?
What interviewers are testing: Whether you place caches at the right layer with TTL, invalidation, and stampede protection.
Caching reduces repeated work and improves latency, but it must be used carefully.
Common caching layers:
| Layer | Use case | Risk |
|---|---|---|
| In-process cache | Hot small data inside one instance | Inconsistent across replicas |
| Redis / Memcached | Shared cache across app instances | Extra network dependency |
| CDN | Static assets and cacheable GET responses | Stale public content |
| Browser cache | Client-side repeat requests | Harder invalidation |
| Database/query cache | Backend-specific caching of repeated reads | Staleness/invalidation and engine-specific behavior |
Common patterns:
- Cache-aside: app checks cache, falls back to DB, then updates cache
- TTL-based caching: data expires after a fixed time
- Stale-while-revalidate: serve old data briefly while refreshing in background
- Write-through: update cache when writing data
Senior topics to mention:
- Cache invalidation strategy
- TTL selection
- Cache stampede prevention
- Per-user vs public cache separation
- Avoid caching sensitive data incorrectly
- Metrics for hit rate and eviction rate
Example architecture:
Client → CDN → Node API → Redis cache → DatabaseA strong answer is:
I would use caching at the right layer: CDN for public GET responses, Redis for shared application cache, and small in-process LRU caches for hot local data. I would also define TTL, invalidation, and cache-stampede protection.
How do you manage database connections from Node?
What interviewers are testing: Whether you pool database connections and tune pool size against replica count and DB limits.
Node.js applications should use a database connection pool, not create a new database connection for every request.
A pool keeps a limited number of reusable connections open.
Bad pattern:
Every request opens a new DB connectionGood pattern:
App starts → create pool → requests borrow connection → query → release connectionWhy pooling matters:
- Opening connections is expensive
- Too many connections can overload the database
- Pooling improves reuse and latency
- Pool limits protect the DB during traffic spikes
Important tuning point:
total possible connections = pool size per app × number of app replicasFor example, if each pod has a pool of 20 and you run 10 pods, the database may receive up to 200 app connections.
Senior candidates should mention:
- Set pool max based on DB capacity
- Use query timeouts
- Handle pool exhaustion clearly
- Fail fast with an appropriate controlled response when the service cannot acquire DB capacity within its deadline
- Monitor slow queries and pool wait time
- Close pools during graceful shutdown
A strong answer is:
I would create one connection pool per app instance, reuse it across requests, tune pool size based on database limits and replica count, set timeouts, monitor pool exhaustion, and close the pool during graceful shutdown.
System design and architecture (senior)
Design a file upload service in Node.
What interviewers are testing: Whether you design uploads without buffering large files in Node memory.
A good file upload service should avoid buffering large files in Node.js memory.
High-level design:
Client → Auth/API → Object storage → Async processing → Metadata DBPreferred approach for large uploads:
- Client requests upload permission
- Node API validates user, file size, and content type
- API returns a pre-signed object storage URL
- Client uploads directly to S3/GCS/Azure Blob
- Storage event triggers async processing
- Worker scans file, extracts metadata, and updates DB
- Client receives completion status or webhook
If the file must pass through Node:
- Use streams
- Set multipart size limits
- Do not store entire file in memory
- Validate file type carefully
- Apply authentication and rate limits
- Store metadata in DB, not the file itself
For multipart upload handling in Node, see upload files in Node.js.
Production concerns:
- Virus scanning
- Content-type validation
- File size limits
- Retry-safe metadata updates
- Cleanup for abandoned uploads
- Access control for downloads
- Audit logs for sensitive files
A strong answer is:
I would prefer direct upload to object storage using a pre-signed URL, keep Node responsible for auth and metadata, process files asynchronously with workers, and avoid buffering large files in memory.
How would you build a reliable webhook delivery system?
What interviewers are testing: Whether you design webhook delivery for at-least-once behavior—durable event creation, asynchronous delivery, retries, idempotent receivers, signing, replay, and failure isolation.
A reliable webhook system should not send webhooks only from the request thread. It should persist events and deliver them asynchronously.
High-level design:
Application event → DB/outbox → Queue → Webhook workers → Subscriber endpointKey components:
- Store webhook events durably
- Push delivery jobs to a queue
- Workers send HTTP POST requests
- Retry with exponential backoff
- Move failed deliveries to a dead-letter queue
- Provide manual replay
- Sign payloads using HMAC
- Include event ID and idempotency key
Important headers:
Idempotency-Key: evt_123
X-Signature: hmac-sha256-signatureReliability concerns:
- Receiver may be down
- Receiver may process the same webhook twice
- Network may timeout after successful processing
- Delivery order may not always be guaranteed
- Retries can create duplicates
So receivers should process duplicate event IDs idempotently because retries can produce duplicate delivery.
Senior candidates should mention the outbox pattern. It prevents losing webhooks when the database write succeeds but the queue publish fails.
A strong answer is:
I would store webhook events durably, publish them to a queue, deliver them with workers, retry using exponential backoff, sign payloads, include idempotency keys, and use a dead-letter queue plus replay for failures.
When do microservices make sense for a Node team?
What interviewers are testing: Whether you justify service boundaries and operational cost before splitting a monolith.
Microservices make sense when the organization has clear service boundaries and real scaling or delivery pain.
Good reasons to split:
- Teams need independent deployment
- Domains are clearly separated
- Services have different scaling needs
- One part of the system changes much faster than others
- Fault isolation is important
- Different data ownership is required
Poor reasons to split:
- The team thinks microservices are automatically more modern
- The monolith is messy but boundaries are unclear
- The team lacks monitoring, tracing, and deployment maturity
- The system is small and still changing rapidly
Costs of microservices:
- Distributed tracing
- Network failures
- Contract testing
- More deployments
- More observability work
- More complex local development
- Data consistency challenges
Pragmatic senior approach:
Start with a modular monolith.
Split into services when boundaries are clear and operational pain is real.A strong answer is:
I would start with a modular monolith unless there is clear need for independent scaling, deployment, or ownership. Microservices help when boundaries are stable, but they add operational cost, distributed tracing, contract testing, and failure-handling complexity.
What testing strategy do senior Node developers use?
What interviewers are testing: Whether you balance unit, integration, contract, and E2E tests with error-path coverage.
Senior Node.js developers use a balanced testing strategy instead of testing only happy paths.
Typical test pyramid:
| Test type | What it checks | Tools |
|---|---|---|
| Unit tests | Pure functions and small services | Jest, Vitest, Node test runner |
| Integration tests | API + database + queues | Supertest, Testcontainers |
| Contract tests | Service-to-service expectations | Pact or schema tests |
| E2E tests | Critical user flows | Playwright, Cypress, API-level E2E |
What to test:
- Success paths
- Validation errors
- Auth and authorization failures
- Database errors
- External API failures
- Retry behavior
- Timeout behavior
- Idempotency
- Middleware behavior
Avoid mocking everything. If database behavior matters, use a real test database or containers. For external services, mock at the boundary.
For APIs, useful checks include:
- Status code
- Response body shape
- Error format
- Auth behavior
- Side effects in database or queue
A strong answer is:
I would use unit tests for business logic, integration tests for API and database behavior, contract tests for service boundaries, and limited E2E tests for critical flows. I would test error paths, auth failures, retries, and timeouts, not only happy paths.
Behavioral interview questions
Walk me through a Node.js project you designed or built.
What interviewers are testing: Whether you can explain architecture, your personal contribution, technical trade-offs, production challenges, and measurable impact rather than listing frameworks.
Present a Node.js project as an engineering story, not as a list of packages you used.
A good structure is:
- Problem — what business or technical problem you solved
- Users / scale — who used it, traffic, data size, or workload if available
- Architecture — API, database, cache, queue, workers, external services
- Your role — what you personally designed, built, fixed, or improved
- Trade-offs — why you chose Express/Fastify, SQL/NoSQL, Redis, queue, etc.
- Failure or challenge — outage, bug, performance issue, race condition, deployment problem
- Result — latency improved, errors reduced, cost saved, release unblocked, users helped
For a Node.js project, interviewers may ask:
- Why did you choose Node.js?
- How did you handle async errors?
- How did you manage database connections?
- Did you use caching? Why?
- How did you handle authentication?
- How did you monitor the service?
- What failed in production, and what did you change after that?
Avoid saying only:
I built APIs using Node.js, Express, MongoDB, and JWT.A better answer explains decisions:
I built an Express API for order processing. We used PostgreSQL for transactional data, Redis for short-lived cache, and a queue for async invoice generation. One issue we faced was slow response time during PDF generation, so we moved that work to background workers and added monitoring around queue delay and API latency.A strong answer is:
I explain the project by covering the problem, architecture, my role, trade-offs, production challenges, and measurable impact. I focus less on listing tools and more on why each technical decision was made.
Final-week Node.js interview checklist
In the final week, focus on revision, mock practice, and explaining concepts clearly. Do not try to learn every Node.js package at the last moment.
Final-week checklist:
- Explain what Node.js is and how it differs from the browser
- Draw event loop phases and explain microtasks
- Explain
process.nextTick(), Promise microtasks,setTimeout(), andsetImmediate() - Explain blocking vs non-blocking code with one production example
- Explain streams, Buffer, and backpressure
- Write an async Express route with centralized error handling
- Explain middleware order and error middleware
- Explain JWT access token, refresh token, expiry, and revocation
- Explain graceful shutdown with
SIGTERM,server.close(), and DB cleanup - Revise database pooling, caching, rate limiting, and monitoring
- Practice 10–15 coding problems on arrays, strings, objects, maps, and async flows
- Prepare 2 system design outlines: file upload service and webhook delivery system
- Prepare 3 STAR stories: production incident, performance improvement, and technical disagreement
Good STAR story format:
| Step | What to explain |
|---|---|
| Situation | What was happening? |
| Task | What were you responsible for? |
| Action | What did you personally do? |
| Result | What improved or what was learned? |
For the last two days, practice speaking answers aloud. Many candidates know Node.js but fail to explain clearly under interview pressure.
More prep:
Modern Node.js APIs
What is AbortController and when do you use it in Node.js?
What interviewers are testing: Whether you cancel in-flight work on timeout or disconnect using AbortSignal.
AbortController provides a standard way to cancel in-flight work when a request times out or a client disconnects.
Common uses:
- HTTP/
fetchtimeouts viasignal - Database or client cancellation where the driver supports
AbortSignal - Preventing abandoned work from continuing after the caller has moved on
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timeout);
}For simple timeouts in current Node versions, AbortSignal.timeout(ms) can avoid manually managing a timeout controller when the API supports it.
A strong answer is:
AbortController lets me cancel fetch and other signal-aware APIs when a request is no longer needed—I wire timeouts and client disconnects so work does not keep running in the background.
What is AsyncLocalStorage?
What interviewers are testing: Whether you carry request context across async boundaries without polluting global state.
AsyncLocalStorage (from node:async_hooks) stores per-async-context data—useful for request/correlation IDs, tracing context, or tenant metadata without passing a context object through every function.
import { AsyncLocalStorage } from 'node:async_hooks';
const requestStore = new AsyncLocalStorage();
function handler(req, res) {
requestStore.run({ requestId: req.id }, () => processRequest(req, res));
}
function processRequest(req, res) {
const { requestId } = requestStore.getStore();
logger.info({ requestId }, 'handling request');
}Treat the store as request context, not as general mutable global state, and be careful when integrating libraries that create unusual async boundaries.
A strong answer is:
AsyncLocalStorage carries request-scoped context across async calls—better than threading a correlation ID through every helper when building observability into Express or Fastify services.
How does Node's built-in test runner compare to Jest or Vitest?
What interviewers are testing: Whether you choose node:test vs Jest/Vitest based on capability requirements, not project size alone.
Modern Node includes node:test and node:assert for first-party unit and integration tests without extra test-framework dependencies. node:test is stable and capable—not merely a lightweight option for small services.
node:test |
Jest / Vitest | |
|---|---|---|
| Availability | Built into Node | External dependency |
| Core capability | Tests, hooks, mocking, reporters, coverage-related tooling depending on Node version | Rich framework/ecosystem integrations |
| Choose when | First-party Node tooling meets your requirements | Existing ecosystem/tooling or framework integrations justify the dependency |
import { test } from 'node:test';
import assert from 'node:assert/strict';
test('adds numbers', () => {
assert.equal(1 + 1, 2);
});A strong answer is:
I start with
node:testwhen its built-in capabilities meet the service's needs. I choose Jest or Vitest when the project already depends on their ecosystem, mocking model, browser tooling, or framework integrations.
What built-in Web APIs does modern Node provide?
What interviewers are testing: Whether you know modern built-in HTTP APIs and when Undici dispatchers vs external clients are warranted.
Modern Node ships browser-compatible APIs for many backend workflows:
| API | Use |
|---|---|
fetch |
HTTP client without node-fetch |
URL / URLSearchParams |
Parsing and building URLs |
AbortController |
Cancellation/timeouts |
| Web Streams | Interop with ReadableStream/WritableStream |
Native fetch() is backed by Undici and supports custom dispatchers for advanced connection management. External HTTP libraries may still be useful for higher-level retry policies, middleware/interceptors, specialized instrumentation, or team-standard abstractions.
A strong answer is:
I use native fetch, URL APIs, and AbortController for ordinary HTTP work. Because Node's fetch is backed by Undici, I can customize connection behavior with a dispatcher; I add another client when I need higher-level retry, interception, or framework-specific features.
Topic cheat sheet (junior → senior)
| Topic | Junior | Senior |
|---|---|---|
| Event loop | Phases, blocking | nextTick vs immediate, loop delay monitoring |
| Async | Promises, async/await | Unhandled rejection policy, queue patterns |
| Streams | Know types | Backpressure, transform pipelines |
| Express | Routes, middleware | Auth, rate limits, validation |
| Production | env vars | Graceful shutdown, clustering, observability |
Master the runtime model first—frameworks change faster than the core event-driven concepts.
Summary
Node.js interviews reward candidates who understand the runtime model—V8 executes JavaScript on one main thread while libuv handles the event loop, OS-backed network I/O, and a thread pool for selected APIs. You practiced scheduling (process.nextTick(), microtasks, timers, setImmediate()), blocking vs non-blocking I/O, streams and backpressure, and async error handling through Express.
Middle sections covered API design, JWT vs session trade-offs, security layering, caching placement, connection pooling, and senior topics such as cluster vs worker threads, graceful shutdown, and readiness that does not cascade on shared dependency failures.
Closing cards address modern Node—AbortController, AsyncLocalStorage, stable node:test, and Undici-backed fetch—plus behavioral project walkthroughs and system-design scenarios for uploads and webhooks. Pair this page with TypeScript interview questions and MongoDB interview questions when the role expects those stacks.

