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 40+ Node.js developer interview questions grouped from runtime fundamentals through senior architecture and production scenarios. Junior sections build core runtime knowledge; later sections assume you have shipped Node services before. For TypeScript interview questions (Express handlers, ESM, runtime validation), see TypeScript interview questions. For MongoDB interview questions (indexes, aggregation, Mongoose), see MongoDB interview questions. For HTTP API and full-stack depth, see full stack developer interview questions.

NOTE
Prep tip: For each technical question, read What interviewers are testing aloud, then practice the full answer. 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. Use A strong answer is as your 20-second closing line in the interview.

Interview context and how to prepare

What Node.js developer interviews actually test

Node.js interviews usually test whether you can build and debug real backend applications, not just recite syntax.

Level Typical focus
Junior JavaScript basics, Node core modules, callbacks, Promises, simple Express routes
Mid Event loop, async/await, middleware flow, database calls, error handling, testing
Senior Performance, scaling, worker threads, auth, observability, API design, production debugging

Live coding is often based on:

  • Array/object manipulation in JavaScript or TypeScript
  • Fixing async bugs
  • Writing or debugging an API endpoint
  • Explaining why a Node.js service became slow

The best preparation is not memorizing 100 answers. You should be able to explain how Node handles I/O, why blocking code is dangerous, and how you would debug a production issue.

What interview formats should you expect

A typical Node.js developer interview may include:

  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.

A realistic 4–5 week prep plan

Week Focus
1 Node runtime, modules, blocking vs non-blocking I/O
2 Event loop, Promises, async/await, error handling
3 Express routes, middleware, REST APIs, validation, HTTP requests
4 Streams, files, testing, database integration, authentication basics
5 Performance, worker threads, clustering, logging, monitoring, mock interviews

For each topic, prepare in this order:

  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.

TypeScript and NestJS in 2026

You do not always need TypeScript or NestJS, but many backend teams now prefer them for larger Node.js applications.

Still, interviewers usually expect you to understand Node.js runtime concepts first:

  • Event loop behavior
  • Blocking vs non-blocking code
  • Promise and async/await flow
  • Stream backpressure
  • Error handling
  • API design

If the job description mentions TypeScript, prepare:

  • Types vs interfaces
  • Generics
  • DTOs
  • Type narrowing
  • Error typing
  • Basic project structure

If the job description mentions NestJS, prepare:

  • Modules
  • Controllers
  • Providers
  • Dependency injection
  • Guards
  • Pipes
  • Interceptors

NestJS is built on top of Node.js concepts. If you understand Node, Express-style request handling, middleware flow, and dependency management, NestJS becomes easier to explain.


Node.js fundamentals

What is Node.js and how is it different from the browser?

What interviewers are testing: Whether you distinguish the JavaScript language from its host environment and know which runtime APIs Node provides that browsers do not.

Node.js is a JavaScript runtime used to run JavaScript outside the browser, commonly for servers, CLIs, APIs, build tools, and automation.

The browser and Node.js both run JavaScript, but they provide different APIs.

Area Browser Node.js
Main use User interface Server-side and tooling
Host globals window, document process, Buffer
APIs DOM, Web APIs, browser storage fs, http, crypto, stream, path
File access Restricted Available through Node modules
Modules ES modules, bundlers CommonJS and ES modules

Both environments provide the standard globalThis. Older Node.js code may also use the legacy global alias.

The key difference is the host environment. JavaScript is the language, but the browser and Node.js provide different capabilities around it. See Node.js global objects explained for process, Buffer, and other runtime globals.

A simple way to answer:

  • Browser JavaScript interacts with pages and users
  • Node.js JavaScript interacts with files, networks, processes, servers, and backend systems

A strong answer is:

Node.js is a JavaScript runtime for running JavaScript outside the browser. The language is the same, but the environment is different: browsers provide DOM APIs, while Node provides server-side APIs like file system, HTTP, streams, crypto, and process control.

What roles do V8 and libuv play?

What interviewers are testing: Whether you understand that V8 executes JavaScript while libuv provides the event loop and asynchronous I/O machinery—and that not all Node work happens on the JavaScript thread.

Node.js is built from multiple parts. Two important ones are V8 and libuv.

  • V8 executes JavaScript code
  • libuv provides the event loop, async I/O support, and a thread pool for some operations

A common interview mistake is saying “Node is single-threaded, so everything runs on one thread.” That is only partly correct.

Your JavaScript code usually runs on the main thread, but Node can use the operating system and libuv thread pool for background work. Network sockets are commonly handled through OS event notification rather than consuming a libuv worker thread; the thread pool is used by APIs such as filesystem operations, dns.lookup(), selected crypto work, and zlib.

Common thread-pool examples:

  • File system operations
  • Some crypto operations
  • dns.lookup() (not the same as dns.resolve*() APIs)
  • Compression

