Node.js Developer Interview Questions and Answers

Node.js developer interview questions for 2026: event loop, async patterns, streams, Express, JWT, clustering, worker threads, and senior Node.js technical prep with answers.

Published

Updated

Tech reviewed byDeepak Prasad

Node.js Developer Interview Questions and Answers

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.

NOTE
Senior bar in 2026: Expect event-loop depth, async error handling, security (helmet, rate limits), and at least one system design sketch (API + cache + queue). TypeScript and NestJS may appear — but runtime questions still come from core Node.

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:

  1. Recruiter or hiring manager screen — background, projects, stack, communication
  2. Online assessment — JavaScript, async behavior, small API or debugging task
  3. Technical deep dive — event loop, Promises, Express, database usage, error handling
  4. Live coding — usually 45–60 minutes in HackerRank, CoderPad, or a shared IDE
  5. 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:

  1. Explain the concept in simple words
  2. Write a small example
  3. Explain one common mistake
  4. 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:

  1. Request reaches Node.js
  2. JavaScript code runs on the main thread
  3. Async I/O is delegated
  4. Event loop receives completion callbacks
  5. 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:

javascript
const fs = require('node:fs');

module.exports = {
  readConfig
};

ES module example:

javascript
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
  • __dirname and __filename are 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 use import and export and 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:

javascript
// Bad inside a request handler
const data = fs.readFileSync('/large-file.json');

Example of non-blocking code:

javascript
// 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:

bash
npm install express
npm install
npm run test
npm run build

npx is used to run package binaries without manually installing them globally.

Common npx uses:

bash
npx eslint .
npx create-next-app my-app
npx prisma migrate dev

Simple 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.json for repeatable dependency resolution
  • Use npm ci in 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:

bash
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 ci in 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:

  1. Timers — callbacks from setTimeout() and setInterval()
  2. Pending callbacks — some deferred system-level callbacks
  3. Idle / prepare — internal Node/libuv work
  4. Poll — receives new I/O events and runs I/O callbacks
  5. Check — runs setImmediate() callbacks
  6. 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) and setImmediate() ordering can be context-dependent.
  • Inside an I/O callback, setImmediate() usually runs before setTimeout(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, and setImmediate() runs in the check phase. I would avoid overusing nextTick because 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()
  • await continuation
  • queueMicrotask()

When an await completes, the remaining part of the async function is scheduled as a microtask.

Example:

javascript
console.log('start');

Promise.resolve().then(() => console.log('promise'));

console.log('end');

Output:

text
start
end
promise

The 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() and await continuations 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() or JSON.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() and crypto.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:

bash
UV_THREADPOOL_SIZE=8 node server.js

A 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:

javascript
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:

text
nextTick
promise

After that, the order of timeout and immediate can be context-dependent:

text
timeout
immediate

or:

text
immediate
timeout

Why?

  • process.nextTick() runs before Promise microtasks in typical CommonJS execution
  • Promise callbacks run as microtasks
  • setTimeout(0) runs in the timers phase
  • setImmediate() 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 nextTick runs first, then Promise microtasks. The order between setTimeout(0) and setImmediate() can depend on context, but inside an I/O callback setImmediate() 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 errors
  • Promise.all() for parallel work
  • Promise.race() for timeout-like patterns

async/await makes Promise-based code look more like synchronous code.

javascript
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 await and use try/catch for 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.all is for all-or-nothing work, allSettled is for batch work with partial failure, race returns the first settled Promise, and any returns 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:

javascript
doWork(); // returns a Promise, but nobody awaits or catches it

Better:

javascript
try {
  await doWork();
} catch (err) {
  logger.error({ err }, 'doWork failed');
}

In production, use a clear policy:

  • Always await Promises 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:

javascript
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.

javascript
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:

javascript
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:

javascript
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

See Node.js error handling.

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:

javascript
const data = await fs.promises.readFile('large.log');

A stream can process chunks as they arrive:

javascript
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.

javascript
readable.on('data', (chunk) => {
  if (!writable.write(chunk)) {
    readable.pause();

    writable.once('drain', () => {
      readable.resume();
    });
  }
});

This means:

  • write() returned false → 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:

javascript
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() returning false tells me to pause reading until the drain event. .pipe() and pipeline() 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:

javascript
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 buffer
  • Buffer.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:

javascript
app.get('/report', (req, res) => {
  const data = fs.readFileSync('./report.json');
  res.send(data);
});

Better options:

javascript
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:

javascript
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:

javascript
(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:

text
request
logger
body parser
authentication
routes
404 handler
error handler

Error-handling middleware has four arguments:

javascript
(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, and next.

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:

json
{
  "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:

  1. User logs in with valid credentials
  2. Server issues a short-lived access token
  3. Client sends the access token with API requests
  4. Server verifies signature and claims
  5. 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, SameSite cookie
  • 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, and aud, 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:

text
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:

  1. Track memory over time using process.memoryUsage()
  2. Compare heapUsed, rss, and external memory
  3. Reproduce the leak under load
  4. Take heap snapshots using node --inspect and Chrome DevTools
  5. Compare snapshots before and after load
  6. Look for growing object counts or retained references
  7. 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:

  1. Listen for SIGTERM and SIGINT
  2. Stop accepting new HTTP connections with server.close()
  3. Mark readiness as failed so load balancers stop sending traffic
  4. Wait for in-flight requests to finish
  5. Stop background consumers and job workers
  6. Close database, Redis, queue, and telemetry connections
  7. Force exit after a timeout if shutdown hangs

Example:

javascript
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:

text
Client → CDN → Node API → Redis cache → Database

A 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:

text
Every request opens a new DB connection

Good pattern:

text
App starts → create pool → requests borrow connection → query → release connection

Why 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:

text
total possible connections = pool size per app × number of app replicas

For 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 503 or 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:

text
Client → Auth/API → Object storage → Async processing → Metadata DB

Preferred approach for large uploads:

  1. Client requests upload permission
  2. Node API validates user, file size, and content type
  3. API returns a pre-signed object storage URL
  4. Client uploads directly to S3/GCS/Azure Blob
  5. Storage event triggers async processing
  6. Worker scans file, extracts metadata, and updates DB
  7. 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:

text
Application event → DB/outbox → Queue → Webhook workers → Subscriber endpoint

Key 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:

text
Idempotency-Key: evt_123
X-Signature: hmac-sha256-signature

Reliability 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:

text
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:

  1. Problem — what business or technical problem you solved
  2. Users / scale — who used it, traffic, data size, or workload if available
  3. Architecture — API, database, cache, queue, workers, external services
  4. Your role — what you personally designed, built, fixed, or improved
  5. Trade-offs — why you chose Express/Fastify, SQL/NoSQL, Redis, queue, etc.
  6. Failure or challenge — outage, bug, performance issue, race condition, deployment problem
  7. 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:

text
I built APIs using Node.js, Express, MongoDB, and JWT.

A better answer explains decisions:

text
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(), and setImmediate()
  • 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.

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …