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 42 questions from fundamentals through senior topics—memory, graceful shutdown, monitoring, and architecture trade-offs. 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 do 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.
What is a realistic 4–5 week prep plan?
A realistic prep plan should cover Node.js fundamentals first, then production topics.
| Week | Focus |
|---|---|
| 1 | Node.js tutorial, runtime basics, modules, npm, simple HTTP server |
| 2 | Event loop, callbacks, 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.
Do you need TypeScript and NestJS for Node interviews 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?
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 |
| Global objects | window, document |
process, Buffer, global |
| 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 |
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?
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 such as:
- File system operations
- Some crypto operations
- DNS lookup
- 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?
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?
Node.js supports two module systems: CommonJS and ES modules.
| Feature | CommonJS | ES modules |
|---|---|---|
| Syntax | require() / module.exports |
import / export |
| Loading style | Mostly synchronous | Static imports, async-friendly |
| 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
__dirnameand__filenameare not available the same way in ES modules- 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?
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?
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 is used to run package binaries without manually installing them globally.
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
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?
The Node.js event loop decides when async callbacks run. It is provided by libuv and runs work in phases.
Common interview phases:
- Timers — callbacks from
setTimeout()andsetInterval() - Pending callbacks — some deferred system-level callbacks
- Idle / prepare — internal Node/libuv work
- Poll — receives new I/O events and runs I/O callbacks
- Check — runs
setImmediate()callbacks - Close callbacks — runs close events like
socket.on('close')
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.
Important modern note: since Node.js 20/libuv 1.45, timer behavior changed and timers run after the poll phase, which can affect edge cases involving setTimeout() and setImmediate().
A strong answer is:
The event loop runs callbacks in phases like timers, pending callbacks, poll, check, and close callbacks. I/O is mainly handled in the poll phase,
setImmediate()runs in the check phase, and microtasks such as Promises run between normal callbacks, so they can affect execution order.
Difference between process.nextTick(), setImmediate(), and setTimeout(fn, 0)?
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()runs before the event loop continues, Promise microtasks run shortly after,setTimeout(0)runs in the timers phase, andsetImmediate()runs in the check phase. I would avoid overusingnextTickbecause it can starve I/O.
How do Promises 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.
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?
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?
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 using
dns.lookup() - 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 for APIs like file system work, DNS lookup, crypto, and zlib. The default 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.
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?
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) {
try {
const user = await db.findUser(id);
if (!user) {
throw new Error('User not found');
}
return user;
} catch (err) {
logger.error({ err, id }, 'failed to load user');
throw err;
}
}The important interview point is not only readability. Promises and async/await also make it easier to centralize error handling and compose multiple async operations.
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?
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 - 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?
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');
}In production, use a clear policy:
- Always
awaitPromises or return them properly - Use
.catch()for intentionally fire-and-forget work - Wrap async route handlers in Express 4
- Use centralized error middleware
- Log unhandled rejections
- Treat unhandled rejections as serious application bugs
A global handler can log and trigger shutdown/alerting:
process.on('unhandledRejection', (reason, promise) => {
logger.error({ reason, promise }, 'unhandled promise rejection');
process.exit(1);
});In many production systems, the safer approach is to log the error, stop accepting new traffic, and restart the process using a supervisor or container platform.
A strong answer is:
I avoid unhandled rejections by always awaiting or catching Promises. At process level, I log unhandled rejections, alert, and usually restart safely because the application may be in an unknown state.
What is EventEmitter and when do you use it?
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.
Also remember to handle error events where applicable, because unhandled error events can crash 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?
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?
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?
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 code with multiple streams, prefer pipeline() because it also handles errors and cleanup better.
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..pipe()andpipeline()usually handle this flow control for me.
What is a Buffer and why does it matter?
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()can be faster but may contain old memory, so use it carefully
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?
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?
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?
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?
JWT authentication should be implemented carefully because a JWT is usually a bearer token: whoever has it can use it until it expires.
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 apps, avoid storing long-lived tokens in localStorage where XSS can steal them. A safer common pattern is:
- Short-lived access token
- Refresh token in
httpOnly,Secure,SameSitecookie - Refresh token rotation
- Revoke refresh token on logout
For Passport.js patterns see Passport authenticate.
A strong answer is:
I would use short-lived access tokens, validate the JWT signature and claims like
exp,iss, andaud, avoid sensitive payload data, and protect refresh tokens using httpOnly secure cookies with rotation and revocation.
What security measures should Node APIs use?
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 and sanitize request body, params, and query |
| 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 | Use lockfiles, npm audit, and dependency updates |
| 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?
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?
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?
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?
Graceful shutdown means the server stops accepting new work, finishes current work safely, closes dependencies, and exits cleanly.
Typical flow:
- Listen for
SIGTERMandSIGINT - Stop accepting new HTTP connections with
server.close() - Mark readiness as failed so load balancers stop sending traffic
- Wait for in-flight requests to finish
- Stop background consumers and job workers
- Close database, Redis, queue, and telemetry connections
- Force exit after a timeout if shutdown hangs
Example:
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
function shutdown() {
server.close(async () => {
await db.close();
await redis.quit();
process.exit(0);
});
setTimeout(() => {
process.exit(1);
}, 30000);
}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?
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. For production, readiness should reflect whether the app can actually serve requests.
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?
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 cache/indexes | Repeated query optimization | DB-specific limits |
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?
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
- Return
503or controlled errors during overload - 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.
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?
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 be told to treat webhook events as idempotent.
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?
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?
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 and final prep
How should you present a Node project in interviews?
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.
What is a final-week checklist for Node.js interviews?
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:
A strong answer is:
In the final week, I would revise core Node concepts, practice async coding, prepare one project walkthrough, rehearse system design basics, and prepare STAR stories for production incidents, performance work, and disagreements.
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 first — frameworks change; the event loop does not.