So the better explanation is:

JavaScript execution is single-threaded by default, but Node.js uses libuv and the operating system to handle asynchronous work efficiently.

Example interview flow:

  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?

What interviewers are testing: Whether you can explain why one JavaScript thread can still serve many concurrent I/O-bound requests and where CPU-bound work breaks that model.

Node.js is called single-threaded because your JavaScript application code normally runs on one main thread.

This means only one piece of JavaScript executes at a time in a single Node.js process.

However, Node.js can still handle many concurrent requests because it does not create one JavaScript thread per request. Instead, it uses:

  • Event loop
  • Non-blocking I/O
  • OS-level async operations
  • libuv thread pool for some background tasks

This is why Node.js works well for I/O-heavy applications such as APIs, chat apps, dashboards, and proxy services.

But CPU-heavy JavaScript is different. If one request performs a large calculation, image processing, huge JSON parsing, or a long loop, it can block the event loop and delay other requests.

For CPU-heavy work, use:

  • Worker threads
  • Child processes
  • Job queues
  • Separate services
  • Native/database-side processing where appropriate

See is Node.js single-threaded? for how the main thread, libuv, and the thread pool interact.

A strong answer is:

Node.js is single-threaded for JavaScript execution, not for every internal operation. It handles concurrency through the event loop and async I/O, but CPU-heavy JavaScript can block the main thread, so I would move that work to worker threads, another process, or a background queue.

CommonJS vs ES modules — when does each matter?

What interviewers are testing: Whether you understand CommonJS vs ESM syntax, interop pitfalls, and when each appears in real Node projects.

Node.js supports two module systems: CommonJS and ES modules.

Feature CommonJS ES modules
Syntax require() / module.exports import / export
Module model require() / module.exports; synchronous require() semantics Standard import / export module graph; supports top-level await
Common in Older Node.js projects Modern JavaScript and TypeScript projects
File indicators .cjs or default in many older projects .mjs or "type": "module"

CommonJS example:

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
  • CommonJS exposes __dirname and __filename; in modern Node.js ES modules, equivalent path information is available through import.meta.dirname and import.meta.filename on supported current releases, with import.meta.url remaining the standard module URL
  • Many TypeScript projects compile to either CommonJS or ESM depending on configuration

In real projects, the best choice depends on the existing codebase, tooling, Node.js version, and deployment environment.

A strong answer is:

CommonJS uses require() and is common in older Node.js projects. ES modules 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?

What interviewers are testing: Whether you recognize blocking calls on the main thread and how they hurt concurrent request handling in production.

Blocking code stops the main JavaScript thread until the operation finishes.

Non-blocking code starts an operation and lets Node.js continue handling other work while waiting for the result.

Example of blocking code:

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?

What interviewers are testing: Whether you distinguish dependency management from running package binaries and understand lockfile discipline in CI.

npm is the package manager for Node.js. It is used to install, update, remove, and manage dependencies.

Common npm uses:

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

npx runs package binaries—often from the local project or package cache—without requiring a separate global install.

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

npx first resolves suitable local or package-cache binaries and may fetch a package when it is not already available, so package identity and version still matter.

Example:

If eslint is installed in your project, this command can run the local binary:

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?

What interviewers are testing: Whether you understand enough event-loop ordering to diagnose scheduling and latency problems rather than merely memorizing phase names.

The Node.js event loop decides when async callbacks run. It is provided by libuv and runs work in phases.

The main libuv phases interviewers usually discuss are pending callbacks, poll, check, close callbacks, and timers, plus internal idle/prepare phases. The important part is what each queue represents rather than memorizing a permanently fixed circular order.

Phase Role
Pending callbacks Some deferred system-level callbacks
Poll Receives new I/O events and runs I/O callbacks
Check Runs setImmediate() callbacks
Close callbacks Runs close events like socket.on('close')
Timers Callbacks from setTimeout() and setInterval()

Microtasks are not a separate libuv phase. Node also drains:

  • process.nextTick() queue
  • Promise microtasks / queueMicrotask()

These run between normal callback execution points, so they can affect ordering.

Since libuv 1.45 / Node 20, timers are processed after poll rather than both before and after it, which changes some setImmediate()/setTimeout() edge cases.

A strong answer is:

I focus on what each phase queue represents—poll for I/O, check for setImmediate(), timers for setTimeout/setInterval—and how microtasks and nextTick interleave. Since Node 20, timer ordering relative to poll changed, so I reason about scheduling rather than memorizing one fixed mnemonic.

Difference between process.nextTick(), setImmediate(), and setTimeout(fn, 0)?

What interviewers are testing: Whether you know the scheduling priority of nextTick, microtasks, timers, and setImmediate—and the starvation risk of overusing nextTick.

These APIs schedule callbacks at different points.

API When it runs
process.nextTick() After current JavaScript finishes, before the event loop continues
queueMicrotask() / Promise jobs Microtask queue, after nextTick in typical CommonJS execution
setTimeout(fn, 0) Timers phase, after the timer threshold is reached
setImmediate() Check phase, usually after poll/I/O callbacks

process.nextTick() has very high priority. If you recursively schedule too many nextTick callbacks, you can starve I/O and timers.

For modern userland code, prefer queueMicrotask() unless you specifically need process.nextTick() behavior. Node.js now marks process.nextTick() as legacy.

Important interview nuance:

  • At top level, setTimeout(fn, 0) 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() drains before the event loop continues; Promise/queueMicrotask() jobs use the microtask queue. Timers and setImmediate() belong to later event-loop phases. I would avoid overusing nextTick because it can starve I/O.

How do Promises interact with the event loop?

What interviewers are testing: Whether you understand Promise and await scheduling as microtasks and how they interact with the event loop.

Promise callbacks do not run in the normal timer or I/O phases. They run in the microtask queue.

Examples of microtasks:

  • Promise.then()
  • Promise.catch()
  • Promise.finally()
  • 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:

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

Exact ordering relative to process.nextTick() can depend on the execution context, so interview answers should focus on the separate queues rather than memorizing one universal output.

A strong answer is:

Promises run their .then() 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?

What interviewers are testing: Whether you can identify event-loop blocking causes and mitigation in production APIs.

Blocking the event loop means one piece of JavaScript keeps the main thread busy, so Node.js cannot quickly process other requests, timers, or I/O callbacks.

Common symptoms:

  • Latency increases on many endpoints
  • Requests time out
  • Timers fire late
  • Health checks fail under load
  • CPU goes high but throughput does not improve

Common causes:

  • Synchronous file operations inside request handlers
  • Large JSON.parse() 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?

What interviewers are testing: Whether you know which async APIs use the libuv thread pool versus OS event notification and how pool saturation shows up under load.

Node.js uses the libuv thread pool when an async API cannot be handled directly by the operating system event mechanism.

Common examples:

  • File system APIs such as fs.readFile()
  • dns.lookup() specifically (uses OS name resolution and can use the libuv thread pool—not the same as dns.resolve*() APIs)
  • 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 by filesystem APIs, dns.lookup(), selected crypto operations, and zlib. Network sockets and APIs such as dns.resolve*() use different mechanisms, so I don't assume every asynchronous Node API consumes a worker-pool thread. The default pool size is 4, and increasing it can help some workloads, but I would profile first because a larger pool can also increase CPU and contention.

Predict output: setTimeout vs setImmediate vs Promise.

What interviewers are testing: Whether you reason about scheduling order without memorizing a single fixed output for all contexts.

Classic interview example:

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?

What interviewers are testing: Whether you explain how Promises and async/await improve composition and error routing over nested callbacks.

Callbacks work, but deeply nested callbacks become hard to read, test, and handle errors in. This is often called callback hell.

Promises improve this by making async work composable:

  • .then() for success
  • .catch() for 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) {
  const user = await db.findUser(id);

  if (!user) {
    throw new Error('User not found');
  }

  return user;
}

The important interview point is not only readability. Promises and async/await also make it easier to compose async operations and route errors to centralized handlers—catch locally only when adding useful context, recovering, translating the error, or performing cleanup.

See try-catch in Node.js.

A strong answer is:

Promises and async/await make asynchronous code easier to read, compose, and handle errors in. Instead of nesting callbacks, I can write sequential-looking code with await and use try/catch for errors.

Promise.all vs Promise.allSettled vs Promise.race?

What interviewers are testing: Whether you pick the right Promise combinator and understand that race does not cancel losing work.

These methods are used to coordinate multiple Promises.

Method Behavior Use when
Promise.all() Rejects as soon as one Promise rejects All results are required
Promise.allSettled() Waits for every Promise and returns success/failure status Partial failure is acceptable
Promise.race() Returns the first settled Promise, success or failure You need the first result or timeout pattern
Promise.any() Returns first fulfilled Promise; rejects only if all fail Any successful result is enough

Examples:

  • Use Promise.all() when loading user, orders, and permissions where all are required
  • Use Promise.allSettled() in batch jobs where some records may fail
  • Use Promise.race() to apply timeout behavior—but a timeout Promise in Promise.race() only settles the race; it does not automatically cancel the underlying I/O. Use AbortController or an API-specific cancellation mechanism when abandoned work should actually stop.
  • Use Promise.any() when calling multiple mirrors/providers and one success is enough

A strong answer is:

Promise.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?

What interviewers are testing: Whether you treat unhandled rejections as defects with bounded shutdown rather than resuming after unknown failures.

An unhandled rejection happens when a Promise rejects but no .catch() or try/catch handles it in time.

Bad example:

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

Better:

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

Treat an unhandled rejection as a serious programming defect. Modern Node's default --unhandled-rejections=throw behavior turns an unhandled rejection into an uncaught exception when no handler is installed.

In production:

  • Always await, return, or otherwise handle Promises
  • For intentional fire-and-forget work, attach .catch() and log or handle failure
  • Express 4 needs explicit async-error forwarding; Express 5 forwards rejected handler Promises to error middleware
  • Treat genuinely unhandled rejections as programming defects
  • Log and monitor unhandled rejections
  • Let a process supervisor restart the application after controlled shutdown—not process.exit() in the handler itself

process.exit() terminates synchronously and can truncate pending logs/output and skip asynchronous cleanup. If you implement controlled shutdown, stop accepting new work, perform bounded cleanup, set a non-zero exit status, and then terminate.

javascript
process.on('unhandledRejection', (reason, promise) => {
  logger.error({ reason, promise }, 'unhandled promise rejection');
  shutdown('unhandledRejection'); // stop accepting work; supervisor restarts
});

If you install unhandledRejection, do not use it to resume normal service after an unknown programming defect. Log the failure, initiate bounded shutdown, and rely on an external supervisor for restart.

A strong answer is:

I avoid unhandled rejections by always awaiting or catching Promises. At process level, I log them as defects, stop accepting new traffic, and let the supervisor restart—I do not treat immediate process.exit() as normal production handling.

What is EventEmitter and when do you use it?

What interviewers are testing: Whether you use in-process events appropriately and handle error listeners on EventEmitters that emit errors.

EventEmitter is Node.js's in-process publish/subscribe mechanism.

It allows one part of the application to emit an event and another part to listen for it.

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.

For EventEmitters that emit 'error', register appropriate error handling; an emitted 'error' event without a listener is thrown and normally terminates the process.

A strong answer is:

EventEmitter is Node’s in-process event system. I use it for decoupling components inside one process, but for communication across services I would use a message queue or broker.

How do you handle async errors in Express middleware?

What interviewers are testing: Whether you route async handler errors to centralized Express error middleware in Express 4 vs 5.

In Express, errors should reach centralized error-handling middleware.

Error middleware has four arguments:

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?

What interviewers are testing: Whether you know stream types and when chunk-based processing beats loading entire payloads into memory.

Streams let Node.js process data piece by piece instead of loading everything into memory at once.

The four main stream types are:

Stream type Meaning Example
Readable Source of data fs.createReadStream()
Writable Destination for data fs.createWriteStream()
Duplex Both readable and writable TCP socket
Transform Duplex stream that modifies data gzip, encryption, parser

Streams are useful for large files, uploads, downloads, logs, network data, and compression.

Instead of this:

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?

What interviewers are testing: Whether you know how Node prevents a fast producer from overwhelming a slower consumer and can connect write(), drain, pipe(), and pipeline().

Backpressure happens when data is being produced faster than it can be consumed.

Example:

  • A readable stream is reading a large file quickly
  • A writable stream is writing to a slow network or disk
  • If Node keeps reading without control, memory usage can grow

In Node.js, a writable stream signals backpressure when write() returns false.

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 stream composition, prefer stream.pipeline() / stream/promises.pipeline() because it propagates errors and coordinates teardown across the stream chain.

A strong answer is:

Backpressure means the destination cannot consume data as fast as the source produces it. In Node.js, write() returning false tells me to pause reading until the drain event. For production, I prefer pipeline() because it handles backpressure, errors, and cleanup across the chain.

What is a Buffer and why does it matter?

What interviewers are testing: Whether you understand binary data handling with Buffer and the cost of unnecessary conversions.

A Buffer is used to work with raw binary data in Node.js.

JavaScript strings are good for text, but many backend operations deal with bytes:

  • File content
  • TCP packets
  • Images
  • PDFs
  • Crypto data
  • Compressed data
  • Binary API payloads

Example:

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() skips zero-filling and must be completely overwritten before any data is read or exposed

In interviews, connect Buffer with streams: streams often deliver data as Buffer chunks.

A strong answer is:

A Buffer represents raw binary data in Node.js. It matters because files, sockets, crypto, images, and streams work with bytes, not just strings. I would avoid unnecessary conversion of huge buffers because it can increase memory usage and block the event loop.

Why is fs.readFileSync discouraged in API handlers?

What interviewers are testing: Whether you avoid synchronous file I/O on the request path and use async or streams instead.

fs.readFileSync() is discouraged in API handlers because it blocks the Node.js event loop.

While the file is being read:

  • No other JavaScript can run in that process
  • Other requests wait
  • Timers may fire late
  • p95/p99 latency can increase under load

Bad in request handlers:

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?

What interviewers are testing: Whether you understand middleware ordering, next(), and four-argument error middleware.

Express middleware is a chain of functions that run during the request-response cycle.

A middleware function usually has this signature:

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?

What interviewers are testing: Whether you design predictable REST APIs with correct methods, status codes, validation, and idempotency.

A well-designed REST API is predictable, consistent, and easy for clients to use.

Important qualities:

Area Good practice
Resource naming Use nouns: GET /users/:id, not /getUser
HTTP methods GET, POST, PUT, PATCH, DELETE correctly
Status codes 201 create, 204 no content, 400 validation, 401 auth, 404 missing, 409 conflict
Validation Validate input at the API boundary
Pagination Use cursor or page / limit for lists
Filtering/sorting Keep query parameters consistent
Errors Return a consistent error shape
Idempotency Make retry behavior clear for PUT, DELETE, and payment-like operations

Example error shape:

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?

What interviewers are testing: Whether you decide if JWT fits the architecture and design token storage, validation, and XSS/CSRF trade-offs together.

JWT authentication should be implemented carefully because a JWT is usually a bearer token: whoever has it can use it until it expires.

JWT is not automatically preferable to server-side sessions. Choose it when stateless or verifiable tokens fit the architecture.

A common pattern is:

  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 clients, avoid long-lived bearer tokens in JavaScript-accessible storage. HttpOnly cookies can reduce token theft through XSS, but cookie-based authentication requires appropriate CSRF defenses and SameSite policy.

For Passport.js patterns see Passport authenticate.

A strong answer is:

I first decide whether JWT is actually needed. If it is, I use short-lived access tokens, verify signature and allowed algorithm plus exp, iss, and aud, keep sensitive data out of the payload, and design refresh-token storage, rotation, revocation, XSS, and CSRF controls together.

What security measures should Node APIs use?

What interviewers are testing: Whether you layer API security controls beyond a single helmet middleware.

A production Node.js API should include security at multiple layers, not just one middleware.

Baseline checklist:

Area What to do
Headers Use helmet
Input Validate input and use context-appropriate escaping or parameterized queries
Auth Verify tokens, roles, and ownership
Rate limiting Protect login and public APIs
CORS Use explicit allowed origins
Secrets Use environment variables or a vault, never commit secrets
Dependencies Lockfiles, automated vulnerability/dependency scanning, timely updates, and supply-chain review (npm audit is one tool)
Transport Use HTTPS/TLS
Errors Do not leak stack traces to users
Logging Log security events without exposing secrets

Example CORS mistake:

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?

What interviewers are testing: Whether you choose GraphQL vs REST based on client flexibility, caching, and operational trade-offs.

Choose GraphQL when clients need flexible data fetching from a graph of related resources.

GraphQL is useful when:

  • Mobile/web clients need different fields
  • REST endpoints cause over-fetching or under-fetching
  • Data comes from multiple backend sources
  • The frontend team benefits from a strongly typed schema
  • You want clients to request exactly the shape of data they need

Choose REST when:

  • The API is simple and resource-oriented
  • HTTP caching/CDN behavior is important
  • You are building a public API
  • File downloads or static resources are involved
  • The team wants simpler tooling and operations

Trade-offs:

Area GraphQL REST
Data fetching Flexible client-selected fields Fixed endpoint responses
Caching More complex Easier with HTTP/CDN
Performance risk N+1 resolvers, complex queries Over-fetching or many round trips
Tooling Strong schema and typed clients Mature, simple HTTP tooling

Senior answer should mention GraphQL risks:

  • N+1 query problem
  • Query depth and complexity limits
  • Resolver-level authorization
  • Batching/caching with DataLoader
  • Monitoring slow fields and operations

A strong answer is:

I would choose GraphQL when clients need flexible, typed queries across related data. I would choose REST for simpler resource APIs, public APIs, file downloads, and CDN-friendly caching. With GraphQL, I would also plan for N+1 prevention, query limits, authorization, and monitoring.


Senior Node.js: scaling, reliability, and operations

Cluster module vs worker threads — when to use each?

What interviewers are testing: Whether you distinguish process-level traffic scaling from thread-based CPU offload and know when neither is needed because the platform already scales replicas.

Both cluster and worker_threads help Node.js use more CPU capacity, but they solve different problems.

Area cluster worker_threads
Execution model Multiple Node.js processes Multiple threads inside one process
Memory Separate memory per process Separate JS execution contexts; memory can be shared explicitly
Best for Scaling HTTP servers across CPU cores CPU-heavy JavaScript work
Failure isolation Better, because one worker process can crash separately Lower than separate processes
Communication IPC between processes Message passing between threads

Use cluster when you want multiple Node.js processes listening for traffic and sharing load across CPU cores.

Use worker threads when the event loop is blocked by CPU-heavy work such as:

  • Image processing
  • Large JSON transformation
  • PDF generation
  • Data compression
  • CPU-heavy calculations

Do not use worker threads just to make normal database or HTTP calls faster. Node's async I/O already handles that well.

In Kubernetes or container platforms, prefer horizontal scaling with multiple replicas for traffic scaling. cluster is less common when the platform already manages multiple app instances.

See Node.js multithreading with worker threads for CPU offload patterns.

A strong answer is:

I would use cluster to run multiple Node.js processes for serving traffic across CPU cores, and worker threads to offload CPU-heavy JavaScript from the main event loop. For large production traffic, I would still rely on horizontal scaling with multiple replicas.

What causes memory leaks in Node and how do you debug them?

What interviewers are testing: Whether you diagnose heap vs RSS growth and common retention patterns.

A memory leak happens when the application keeps references to objects that are no longer needed, so garbage collection cannot free them.

Common causes:

  • Global arrays or maps growing without limits
  • Caches without TTL or eviction
  • Forgotten setInterval() timers
  • Event listeners added repeatedly but never removed
  • Closures holding large objects
  • Request data stored accidentally in module-level variables
  • Large Buffers or external memory not being released

Debugging approach:

  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?

What interviewers are testing: Whether you drain traffic and close dependencies cleanly under orchestrator termination signals.

Graceful shutdown means the server stops accepting new work, finishes current work safely, closes dependencies, and exits cleanly.

Conceptual sequence:

  1. Receive termination signal (SIGTERM / SIGINT)
  2. Make shutdown idempotent
  3. Mark instance not ready / begin traffic draining
  4. Stop accepting new work (server.close() and stop consumers)
  5. Allow bounded in-flight completion
  6. Close dependencies, telemetry, and pools
  7. Let handles drain or force exit at deadline

Do not assume readiness changes instantly stop traffic—load-balancer and endpoint propagation can take time.

Example:

javascript
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

function shutdown() {
  const forceTimer = setTimeout(() => {
    logger.error('forced shutdown after timeout');
    process.exit(1);
  }, 30000);

  forceTimer.unref();

  server.close(async () => {
    try {
      await db.close();
      await redis.quit();
      clearTimeout(forceTimer);
      process.exitCode = 0;
    } catch (err) {
      logger.error({ err }, 'shutdown cleanup failed');
      process.exitCode = 1;
    }
  });
}

Prefer letting the event loop drain after closing handles rather than calling process.exit(0) on the normal path. Clear the force-exit timer after successful cleanup, and use unref() so the failsafe timer does not keep the process alive unnecessarily. Reserve process.exit() for timeout or fatal fallback when shutdown hangs.

In Kubernetes, this matters because pods normally receive SIGTERM before termination. If the app exits immediately, users may see failed requests during deployment.

Senior candidates should also mention:

  • Readiness should fail before shutdown completes
  • In-flight requests need a timeout
  • Queue consumers should stop pulling new jobs
  • Shutdown should be idempotent
  • Logs should clearly show shutdown progress

A strong answer is:

I would handle SIGTERM and SIGINT, stop accepting new traffic, fail readiness, wait briefly for in-flight requests, close databases and queues, and force exit after a timeout so deployments do not hang.

How do you monitor Node.js in production?

What interviewers are testing: Whether you separate liveness from readiness and monitor runtime health signals beyond request counts.

Node.js monitoring should cover application health, performance, errors, and resource usage.

Important areas:

Area What to monitor
Health Liveness and readiness endpoints
Traffic Request rate, error rate, latency
Latency p50, p95, p99 response times
Event loop Event loop delay and utilization
Memory Heap, RSS, external memory, GC behavior
CPU CPU usage and saturation
Logs Structured logs with request IDs
Traces Distributed tracing across services
Dependencies DB latency, Redis latency, queue depth

Common tools:

  • Prometheus + Grafana
  • OpenTelemetry
  • Datadog / New Relic / AppDynamics
  • Pino or Winston for structured logs
  • perf_hooks.monitorEventLoopDelay() for event loop delay

Separate health checks clearly:

  • Liveness: is the process alive?
  • Readiness: is the app ready to serve traffic?

A common mistake is making /health return OK even when the database, queue, or critical dependency is unavailable. Readiness should fail only when the instance cannot meaningfully serve the traffic it is responsible for. Do not blindly make readiness depend on every downstream service—otherwise one shared dependency outage can remove every application replica simultaneously.

A strong answer is:

I would monitor request rate, errors, latency, event loop delay, memory, CPU, dependency latency, logs, and traces. I would also separate liveness from readiness so bad instances are removed from traffic before users are affected.

Where does caching fit in a Node API architecture?

What interviewers are testing: Whether you place caches at the right layer with TTL, invalidation, and stampede protection.

Caching reduces repeated work and improves latency, but it must be used carefully.

Common caching layers:

Layer Use case Risk
In-process cache Hot small data inside one instance Inconsistent across replicas
Redis / Memcached Shared cache across app instances Extra network dependency
CDN Static assets and cacheable GET responses Stale public content
Browser cache Client-side repeat requests Harder invalidation
Database/query cache Backend-specific caching of repeated reads Staleness/invalidation and engine-specific behavior

Common patterns:

  • Cache-aside: app checks cache, falls back to DB, then updates cache
  • TTL-based caching: data expires after a fixed time
  • Stale-while-revalidate: serve old data briefly while refreshing in background
  • Write-through: update cache when writing data

Senior topics to mention:

  • Cache invalidation strategy
  • TTL selection
  • Cache stampede prevention
  • Per-user vs public cache separation
  • Avoid caching sensitive data incorrectly
  • Metrics for hit rate and eviction rate

Example architecture:

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?

What interviewers are testing: Whether you pool database connections and tune pool size against replica count and DB limits.

Node.js applications should use a database connection pool, not create a new database connection for every request.

A pool keeps a limited number of reusable connections open.

Bad pattern:

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
  • Fail fast with an appropriate controlled response when the service cannot acquire DB capacity within its deadline
  • Monitor slow queries and pool wait time
  • Close pools during graceful shutdown

A strong answer is:

I would create one connection pool per app instance, reuse it across requests, tune pool size based on database limits and replica count, set timeouts, monitor pool exhaustion, and close the pool during graceful shutdown.


System design and architecture (senior)

Design a file upload service in Node.

What interviewers are testing: Whether you design uploads without buffering large files in Node memory.

A good file upload service should avoid buffering large files in Node.js memory.

High-level design:

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?

What interviewers are testing: Whether you design webhook delivery for at-least-once behavior—durable event creation, asynchronous delivery, retries, idempotent receivers, signing, replay, and failure isolation.

A reliable webhook system should not send webhooks only from the request thread. It should persist events and deliver them asynchronously.

High-level design:

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 process duplicate event IDs idempotently because retries can produce duplicate delivery.

Senior candidates should mention the outbox pattern. It prevents losing webhooks when the database write succeeds but the queue publish fails.

A strong answer is:

I would store webhook events durably, publish them to a queue, deliver them with workers, retry using exponential backoff, sign payloads, include idempotency keys, and use a dead-letter queue plus replay for failures.

When do microservices make sense for a Node team?

What interviewers are testing: Whether you justify service boundaries and operational cost before splitting a monolith.

Microservices make sense when the organization has clear service boundaries and real scaling or delivery pain.

Good reasons to split:

  • Teams need independent deployment
  • Domains are clearly separated
  • Services have different scaling needs
  • One part of the system changes much faster than others
  • Fault isolation is important
  • Different data ownership is required

Poor reasons to split:

  • The team thinks microservices are automatically more modern
  • The monolith is messy but boundaries are unclear
  • The team lacks monitoring, tracing, and deployment maturity
  • The system is small and still changing rapidly

Costs of microservices:

  • Distributed tracing
  • Network failures
  • Contract testing
  • More deployments
  • More observability work
  • More complex local development
  • Data consistency challenges

Pragmatic senior approach:

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?

What interviewers are testing: Whether you balance unit, integration, contract, and E2E tests with error-path coverage.

Senior Node.js developers use a balanced testing strategy instead of testing only happy paths.

Typical test pyramid:

Test type What it checks Tools
Unit tests Pure functions and small services Jest, Vitest, Node test runner
Integration tests API + database + queues Supertest, Testcontainers
Contract tests Service-to-service expectations Pact or schema tests
E2E tests Critical user flows Playwright, Cypress, API-level E2E

What to test:

  • Success paths
  • Validation errors
  • Auth and authorization failures
  • Database errors
  • External API failures
  • Retry behavior
  • Timeout behavior
  • Idempotency
  • Middleware behavior

Avoid mocking everything. If database behavior matters, use a real test database or containers. For external services, mock at the boundary.

For APIs, useful checks include:

  • Status code
  • Response body shape
  • Error format
  • Auth behavior
  • Side effects in database or queue

A strong answer is:

I would use unit tests for business logic, integration tests for API and database behavior, contract tests for service boundaries, and limited E2E tests for critical flows. I would test error paths, auth failures, retries, and timeouts, not only happy paths.


Behavioral interview questions

Walk me through a Node.js project you designed or built.

What interviewers are testing: Whether you can explain architecture, your personal contribution, technical trade-offs, production challenges, and measurable impact rather than listing frameworks.

Present a Node.js project as an engineering story, not as a list of packages you used.

A good structure is:

  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.


Final-week Node.js interview checklist

In the final week, focus on revision, mock practice, and explaining concepts clearly. Do not try to learn every Node.js package at the last moment.

Final-week checklist:

  • Explain what Node.js is and how it differs from the browser
  • Draw event loop phases and explain microtasks
  • Explain process.nextTick(), Promise microtasks, setTimeout(), 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:


Modern Node.js APIs

What is AbortController and when do you use it in Node.js?

What interviewers are testing: Whether you cancel in-flight work on timeout or disconnect using AbortSignal.

AbortController provides a standard way to cancel in-flight work when a request times out or a client disconnects.

Common uses:

  • HTTP/fetch timeouts via signal
  • Database or client cancellation where the driver supports AbortSignal
  • Preventing abandoned work from continuing after the caller has moved on
javascript
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

try {
  const res = await fetch(url, { signal: controller.signal });
} finally {
  clearTimeout(timeout);
}

For simple timeouts in current Node versions, AbortSignal.timeout(ms) can avoid manually managing a timeout controller when the API supports it.

A strong answer is:

AbortController lets me cancel fetch and other signal-aware APIs when a request is no longer needed—I wire timeouts and client disconnects so work does not keep running in the background.

What is AsyncLocalStorage?

What interviewers are testing: Whether you carry request context across async boundaries without polluting global state.

AsyncLocalStorage (from node:async_hooks) stores per-async-context data—useful for request/correlation IDs, tracing context, or tenant metadata without passing a context object through every function.

javascript
import { AsyncLocalStorage } from 'node:async_hooks';

const requestStore = new AsyncLocalStorage();

function handler(req, res) {
  requestStore.run({ requestId: req.id }, () => processRequest(req, res));
}

function processRequest(req, res) {
  const { requestId } = requestStore.getStore();
  logger.info({ requestId }, 'handling request');
}

Treat the store as request context, not as general mutable global state, and be careful when integrating libraries that create unusual async boundaries.

A strong answer is:

AsyncLocalStorage carries request-scoped context across async calls—better than threading a correlation ID through every helper when building observability into Express or Fastify services.

How does Node's built-in test runner compare to Jest or Vitest?

What interviewers are testing: Whether you choose node:test vs Jest/Vitest based on capability requirements, not project size alone.

Modern Node includes node:test and node:assert for first-party unit and integration tests without extra test-framework dependencies. node:test is stable and capable—not merely a lightweight option for small services.

node:test Jest / Vitest
Availability Built into Node External dependency
Core capability Tests, hooks, mocking, reporters, coverage-related tooling depending on Node version Rich framework/ecosystem integrations
Choose when First-party Node tooling meets your requirements Existing ecosystem/tooling or framework integrations justify the dependency
javascript
import { test } from 'node:test';
import assert from 'node:assert/strict';

test('adds numbers', () => {
  assert.equal(1 + 1, 2);
});

A strong answer is:

I start with node:test when its built-in capabilities meet the service's needs. I choose Jest or Vitest when the project already depends on their ecosystem, mocking model, browser tooling, or framework integrations.

What built-in Web APIs does modern Node provide?

What interviewers are testing: Whether you know modern built-in HTTP APIs and when Undici dispatchers vs external clients are warranted.

Modern Node ships browser-compatible APIs for many backend workflows:

API Use
fetch HTTP client without node-fetch
URL / URLSearchParams Parsing and building URLs
AbortController Cancellation/timeouts
Web Streams Interop with ReadableStream/WritableStream

Native fetch() is backed by Undici and supports custom dispatchers for advanced connection management. External HTTP libraries may still be useful for higher-level retry policies, middleware/interceptors, specialized instrumentation, or team-standard abstractions.

A strong answer is:

I use native fetch, URL APIs, and AbortController for ordinary HTTP work. Because Node's fetch is backed by Undici, I can customize connection behavior with a dispatcher; I add another client when I need higher-level retry, interception, or framework-specific features.


Topic cheat sheet (junior → senior)

Topic Junior Senior
Event loop Phases, blocking nextTick vs immediate, loop delay monitoring
Async Promises, async/await Unhandled rejection policy, queue patterns
Streams Know types Backpressure, transform pipelines
Express Routes, middleware Auth, rate limits, validation
Production env vars Graceful shutdown, clustering, observability

Master the runtime model first—frameworks change faster than the core event-driven concepts.


Summary

Node.js interviews reward candidates who understand the runtime model—V8 executes JavaScript on one main thread while libuv handles the event loop, OS-backed network I/O, and a thread pool for selected APIs. You practiced scheduling (process.nextTick(), microtasks, timers, setImmediate()), blocking vs non-blocking I/O, streams and backpressure, and async error handling through Express.

Middle sections covered API design, JWT vs session trade-offs, security layering, caching placement, connection pooling, and senior topics such as cluster vs worker threads, graceful shutdown, and readiness that does not cascade on shared dependency failures.

Closing cards address modern NodeAbortController, AsyncLocalStorage, stable node:test, and Undici-backed fetch—plus behavioral project walkthroughs and system-design scenarios for uploads and webhooks. Pair this page with TypeScript interview questions and MongoDB interview questions when the role expects those stacks.


References

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 experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)