AceDevHub

Interview questions

100 Node.js Interview Questions and Answers (2026)

100 Node.js interview questions with direct answers — event loop, libuv, modules, streams, Express, and production patterns in strict learning order.

100 questionsBeginnerIntermediateAdvancedCode OutputScenario
1

Beginner Node.js Interview Questions

Question 1BeginnerConcept

What is Node.js?

Direct answer

Node.js is a JavaScript runtime built on Chrome's V8 engine — it runs JavaScript outside the browser on servers, CLIs, and tools, using a single-threaded event loop with non-blocking I/O for concurrent I/O-heavy workloads.

Node.js is not a programming language and not a web framework — it is a runtime environment that executes JavaScript with APIs for files, networking, processes, and cryptography. Ryan Dahl released it in 2009; the OpenJS Foundation maintains it today.

Why interviewers ask this

They want to hear runtime vs framework vs library — and why companies (Netflix, LinkedIn, Uber) choose Node for I/O-bound APIs, real-time apps, and tooling — not CPU-heavy batch jobs without mitigation.

  • V8 — compiles JavaScript to native machine code; same engine as Chrome.
  • libuv — C library providing the event loop, thread pool, and async I/O (Q8).
  • npm ecosystem — largest package registry; Express, Fastify, NestJS sit on top of Node.
  • One language full-stack — share types and validation between React frontend and Node API (AceDevHub stack).
hello-server.mjs
import http from "node:http";

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello from Node.js");
});

server.listen(4000, () => {
  console.log("API listening on http://localhost:4000");
});
Question 2BeginnerConcept

How does Node.js work?

Direct answer

Node.js receives JavaScript on the main thread, V8 compiles and runs it, async I/O delegates to libuv (OS or thread pool), and completed work returns via callbacks queued on the event loop — so the main thread stays free while I/O runs in the background.

Picture a request lifecycle: your route handler runs synchronously until it hits async I/O (read file, DB query, HTTP call). Node registers the operation with libuv, continues other work, then runs your callback when data is ready.

  1. Script entry — node app.js; V8 parses and executes top-level code.
  2. Sync work — runs immediately on the call stack until empty.
  3. Async I/O — fs.readFile, net.connect hand off to libuv; callback queued when done.
  4. Event loop — picks callbacks from phase queues when the stack is clear (Q4–5).
  5. Response — handler sends res.json(); loop waits for next request.
request-flow.js
import fs from "node:fs";

console.log("1: start");

fs.readFile("package.json", "utf8", (err, data) => {
  console.log("3: file read", data.length, "bytes");
});

console.log("2: end — main thread free while libuv reads disk");
Output
1: start
2: end — main thread free while libuv reads disk
3: file read 842 bytes
Question 3BeginnerConcept

Why is Node.js single-threaded?

Direct answer

JavaScript on the main thread is single-threaded by design — one call stack avoids locks and race conditions in user code; Node achieves concurrency via non-blocking I/O and libuv's thread pool, not one thread per request.

"Single-threaded" refers to your JavaScript, not the entire Node process — libuv uses a small worker thread pool (default 4) for file compression, crypto, and some DNS work behind the scenes.

Main JS threadlibuv thread pool
RunsYour callbacks, Express handlersSelected blocking OS tasks
CountOne for JavaScriptDefault 4 (UV_THREADPOOL_SIZE)
You manageAvoid long sync loopsRarely — automatic for fs/crypto
  • Why one JS thread — simpler mental model; no mutexes in typical app code; great for thousands of idle connections.
  • Cost of one thread — CPU-heavy JSON parse or bcrypt in a route blocks all clients until done (Q74).
  • Scaling CPUs — cluster module (one process per core) or worker_threads for parallel JS (Q68–69).
blocking-trap.js
// BAD — blocks entire server for ~2 seconds
app.get("/hash", (req, res) => {
  const start = Date.now();
  while (Date.now() - start < 2000) {} // sync CPU loop
  res.send("done");
});

// GOOD — offload or use async crypto
import { hash } from "node:crypto";
hash("sha256", data, () => res.send("hashed")); // uses thread pool
Question 4BeginnerConcept

What is the Node.js event loop?

Direct answer

The event loop is libuv's mechanism that runs phases (timers, poll, check, etc.) and executes queued callbacks when the JavaScript call stack is empty — enabling non-blocking I/O on a single main thread.

When V8 finishes synchronous code, the loop checks whether timers fired, I/O completed, or setImmediate callbacks are pending — then runs the associated functions. It repeats for the process lifetime.

Relationship to the browser

Both browser and Node have event loops, but Node's loop comes from libuv with different phases and APIs (fs, net, process). Browser loop is tied to rendering; Node has no DOM.

  • Call stack first — synchronous code always runs to completion before any queued callback.
  • Microtasks — process.nextTick and resolved Promises run between phases (Q6).
  • Starvation — infinite nextTick recursion prevents I/O callbacks — production outage pattern.
event-loop-demo.js
console.log("sync start");

setTimeout(() => console.log("timeout"), 0);

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

process.nextTick(() => console.log("nextTick"));

console.log("sync end");
Output
sync start
sync end
nextTick
promise microtask
timeout
Question 5BeginnerConcept

What are the Node.js event loop phases?

Direct answer

libuv's loop runs timers → pending callbacks → idle/prepare → poll → check → close callbacks — with process.nextTick and Promise microtasks draining between every phase transition.

Each phase has a FIFO queue of callbacks. Node runs all callbacks in the current phase (or until a limit), then drains microtasks, then moves on — order matters for setTimeout vs setImmediate (Q7).

PhaseRuns
timerssetTimeout / setInterval callbacks whose threshold expired
pending callbacksDeferred I/O callbacks (e.g. some TCP errors)
idle, prepareInternal libuv housekeeping — ignore in apps
pollFetch new I/O events; execute I/O callbacks; may block waiting
checksetImmediate callbacks
close callbackse.g. socket.on('close') handlers
  1. Between phases — process.nextTick queue drains completely, then Promise microtasks, then next phase.
  2. Poll blocking — if no timers and no setImmediate, poll waits for I/O — efficient for servers.
  3. Node 11+ — timers phase uses libuv timer refactor; behavior stable for interviews but mention version if asked.
phases-demo.js
import fs from "node:fs";

setTimeout(() => console.log("timers phase"), 0);

setImmediate(() => console.log("check phase"));

fs.readFile(__filename, () => {
  console.log("poll phase — I/O callback");
  setTimeout(() => console.log("timer inside I/O cb"), 0);
  setImmediate(() => console.log("immediate inside I/O cb"));
});
Question 6BeginnerComparison

What is the difference between process.nextTick and setImmediate?

Direct answer

process.nextTick runs before the next event loop phase continues — highest priority, can starve I/O; setImmediate runs in the check phase after poll, designed to defer work until after current I/O callbacks finish.

Both schedule callbacks asynchronously, but they sit in different queues with different priority. nextTick is not part of libuv phases — Node drains the entire nextTick queue between every phase transition.

process.nextTicksetImmediate
QueuenextTick queue (Node-specific)check phase of event loop
PriorityHigher — before Promises and next phaseAfter poll phase
Use forDefer error propagation, run after sync assignSplit long sync work across turns
RiskRecursive nextTick starves I/OSafer for yielding; still use workers for CPU
nexttick-vs-immediate.js
console.log("start");

setImmediate(() => console.log("setImmediate"));

process.nextTick(() => console.log("nextTick 1"));

process.nextTick(() => console.log("nextTick 2"));

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

console.log("end");
Output
start
end
nextTick 1
nextTick 2
promise
setImmediate
  • nextTick use case — emit 'error' on next tick so all 'error' listeners attach first (EventEmitter pattern).
  • setImmediate use case — break up sync CPU without blocking poll as aggressively as nextTick chains.
  • queueMicrotask — standard microtask API; runs after nextTick, before macrotasks.
Question 7BeginnerComparison

What is the difference between setImmediate and setTimeout in Node.js?

Direct answer

setImmediate runs in the check phase right after poll; setTimeout(0) runs in the timers phase — order depends on context: outside I/O timers often run first; inside an I/O callback setImmediate usually runs before setTimeout(0).

Why interviewers ask this

This tests whether you understand phase order, not just API names. The answer changes based on whether code runs at top level or inside an I/O callback.

immediate-vs-timeout-top.js
// Top-level — order can vary by runtime load; often timeout first
setTimeout(() => console.log("timeout 0"), 0);
setImmediate(() => console.log("immediate"));
immediate-vs-timeout-io.js
import fs from "node:fs";

fs.readFile(__filename, () => {
  // Inside I/O callback — check phase comes before next timers phase
  setTimeout(() => console.log("timeout inside I/O"), 0);
  setImmediate(() => console.log("immediate inside I/O"));
});
Output
immediate inside I/O
timeout inside I/O
  • setTimeout(0) — minimum delay 1ms in Node (not truly zero); scheduled in timers phase.
  • setImmediate — Node-only; not in browser; preferred to defer after current poll work.
  • Production code — rarely rely on ordering; use explicit Promises or queues for correctness.
Question 8BeginnerConcept

What is libuv in Node.js?

Direct answer

libuv is the C library Node uses for the event loop, async I/O, thread pool, timers, and cross-platform abstractions over epoll/kqueue/IOCP — V8 runs JavaScript; libuv handles waiting on disk and network.

Node is not built directly on OS APIs in JavaScript — libuv provides a unified layer so the same Node code runs on Linux, macOS, and Windows with consistent async behavior.

  • Event loop — phase scheduling (timers, poll, check) lives in libuv.
  • Thread pool — worker threads for operations that cannot be fully async at OS level (Q9).
  • Network & fs — TCP, UDP, pipes, async file operations bridge through libuv.
  • Cross-platform — epoll (Linux), kqueue (macOS), IOCP (Windows) hidden behind one API.
libuv-io.js
import dns from "node:dns";
import fs from "node:fs";

// Network I/O — often true async via OS (no thread pool)
fs.readFile("package.json", () => console.log("fs done"));

dns.lookup("nodejs.org", () => console.log("dns done"));

console.log("JS continues — libuv handles waits");
Question 9BeginnerConcept

What is the Node.js thread pool?

Direct answer

libuv maintains a fixed-size thread pool (default 4 threads, UV_THREADPOOL_SIZE) for work that cannot use fully non-blocking OS APIs — including fs.* on some systems, crypto, compression, and dns.lookup.

The main JavaScript thread stays free, but pool threads do blocking work on Node's behalf. If all four are busy, fifth crypto.hash waits in queue — latency spikes under load.

Often uses thread poolUsually true async (no pool)
fs.readFile / write on some platformsTCP net.Server accept/read
crypto.pbkdf2, scrypt, randomBytes (some paths)net.connect on modern OS
dns.lookup (not dns.resolve)fs.read on Linux io_uring paths (evolving)
zlib compressionMost timer and setImmediate scheduling
  • UV_THREADPOOL_SIZE — set env var before process start; max 1024; tune for crypto-heavy APIs.
  • dns.lookup vs dns.resolve — resolve uses network (no pool); lookup uses getaddrinfo (pool) — common perf interview detail.
  • Not unlimited parallelism — more concurrent bcrypt than pool size queues — use worker_threads for isolation (Q69).
thread-pool-crypto.js
import { pbkdf2 } from "node:crypto";

console.log("Starting 6 password hashes on 4 pool threads…");

for (let i = 0; i < 6; i++) {
  pbkdf2("password", "salt", 100000, 64, "sha512", () => {
    console.log(`hash ${i} done`);
  });
}

// First 4 run in parallel; 5th and 6th wait for a free thread
Question 10BeginnerComparison

What is the difference between blocking and non-blocking I/O in Node.js?

Direct answer

Blocking I/O pauses the caller until the operation finishes — sync fs.readFileSync freezes the main thread; non-blocking I/O starts the operation and returns immediately, invoking a callback or Promise when libuv signals completion.

Node's design goal is non-blocking by default for server throughput — one thread serves many connections while waiting on DB or disk. Sync APIs exist for scripts and startup, not hot request paths.

Blocking (sync)Non-blocking (async)
Examplefs.readFileSyncfs.promises.readFile / readFile cb
Main threadStopped until I/O completesRuns other callbacks while waiting
Use whenCLI tools, small config at bootServers, any concurrent workload
Risk under loadEntire API unresponsiveStarvation only if CPU work blocks thread
blocking-vs-async.js
import fs from "node:fs";

// BLOCKING — no other request handled during read
app.get("/bad", (req, res) => {
  const data = fs.readFileSync("large.json", "utf8");
  res.send(data);
});

// NON-BLOCKING — event loop serves other routes while disk reads
app.get("/good", (req, res) => {
  fs.readFile("large.json", "utf8", (err, data) => {
    if (err) return res.status(500).end();
    res.send(data);
  });
});
  • Async ≠ parallel — non-blocking I/O is concurrent scheduling, not multi-core JS execution.
  • Hidden blocking — JSON.parse on 50MB body, regex catastrophic backtracking — CPU blocks like sync I/O.
  • AceDevHub API — Fastify handlers use async/await + pg pool; never readFileSync per request on interview content routes.
Question 11BeginnerComparison

What is the difference between Node.js and browser JavaScript?

Direct answer

Same language (ECMAScript), different runtimes — browsers add DOM, window, and layout; Node adds fs, process, http, and libuv-backed I/O with no document or rendering APIs.

JavaScript syntax you learn for React applies on the server, but globals and APIs differ. Code using document or localStorage fails in Node; code using fs or process fails in the browser unless bundled with polyfills.

BrowserNode.js
Global objectwindowglobalThis (global in CommonJS)
UI / DOMdocument, alert, canvasNot available
I/Ofetch, IndexedDB (sandboxed)fs, net, child_process, pg client
Module systemsES modules + bundlersCommonJS + native ESM
Event loopRendering + taskslibuv phases, no paint
  • Shared core — Promises, async/await, classes, Map/Set, optional chaining work in both.
  • Isomorphic code — Zod schemas in packages/shared validate on web and API without DOM imports.
  • Security model — browser sandboxes tabs; Node server has full filesystem and env access — validate inputs.
runtime-check.js
// Works in both
const total = [1, 2, 3].reduce((a, b) => a + b, 0);

// Browser only
if (typeof document !== "undefined") {
  document.title = "AceDevHub";
}

// Node only
if (typeof process !== "undefined" && process.versions?.node) {
  console.log("Node", process.version);
}
Question 12BeginnerConcept

What is the V8 engine in Node.js?

Direct answer

V8 is Google's open-source JavaScript engine that compiles JS to native machine code — Node embeds V8 to execute your scripts; Node version bumps often ship a newer V8 with language features and performance fixes.

Why interviewers ask this

They check whether you know Node is V8 plus native bindings, not a custom interpreter — and whether you can link Node version to supported ECMAScript features.

  • JIT compilation — Ignition interpreter + TurboFan optimizer hot-path code for speed.
  • Garbage collection — generational GC frees unused objects; long-lived caches cause heap growth (Q72).
  • Node ↔ V8 mapping — node -p process.versions shows v8 string; LTS tracks stable V8 branch.
v8-versions.js
console.log({
  node: process.version,
  v8: process.versions.v8,
  modules: process.versions.modules,
});

// Feature availability follows V8 in your Node version
// e.g. native ESM, top-level await, structuredClone
Question 13BeginnerConcept

What is the Node.js architecture?

Direct answer

Node layers JavaScript user code on Node bindings (C++), libuv for the event loop and async I/O, and V8 for execution — with optional worker threads and child processes for parallelism outside the main loop.

A complete mental model stacks four pieces: your JS → Node core modules (written in JS + C++) → libuv → operating system kernel.

  1. Application layer — Express/Fastify routes, your business logic, npm packages.
  2. Node API surface — fs, http, crypto, stream wrappers exposing libuv to JavaScript.
  3. libuv — event loop, thread pool, cross-platform polling (Q8–9).
  4. V8 — executes JS and calls into bindings when you invoke node:fs.readFile.
architecture-flow.js
import http from "node:http";
import fs from "node:fs";

// JS handler
const server = http.createServer((req, res) => {
  // Binding → libuv async read → callback when ready
  fs.readFile("./data.json", (err, buf) => {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(buf);
  });
});

server.listen(4000);
Question 14BeginnerPractical

How do you install and manage Node.js versions?

Direct answer

Use nvm (Node Version Manager) or fnm to install multiple Node versions per project — pin with .nvmrc, prefer Active LTS for production, and match CI/Docker to local version.

Teams standardize on LTS releases for stability — AceDevHub API targets a single Node LTS in Docker and documents it in package.json engines field.

  • nvm — nvm install --lts; nvm use; .nvmrc file auto-switches in project root.
  • fnm — fast Rust alternative; popular on macOS/Linux CI images.
  • Official installer — nodejs.org MSI/pkg fine for beginners; harder to juggle multiple versions.
  • engines field — "engines": { "node": ">=20" } warns when team runs wrong version.
nvm-setup.sh
# Install LTS and set default
nvm install --lts
nvm alias default 22

# Project pin (.nvmrc contains "22")
cd ace-devhub-api
nvm use

node -v   # v22.x.x
npm -v
Question 15BeginnerConcept

What is npm and how does it work with Node.js?

Direct answer

npm (Node Package Manager) installs JavaScript packages into node_modules, records semver ranges in package.json, and runs scripts — it ships with Node and powers the largest open-source registry for server and tooling dependencies.

npm is both a CLI tool and a registry. Monorepos like AceDevHub use npm workspaces so apps/api and apps/web share lockfile integrity from the repo root.

CommandPurpose
npm installInstall deps from package-lock.json
npm install fastifyAdd runtime dependency
npm install -D vitestAdd devDependency
npm run devExecute scripts.dev from package.json
npm ciClean install for CI — fails if lock out of sync
  • node_modules — nested dependency tree; lockfile pins exact versions for reproducible builds.
  • semver — ^1.2.3 allows minor/patch; exact pins for critical prod (Q41).
  • Alternatives — pnpm (content-addressable), yarn — same registry, different install strategy.
package.json-snippet.json
{
  "name": "@acedevhub/api",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "content:import:interviews": "tsx src/scripts/import-content-interviews.ts"
  },
  "dependencies": {
    "fastify": "^5.0.0",
    "pg": "^8.13.0"
  },
  "devDependencies": {
    "tsx": "^4.19.0"
  }
}
Question 16BeginnerConcept

What is package.json in Node.js?

Direct answer

package.json is the manifest for a Node project — it declares name, version, entry point (main/module), scripts, dependencies, and metadata npm and tooling use to install, run, and publish the package.

Every Node app and publishable library has a package.json at the project root. npm reads it before creating node_modules; Node can resolve "type": "module" to decide ESM vs CommonJS (Q37).

FieldPurpose
name / versionPackage identity; semver for publishes
mainCommonJS entry (require)
module / exportsESM entry and conditional exports
scriptsNamed commands: npm run dev
dependenciesRuntime packages
devDependenciesBuild/test tools only
enginesSupported Node/npm versions
type"module" enables native ESM in .js files
  • private: true — prevents accidental npm publish of internal apps like @acedevhub/api.
  • workspaces — monorepo array ["apps/*", "packages/*"] links local packages.
  • Lockfile pair — package-lock.json pins exact tree; commit both together.
package.json
{
  "name": "@acedevhub/api",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "start": "node dist/index.js"
  },
  "engines": { "node": ">=20" },
  "dependencies": { "fastify": "^5.0.0" }
}
Question 17BeginnerConcept

What is the Node.js REPL?

Direct answer

The REPL (Read-Eval-Print Loop) is an interactive Node shell — type node with no file to experiment with JavaScript, inspect variables with _, and test APIs before adding them to your project.

The REPL is the fastest way to probe Node APIs — crypto output, Buffer encoding, or regex — without creating a temp file. It uses the same V8 engine as running node app.js.

  • Start — node in terminal; .exit or Ctrl+D twice to quit.
  • Special _ variable — holds result of last expression; _ + Enter repeats.
  • Dot commands — .help, .save script.js, .load script.js, .clear context.
  • await in REPL — top-level await supported in modern Node REPL for quick fetch tests.
repl-session.txt
> 2 + 2
4
> const fs = await import('node:fs/promises')
> const pkg = JSON.parse(await fs.readFile('package.json', 'utf8'))
> pkg.name
'@acedevhub/api'
> .exit
Question 18BeginnerPractical

How do you run Node.js scripts?

Direct answer

Run node file.js or npm run script — use node for direct execution, npm scripts for project commands with PATH and env; pass args after -- and set NODE_ENV for environment-specific behavior.

Common execution patterns

  1. Direct — node src/index.ts (often via tsx/ts-node in dev).
  2. npm script — npm run dev runs scripts.dev; adds node_modules/.bin to PATH.
  3. Arguments — node app.js -- --port 4000; process.argv parses flags.
  4. Shebang — #!/usr/bin/env node on CLI tools makes file executable on Unix.
cli-args.mjs
#!/usr/bin/env node

const args = process.argv.slice(2);
const port = args.includes("--port")
  ? Number(args[args.indexOf("--port") + 1])
  : 4000;

console.log(`Starting on port ${port}`);

// Run: node cli-args.mjs --port 5000
// Or: npm run content:import:interviews -- data/file.json
  • watch mode — node --watch (Node 18+) restarts on file changes; tsx watch in AceDevHub dev.
  • env vars — NODE_ENV=production node dist/index.js; use dotenv in dev (Q48).
Question 19BeginnerComparison

What is the difference between .js, .mjs, and .cjs in Node.js?

Direct answer

.js module type follows package.json "type" field; .mjs is always ESM (import/export); .cjs is always CommonJS (require/module.exports) — use explicit extensions when mixing systems in one project.

Node supports dual module systems during the long ESM migration. Extension + package.json type remove ambiguity for both Node and bundlers.

ExtensionModule systemNotes
.jsDepends on nearest package.json typetype:module → ESM; default/CommonJS → require
.mjsAlways ESMExplicit; good for config at repo root
.cjsAlways CommonJSLegacy tools, some config files
  • node: prefix — import fs from "node:fs" resolves core modules clearly (recommended).
  • Interop — ESM can import CJS default; CJS cannot require ESM without dynamic import().
  • AceDevHub — API uses "type": "module" + .ts compiled to ESM; content import scripts use tsx.
esm-vs-cjs.mjs
// file: utils.mjs — always ESM
export function greet(name) {
  return `Hello, ${name}`;
}

// file: legacy.cjs — always CommonJS
module.exports = { version: 1 };

// ESM importing CJS
import legacy from "./legacy.cjs";
console.log(legacy.version);
Question 20BeginnerConcept

What is the global object in Node.js?

Direct answer

globalThis (alias global in Node) is the top-level object — unlike browser window, Node exposes process, Buffer, and timers on global; prefer explicit imports (node:fs) over relying on globals in application code.

ECMAScript standardized globalThis so the same code can access the global object in browser and Node. Node still adds runtime-specific properties not found in browsers.

GlobalNodeBrowser
globalThisYesYes (window in browsers)
processYes — env, argv, exitNo
BufferYes — binary dataNo (Uint8Array instead)
documentNoYes
setTimeout / setIntervalYesYes
  • No var pollution needed — top-level const/function in modules are module-scoped, not global.
  • global.user = x anti-pattern — hides dependencies; use explicit modules or DI.
  • ESM strict — modules run in strict mode; this at top level is undefined.
global-demo.mjs
console.log(globalThis === global); // true in Node

console.log(process.version);
console.log(Buffer.from("hi").toString()); // Buffer is global

// Module scope — not on global
export const API_PORT = 4000;
Question 21BeginnerConcept

What is the process object in Node.js?

Direct answer

process is a global Node object representing the running Node process — it exposes env vars, argv, cwd, exit codes, stdin/stdout/stderr, and lifecycle events like beforeExit and signal handlers for graceful shutdown.

Every Node program has exactly one process instance. Servers read PORT from process.env; CLIs parse process.argv; production apps listen for SIGTERM to drain connections (Q73).

Property / methodPurpose
process.envEnvironment variables (strings)
process.argvCLI args — [node, script, ...flags]
process.cwd()Current working directory
process.exit(code)Terminate process — 0 success, non-zero error
process.pidOS process ID
process.on('SIGTERM')Graceful shutdown hook in containers
  • stdin / stdout / stderr — streams for CLI tools and piping: node script.js | grep foo.
  • process.version — Node semver string; pair with process.versions.v8 (Q12).
  • uncaughtException — last-resort handler; prefer fixing errors, not relying on it (Q63).
process-basics.mjs
const port = Number(process.env.PORT) || 4000;
const isProd = process.env.NODE_ENV === "production";

console.log("Node", process.version, "pid", process.pid);
console.log("Args:", process.argv.slice(2));

process.on("SIGTERM", () => {
  console.log("Shutting down gracefully…");
  server.close(() => process.exit(0));
});
Question 22BeginnerConcept

What are __dirname and __filename in Node.js?

Direct answer

__filename is the absolute path of the current module file; __dirname is its directory — CommonJS globals for resolving relative paths; in ESM use import.meta.url with fileURLToPath instead.

Before ESM, __dirname was the standard way to load config.json next to your script regardless of cwd. import.meta.url is the ESM equivalent tied to the module URL.

CommonJSESM
File path__filenamefileURLToPath(import.meta.url)
Directory__dirnamepath.dirname(fileURLToPath(import.meta.url))
AvailableIn .cjs / type absentNot defined — use meta.url pattern
  • cwd vs __dirname — process.cwd() changes if user cd's before node; __dirname always follows the file location.
  • path.join — join(__dirname, 'data', 'file.json') avoids manual slash bugs (Q24).
  • Bundlers — may rewrite paths at build time; server runtime uses real filesystem paths.
dirname-esm.mjs
import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const configPath = path.join(__dirname, "config", "default.json");
console.log(configPath);
Question 23BeginnerConcept

How do environment variables work in Node.js?

Direct answer

process.env is an object of string key-value pairs inherited from the shell and deployment platform — use it for PORT, DATABASE_URL, and secrets; validate at startup and never commit secrets to git.

Why interviewers ask this

Config via env vars keeps twelve-factor apps portable — same Docker image runs in local, staging, and prod with different env injection (Compose, Kubernetes, Hetzner).

  • All strings — process.env.PORT is "4000" not number; coerce with Number() or validation lib.
  • NODE_ENV — convention: development | test | production; frameworks tune logging/cache.
  • .env files — dotenv loads into process.env in dev only; prod uses real env (Q48).
  • Secrets — never log process.env in production; redact in error reports.
env-config.mjs
function requireEnv(name) {
  const value = process.env[name];
  if (!value) throw new Error(`Missing required env: ${name}`);
  return value;
}

const config = {
  port: Number(process.env.PORT ?? 4000),
  databaseUrl: requireEnv("DATABASE_URL"),
  nodeEnv: process.env.NODE_ENV ?? "development",
};

export default config;
Question 24BeginnerConcept

What is the Node.js path module?

Direct answer

The path module joins, resolves, and normalizes file paths in an OS-aware way — use path.join and path.resolve instead of string concatenation to avoid broken paths on Windows vs POSIX.

Hardcoding foo/bar/baz breaks on Windows backslashes. path.join uses the correct separator for the current platform.

MethodUse
path.join(...parts)Safe concatenation — ignores extra slashes
path.resolve(...parts)Absolute path from cwd + segments
path.basename(p)File name — config.json
path.dirname(p)Parent directory
path.extname(p)Extension — .json
path.parse(p)Object with root, dir, base, ext, name
  • Security — path.join upload dir + user input can escape via .. — validate with path.resolve and prefix check (Q66).
  • posix vs win32 — path.posix.join for URLs; path.win32 on Windows-specific tools.
path-examples.mjs
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const dataFile = path.join(__dirname, "data", "interviews", "nodejs");
const absolute = path.resolve("./config", "app.json");

console.log(path.basename(dataFile)); // nodejs
console.log(path.extname(absolute));   // .json
Question 25BeginnerPractical

How do you read and write files in Node.js?

Direct answer

Use fs.promises.readFile/writeFile for async Promise-based I/O, or fs.createReadStream for large files — avoid readFileSync on servers; handle errors with try/catch and validate paths.

The fs module wraps libuv file operations (Q8–10). Promises API fits async/await handlers; streams fit large JSON imports like AceDevHub interview content files.

APIStyleWhen
fs.promises.readFileAsync PromiseSmall/medium files, JSON config
fs.readFile + callbackAsync callbackLegacy codebases
fs.readFileSyncBlocking syncCLI scripts at startup only
fs.createReadStreamStreamLarge files, line-by-line parsing
fs.promises.writeFileAsync PromiseSave output, generate files
  • Encoding — 'utf8' returns string; omit encoding for Buffer binary.
  • JSON.parse — wrap readFile + parse in try/catch; invalid JSON throws.
  • Permissions — EACCES and ENOENT need user-friendly errors; do not leak paths in public API.
fs-read-write.mjs
import fs from "node:fs/promises";
import path from "node:path";

async function loadInterviewJson(relativePath) {
  const fullPath = path.resolve(relativePath);
  const raw = await fs.readFile(fullPath, "utf8");
  return JSON.parse(raw);
}

async function saveReport(dir, filename, data) {
  await fs.mkdir(dir, { recursive: true });
  await fs.writeFile(
    path.join(dir, filename),
    JSON.stringify(data, null, 2),
    "utf8"
  );
}
Question 26BeginnerConcept

What is the Node.js http module?

Direct answer

The http module creates HTTP servers and clients on top of TCP — http.createServer handles requests with a callback or request listener; production APIs often use Fastify or Express built on the same Node HTTP foundation.

Node's low-level http module maps directly to HTTP/1.1 semantics — method, headers, status code, body stream — without routing or JSON parsing. Frameworks add those ergonomics.

  • http.createServer — returns http.Server; emits 'request' for each incoming connection.
  • http.request / fetch — client-side outbound calls to other APIs.
  • https module — TLS layer; terminate SSL at Caddy/reverse proxy in AceDevHub prod.
  • HTTP/2 — http2 module for multiplexing; less common in simple REST APIs.
http-basics.mjs
import http from "node:http";

const server = http.createServer((req, res) => {
  if (req.method === "GET" && req.url === "/health") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ ok: true }));
    return;
  }
  res.writeHead(404).end("Not found");
});

server.listen(4000);
Question 27BeginnerPractical

How do you create a simple HTTP server in Node.js?

Direct answer

Call http.createServer(handler).listen(port) — the handler receives req and res for each request; set status and headers with writeHead, send body with end(), and keep the handler non-blocking.

Minimal server checklist

  1. Create server — http.createServer or http.Server with addListener('request').
  2. Route by method + url — if (req.method === 'GET' && req.url === '/health').
  3. Respond — writeHead(status, headers); res.end(body).
  4. Listen — server.listen(PORT, callback); bind 0.0.0.0 in Docker.
simple-server.mjs
import http from "node:http";

const PORT = Number(process.env.PORT) || 4000;

const server = http.createServer(async (req, res) => {
  if (req.url === "/" && req.method === "GET") {
    res.writeHead(200, { "Content-Type": "text/plain" });
    res.end("AceDevHub API shell\n");
    return;
  }

  res.writeHead(405, { "Allow": "GET" });
  res.end("Method Not Allowed");
});

server.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}`);
});
Question 28BeginnerConcept

What are the req and res objects in Node.js HTTP?

Direct answer

req (IncomingMessage) is a readable stream of the request — method, url, headers, body; res (ServerResponse) is writable — set status/headers, write chunks, end response; both inherit stream backpressure behavior.

Treat req as input and res as output for one HTTP exchange. Large POST bodies should be consumed as streams, not buffered unbounded in memory.

reqres
methodstatusCode (default 200)
urlsetHeader / writeHead
headerswrite / end
Readable stream bodyWritable stream body
socketheadersSent flag
  • Read body — collect chunks: req.on('data'); or use framework parser for JSON.
  • headersSent — after writeHead/send, cannot change status — guard double responses.
  • Framework mapping — Fastify request/reply wrap same concepts with validation and serializers.
req-res-body.mjs
function readJsonBody(req) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    req.on("data", (chunk) => chunks.push(chunk));
    req.on("end", () => {
      try {
        resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
      } catch (err) {
        reject(err);
      }
    });
    req.on("error", reject);
  });
}

// In handler:
// const body = await readJsonBody(req);
// res.writeHead(201, { "Content-Type": "application/json" });
// res.end(JSON.stringify({ id: 1 }));
Question 29BeginnerConcept

What is the Node.js url module?

Direct answer

The url module parses and formats URLs — use new URL() (WHATWG API, global in Node) or url.parse legacy; extract pathname, searchParams, and hostname for routing and query handling.

req.url on a server is often path + query only (/interviews?page=2), not a full URL — construct a base URL or parse with URL class carefully.

  • new URL(input, base) — recommended WHATWG API; searchParams is URLSearchParams.
  • pathname — route matching without query string.
  • fileURLToPath — converts file:// URLs from import.meta.url to OS paths (Q22).
url-parse.mjs
const absolute = new URL("https://acedevhub.com/interviews/nodejs?page=2&sort=asc");

console.log(absolute.pathname);  // /interviews/nodejs
console.log(absolute.hostname);  // acedevhub.com
console.log(absolute.searchParams.get("page")); // "2"

// Server-relative req.url
const reqUrl = "/interviews/nodejs-interview-questions?limit=10";
const parsed = new URL(reqUrl, "http://localhost");
console.log(parsed.pathname);
Question 30BeginnerPractical

How do you parse query strings in Node.js?

Direct answer

Use URLSearchParams or new URL(req.url, base).searchParams — get(), getAll(), has() for filters and pagination; validate and coerce types before using in SQL or business logic.

Query strings carry optional filters — ?page=2&limit=20&difficulty=beginner — without extra path segments. REST list endpoints depend on consistent parsing.

ApproachNotes
URL.searchParamsPreferred — standards-based
querystring.parse (legacy)Returns plain object; prototype pollution risk on old patterns
Framework queryFastify req.query — parsed automatically
  • Repeated keys — ?tag=js&tag=node → getAll('tag') returns array.
  • Validation — query values are strings; Number(page) or Zod schema before DB offset.
  • Defaults — limit = Number(params.get('limit') ?? 20); cap max to prevent abuse.
query-parse.mjs
function parseListQuery(reqUrl) {
  const { searchParams } = new URL(reqUrl, "http://localhost");

  const page = Math.max(1, Number(searchParams.get("page") ?? 1));
  const limit = Math.min(100, Number(searchParams.get("limit") ?? 20));
  const difficulty = searchParams.get("difficulty") ?? undefined;

  return { page, limit, difficulty, offset: (page - 1) * limit };
}

// GET /interviews?page=2&limit=10&difficulty=beginner
parseListQuery("/interviews?page=2&limit=10&difficulty=beginner");
Question 31BeginnerPractical

How do you work with JSON in Node.js?

Direct answer

Use JSON.parse for strings and JSON.stringify for objects — read/write JSON files with fs and validate shape before use; handle parse errors and avoid circular references when stringifying.

JSON is the default wire format for REST APIs and config files. Node exposes JSON globally — no import needed — but parsing untrusted input still requires validation.

Why interviewers ask

  • API handlers — every POST/PUT body and JSON response path uses parse/stringify.
  • Config & content — package.json, interview import JSON, .env is not JSON but often paired with JSON config.
  • Failure modes — invalid JSON, BigInt, Date, undefined in objects, circular graphs.
MethodUse case
JSON.parse(str)String → object; throws SyntaxError on bad input
JSON.stringify(obj)Object → string; replacer/space for pretty print
fs + parseLoad local JSON files (content import, fixtures)
res.end(JSON.stringify(...))Raw http JSON response
json-handling.mjs
function safeParseJson(raw) {
  try {
    return { ok: true, data: JSON.parse(raw) };
  } catch (err) {
    return { ok: false, error: err.message };
  }
}

const payload = { topic: "nodejs-interview-questions", count: 30 };
const json = JSON.stringify(payload, null, 2);

// BigInt and undefined are not JSON-safe by default
// JSON.stringify({ n: 1n }) throws
Question 32BeginnerConcept

How does console and logging work in Node.js?

Direct answer

console methods (log, error, warn, time) write to stdout/stderr — fine for dev; production APIs use structured loggers (pino in Fastify) with levels, JSON output, and request correlation IDs.

Node's console is a thin wrapper around process.stdout and process.stderr. It is synchronous for small writes — acceptable in scripts, risky as the only observability in high-traffic servers.

  • console.log / info — general output; objects print with util.inspect depth.
  • console.error — stderr; survives when stdout is piped or captured differently.
  • console.time / timeEnd — quick duration labels for local profiling.
  • Structured logging — pino/bunyan: level, msg, reqId, err stack as JSON lines for log aggregators.
logging-basics.mjs
console.log("Server starting on port", process.env.PORT ?? 4000);

console.time("import");
// await importInterviewFile(...)
console.timeEnd("import");

// Production-style (Fastify uses pino internally)
const log = {
  info: (obj, msg) => console.log(JSON.stringify({ level: 30, ...obj, msg })),
  error: (obj, msg) => console.error(JSON.stringify({ level: 50, ...obj, msg })),
};

log.info({ topicSlug: "nodejs-interview-questions" }, "content imported");
Question 33BeginnerPractical

What are the basics of debugging Node.js applications?

Direct answer

Use node --inspect for breakpoints, read stack traces from errors, reproduce with minimal scripts, and distinguish sync throws vs unhandled promise rejections — combine with logging and NODE_OPTIONS for local dev.

Node debugging spans runtime inspection (Chrome DevTools / VS Code), printf-style logs, and error semantics (where the failure surfaced vs where it originated).

  1. Inspector — node --inspect-brk app.mjs; attach VS Code "JavaScript Debug Terminal".
  2. Stack traces — read err.stack; use Error.captureStackTrace for custom errors sparingly.
  3. REPL — node then require/import module under test (Q17).
  4. Environment — NODE_DEBUG=module for low-level traces; DEBUG=* for some libraries.
SymptomFirst check
Hang / no responseMissing res.end(), open DB connection, deadlock
ECONNREFUSEDWrong host/port, service not running (Postgres 5433)
SyntaxError in JSON importTrailing comma, invalid file — validate with jq
Unhandled rejectionMissing await or .catch on Promise chain
debug-helper.mjs
process.on("unhandledRejection", (reason) => {
  console.error("Unhandled rejection:", reason);
});

function wrapHandler(fn) {
  return async (req, res) => {
    try {
      await fn(req, res);
    } catch (err) {
      console.error(err.stack);
      if (!res.headersSent) {
        res.writeHead(500, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ error: "Internal Server Error" }));
      }
    }
  };
}
2

Intermediate Node.js Interview Questions

Question 34IntermediateConcept

What is the difference between CommonJS and ES modules in Node.js?

Direct answer

CommonJS uses require/module.exports with synchronous load and runtime resolution; ES modules use import/export with static analysis, async top-level await, and live bindings — Node supports both; package.json "type" and .mjs/.cjs extensions pick the default.

Node evolved from CommonJS (CJS) as its original module system to native ECMAScript modules (ESM) aligned with browser JavaScript. AceDevHub's monorepo uses ESM in apps/api and apps/web with "type": "module" or .mjs where needed.

Why interviewers ask

AspectCommonJSES Modules
Syntaxrequire(), module.exportsimport / export
LoadingSynchronous at require timeAsync graph; import hoisted
AnalysisDynamic — require(variable)Static — imports must be top-level strings
Default in Node.js when "type": "commonjs" or absent.js when "type": "module"
InteropcreateRequire, dynamic import()import() for CJS default export
  • When to use ESM — new Node projects, shared isomorphic code with browsers, top-level await in scripts.
  • When CJS remains — legacy packages, some tooling configs, gradual migration paths.
cjs-vs-esm
// CommonJS (utils.cjs)
const path = require("node:path");
module.exports = { join: path.join };

// ES Module (utils.mjs)
import path from "node:path";
export const join = path.join;

// ESM importing CJS
import pkg from "legacy-cjs-package";
const { createRequire } = await import("node:module");
const require = createRequire(import.meta.url);
Question 35IntermediateConcept

What is the difference between require and import in Node.js?

Direct answer

require is CommonJS — synchronous, can run anywhere, returns a copied exports object; import is ESM — hoisted, async module graph, live bindings for named exports; use dynamic import() from CJS or await import() for conditional loads.

Mixing require in .mjs or top-level import in .cjs throws — file extension and package.json type define which loader runs.

  • require(id) — resolves path, loads synchronously, caches by resolved filename.
  • import decl — must be static string at top level (except dynamic import()).
  • import() — returns Promise; works in CJS and ESM for lazy/conditional loading.
  • Default vs named — require gives module.exports; import default from matches .default interop for CJS.
require-vs-import.mjs
// ESM static import (evaluated before other code in module)
import fs from "node:fs/promises";

// Dynamic import — conditional, async
async function loadPlugin(name) {
  const mod = await import(`./plugins/${name}.js`);
  return mod.default;
}

// From CommonJS file only:
// const fs = require("node:fs");
// (async () => { const m = await import("node:fs/promises"); })();
Question 36IntermediateConcept

How does module caching work in Node.js?

Direct answer

Node caches modules by resolved absolute path after first load — subsequent require/import of the same file return the same exports object; delete require.cache[path] forces reload (dev only); ESM cache is separate and not meant for hot reload via cache deletion.

Module caching implements the singleton pattern — config loaders, DB pool setup, and shared state run once per process. Understanding cache prevents surprise shared mutable state bugs.

Why interviewers ask

  1. First load — wrap module in function, execute, store exports in require.cache.
  2. Second load — return cached exports; module body does not re-run.
  3. Mutating exports — all importers see the same object if you mutate properties after load.
  4. Circular deps — partial exports visible because cache entry exists before module finishes (Q39).
module-cache.cjs
// counter.cjs
let count = 0;
module.exports = {
  increment() { return ++count; },
};

// app.cjs
const a = require("./counter.cjs");
const b = require("./counter.cjs");

console.log(a.increment()); // 1
console.log(b.increment()); // 2 — same instance

// Dev-only reload:
// delete require.cache[require.resolve("./counter.cjs")];
Question 37IntermediateConcept

What does "type": "module" mean in package.json?

Direct answer

"type": "module" treats .js files in that package as ES modules — import/export allowed, require absent unless .cjs; omitting type or "commonjs" keeps .js as CommonJS; use .mjs/.cjs to override per file.

The type field sets the default module system for .js files in a package boundary — critical in monorepos where apps/api and apps/web may differ from legacy CJS tools.

Setting.js behaviorExtension override
"type": "module"ESM — import/export.cjs → CommonJS
"type": "commonjs" or omittedCJS — require.mjs → ESM
No type in root package.jsonNode default: CommonJS for .jsPer-package in workspaces
  • Node version — ESM stable without flags in modern Node LTS; check engines in package.json.
  • Tooling — tsx/ts-node respect type; Jest/Vitest need ESM config when type is module.
  • Dual packages — libraries may ship "exports" map with import and require conditions.
package.json
{
  "name": "@acedevhub/api",
  "type": "module",
  "engines": { "node": ">=20" },
  "scripts": {
    "content:import:interviews": "tsx --env-file=.env src/scripts/import-content-interviews.ts"
  }
}
Question 38IntermediateConcept

What is the difference between exports and module.exports in Node.js?

Direct answer

module.exports is the actual export object require() returns; exports is a shorthand reference to it — reassigning exports breaks the link, but adding properties to exports works; set module.exports to a function or class when that is the whole export.

At load time Node sets exports = module.exports — they start as the same object. Interview questions often trap candidates who reassign exports to a new object.

PatternWorks?Why
exports.foo = 1YesMutates shared module.exports object
module.exports = { foo: 1 }YesReplaces export wholesale
exports = { foo: 1 }NoRebinds local exports variable only
module.exports = function() {}YesSingle function export pattern
  • ESM equivalent — export default / export { named } — no module.exports.
  • Barrel files — re-export from index.cjs: module.exports = require('./impl');
exports-pattern.cjs
// Correct: attach properties
exports.parse = (s) => JSON.parse(s);

// Correct: replace entire export
module.exports = function createServer() {
  return http.createServer();
};

// Wrong — importers still get old object
// exports = { parse: (s) => JSON.parse(s) };

// require('./exports-pattern.cjs') returns createServer function
Question 39IntermediateConcept

What are circular dependencies in Node.js and how do you handle them?

Direct answer

Circular deps occur when module A requires B while B requires A — Node returns partially initialized exports from cache; fix by restructuring layers, lazy require inside functions, or dependency injection instead of mutual top-level imports.

Because modules are cached during load (Q36), a circular graph does not infinite-loop — but one side may read undefined exports if it accesses bindings before the other module finishes executing.

Why interviewers ask

  1. Load order — A starts, requires B; B requires A; A's exports object exists but properties may be unset.
  2. Symptoms — TypeError: X is not a function, undefined handler at startup.
  3. Fix: restructure — extract shared types/utils to a third module both depend on.
  4. Fix: lazy load — require('./b') inside a function after all modules initialized.
circular-fix.cjs
// a.cjs
exports.name = "A";
const b = require("./b.cjs"); // b may see partial a

// Better: shared.cjs holds interfaces
// a.cjs and b.cjs both require('./shared.cjs') only

// Lazy pattern in a.cjs
function getB() {
  return require("./b.cjs");
}
module.exports = { name: "A", getB };
Question 40IntermediateConcept

What is the difference between npm and npx?

Direct answer

npm installs and manages package dependencies in node_modules; npx executes package binaries — locally from node_modules/.bin or temporarily downloading a package — without global install, ideal for one-off CLIs like create-* scaffolds and tsx scripts.

npm is the package manager — install, update, audit, publish. npx is the package runner — run a command from a dependency without polluting global PATH.

CommandPurposeExample
npm install pkgAdd dependency to projectnpm install fastify
npm run scriptRun package.json scriptnpm run content:import:interviews
npx tsx file.tsRun local or fetch tsxnpx tsx src/scripts/seed.ts
npx create-next-appOne-shot scaffoldNo global create-next-app needed
  • Workspaces — npm install at monorepo root hoists shared deps; --workspace targets apps/api.
  • npm ci — clean install from lockfile in CI; faster and reproducible.
  • npx vs npm exec — npm exec is the modern equivalent; npx remains widely used.
npm-npx.sh
# Install dev tool in monorepo
npm install -D tsx --workspace @acedevhub/api

# Run without adding to scripts
npx tsx apps/api/src/scripts/import-content-interviews.ts

# Workspace-scoped script (preferred in AceDevHub)
npm run content:import:interviews --workspace @acedevhub/api
Question 41IntermediateConcept

What is semantic versioning (semver) in npm?

Direct answer

Semver uses MAJOR.MINOR.PATCH — breaking changes bump major, backward-compatible features minor, bug fixes patch; npm ranges (^, ~, exact) control auto-upgrades; lockfiles pin resolved versions for reproducible builds.

Version strings like 2.4.1 communicate compatibility expectations — critical when dozens of packages compose AceDevHub's monorepo.

SegmentWhen it bumpsExample change
MAJORBreaking APIRemoved export, changed signature
MINORBackward-compatible featureNew optional option
PATCHBackward-compatible fixSecurity patch, bug fix
  • ^1.2.3 — allow >=1.2.3 <2.0.0 (default npm install range).
  • ~1.2.3 — allow patch updates within 1.2.x.
  • package-lock.json — records exact tree; commit it for apps.
  • npm audit — surfaces vulnerable semver-resolved versions.
semver-ranges.json
{
  "dependencies": {
    "fastify": "^5.0.0",
    "pg": "~8.13.0"
  },
  "devDependencies": {
    "tsx": "4.19.2"
  }
}
Question 42IntermediateConcept

What is middleware in Node.js HTTP servers?

Direct answer

Middleware is a chain of functions (req, res, next) that inspect or transform requests before the final handler — auth, logging, body parsing, CORS; each calls next() to continue or ends the response to stop the chain.

Raw http.createServer has no middleware — frameworks compose ordered functions. Fastify uses hooks and plugins with similar pipeline semantics and encapsulation.

Why interviewers ask

  • Order matters — body parser before route that reads req.body; auth before protected routes.
  • next(err) — skip to error-handling middleware in Express-style stacks.
  • Short-circuit — send 401 and return without next() for failed auth.
  • Cross-cutting concerns — logging, rate limits, request IDs — not duplicated per route.
middleware-chain.mjs
function logger(req, res, next) {
  console.log(req.method, req.url);
  next();
}

function requireJson(req, res, next) {
  if (!req.headers["content-type"]?.includes("application/json")) {
    res.writeHead(415).end("JSON only");
    return;
  }
  next();
}

function compose(...fns) {
  return (req, res) => {
    let i = 0;
    const next = () => {
      const fn = fns[i++];
      if (fn) fn(req, res, next);
    };
    next();
  };
}
Question 43IntermediateConcept

What are the basics of Express.js?

Direct answer

Express is a minimal Node.js web framework — app.get/post for routes, app.use for middleware, Router for modular paths, and res.json/status for responses; AceDevHub uses Fastify instead, but Express patterns appear in most Node interview loops.

Express sits on Node's http module and adds routing, middleware composition, and response helpers — the de facto teaching stack even when production code picks Fastify or Koa.

APIRole
express()Create app instance
app.use(mw)Register middleware for all/mounted paths
app.get('/path', handler)HTTP verb + route handler
express.Router()Sub-router mounted at prefix
res.json({})Send JSON with Content-Type
next()Pass control in middleware chain
  • Static files — express.static('public') for assets.
  • Error middleware — four-arg (err, req, res, next) handler last in stack.
  • Fastify contrast — schema-first validation, faster JSON serialization, plugin encapsulation.
express-basics.mjs
import express from "express";

const app = express();
app.use(express.json());

const interviews = express.Router();
interviews.get("/:slug", (req, res) => {
  res.json({ slug: req.params.slug, topic: "nodejs-interview-questions" });
});

app.use("/interviews", interviews);

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({ error: "Internal Server Error" });
});

app.listen(4000);
Question 44IntermediatePractical

How does routing work in Express.js?

Direct answer

Express matches HTTP method + path patterns — app.get/post/put/delete, route params (:id), optional regex, and mounted Routers at prefixes; first matching route wins, so order static paths before parametric ones.

Routing maps URL + verb to handler functions. Express builds a routing table internally — similar mental model to Fastify route registration with method and url.

PatternExampleMatches
Literalapp.get('/health')GET /health
Paramapp.get('/topics/:slug')GET /topics/nodejs-interview-questions
Optionalapp.get('/users/:id?')GET /users or /users/42
Router mountapp.use('/interviews', router)All paths under /interviews/*
  • req.params — named segments from :slug, :id.
  • Route order — /users/me before /users/:id or 'me' is captured as id.
  • app.all — same path, any HTTP method.
express-routing.mjs
import express from "express";

const app = express();
const topics = express.Router({ mergeParams: true });

topics.get("/:topicSlug/questions/:questionSlug", (req, res) => {
  const { topicSlug, questionSlug } = req.params;
  res.json({ topicSlug, questionSlug });
});

app.use("/interviews/topics", topics);

// Specific before generic
app.get("/users/me", (req, res) => res.json({ self: true }));
app.get("/users/:id", (req, res) => res.json({ id: req.params.id }));
Question 45IntermediateConcept

What are Express request handlers?

Direct answer

Request handlers are (req, res, next) functions that read req (params, query, body, headers) and write res (status, json, send, redirect) — async handlers must catch errors or forward with next(err) to reach error middleware.

Handlers are the terminal or intermediate steps in the middleware chain — they implement business logic after parsers and auth run.

Why interviewers ask

reqres
params — route :segmentsstatus(code) — set HTTP status
query — ?key=valuejson(obj) — JSON + Content-Type
body — parsed POST JSONsend(str) — body + end
headers, cookiesredirect(url) — 302/301
method, pathset(header, value)
  • Async handlers — wrap await in try/catch and next(err), or use express-async-errors.
  • One response — calling res.json after res.send throws ERR_HTTP_HEADERS_SENT.
  • Fastify mapping — handler becomes route function; reply.code().send() parallels res.status().json().
express-handler.mjs
async function getTopic(req, res, next) {
  try {
    const { slug } = req.params;
    const page = Number(req.query.page ?? 1);
    // const topic = await interviewsService.getBySlug(slug, page);
    res.status(200).json({ slug, page });
  } catch (err) {
    next(err);
  }
}

// app.get('/interviews/topics/:slug', getTopic);
Question 46IntermediatePractical

How do you parse JSON request bodies in Node.js?

Direct answer

Use express.json() middleware or manually read req stream and JSON.parse — set size limits, validate schema after parse, return 400 on malformed JSON; Fastify parses JSON bodies when Content-Type is application/json.

POST/PUT/PATCH bodies arrive as a readable stream on req — parsers aggregate chunks into req.body before your handler runs.

  • express.json() — built-in since Express 4.16; replaces body-parser for JSON.
  • Limit option — express.json({ limit: '100kb' }) prevents large payload DoS.
  • Content-Type — parser runs only when header is application/json.
  • Validation — Zod/Fastify schema after parse; never trust shape from client.
json-body.mjs
import express from "express";

const app = express();
app.use(express.json({ limit: "256kb" }));

app.post("/waitlist", (req, res) => {
  const { email, courseSlug } = req.body ?? {};
  if (!email || !courseSlug) {
    return res.status(400).json({ error: "email and courseSlug required" });
  }
  res.status(201).json({ ok: true });
});

// Raw http: collect chunks (Q28) then JSON.parse with try/catch
Question 47IntermediateConcept

What is CORS and how do you handle it in Node.js?

Direct answer

CORS is a browser security policy — servers must send Access-Control-Allow-Origin (and related headers) for cross-origin fetches; use cors middleware in Express or @fastify/cors in Fastify with explicit allowed origins, not * with credentials.

CORS applies to browsers, not server-to-server calls. AceDevHub web (localhost:3000) calling API (localhost:4000) is cross-origin — the API must allow the web origin.

Why interviewers ask

HeaderPurpose
Access-Control-Allow-OriginWhich origins may read the response
Access-Control-Allow-MethodsAllowed verbs for preflight
Access-Control-Allow-HeadersAllowed request headers (Authorization, Content-Type)
Access-Control-Allow-Credentialstrue when cookies/httpOnly JWT cross-origin
  • Preflight OPTIONS — browser sends OPTIONS before non-simple requests; server must respond 204 with CORS headers.
  • Credentials — fetch(..., { credentials: 'include' }) requires specific origin, not *.
  • SameSite cookies — auth design pairs with CORS for Google OAuth session cookies.
cors-express.mjs
import express from "express";
import cors from "cors";

const app = express();

app.use(cors({
  origin: ["http://localhost:3000", "https://acedevhub.com"],
  credentials: true,
  methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
}));

// Fastify: await fastify.register(import('@fastify/cors'), { origin: [...] })
Question 48IntermediatePractical

How does dotenv work for Node.js configuration?

Direct answer

dotenv loads KEY=value pairs from a .env file into process.env at startup — use for local dev only; production injects env via Docker/CI; validate required vars with a schema and never commit secrets.

Twelve-factor apps treat config as environment — dotenv bridges local .env files to process.env so code reads process.env.DATABASE_URL uniformly across environments.

  • import 'dotenv/config' — loads .env from cwd early in entry file.
  • tsx --env-file=.env — Node 20+ native env file loading; AceDevHub import scripts use this.
  • .env.example — committed template without secrets; documents required keys.
  • Precedence — real environment variables override .env file values.
dotenv-config.mjs
import "dotenv/config";

const config = {
  port: Number(process.env.PORT ?? 4000),
  databaseUrl: process.env.DATABASE_URL,
  nodeEnv: process.env.NODE_ENV ?? "development",
};

if (!config.databaseUrl) {
  throw new Error("DATABASE_URL is required — see apps/api/.env.example");
}

export { config };
Question 49IntermediateConcept

What are streams in Node.js?

Direct answer

Streams process data in chunks over time instead of loading everything into memory — Node uses them for files, HTTP bodies, compression, and crypto; pipe() connects readable → writable with backpressure handling.

Streams are Node's answer to large or slow I/O — read a 500MB log file or proxy a download without a 500MB Buffer. They align with non-blocking I/O (Q10) and the event-driven model.

Why interviewers ask

  • Memory efficiency — constant memory vs readFile on huge inputs.
  • Latency — start processing first chunk before entire file arrives.
  • Composability — fs.createReadStream → gzip → http response.
  • Built-in usage — req/res are streams; stdout/stdin are streams.
stream-pipe.mjs
import fs from "node:fs";
import { pipeline } from "node:stream/promises";
import { createGzip } from "node:zlib";

// Stream large interview JSON export instead of readFileSync
await pipeline(
  fs.createReadStream("data/interviews/nodejs/nodejs-interview-questions.json"),
  createGzip(),
  fs.createWriteStream("/tmp/interviews.json.gz")
);
Question 50IntermediateConcept

What are the types of streams in Node.js?

Direct answer

Node has four stream types — Readable (source), Writable (destination), Duplex (both, e.g. TCP socket), and Transform (Duplex that modifies data, e.g. gzip); all extend EventEmitter and implement the stream API.

Classifying streams by data direction helps pick the right API and debug flow — interviewers often ask which type fits a file read vs HTTP proxy vs encryption.

TypeDirectionExamples
ReadableRead onlyfs.createReadStream, http.IncomingMessage
WritableWrite onlyfs.createWriteStream, http.ServerResponse
DuplexRead + write independentnet.Socket, TLS socket
TransformRead → transform → writezlib.createGzip, crypto.createCipheriv
  • Object mode — streams push JS objects instead of Buffers/strings when objectMode: true.
  • Legacy vs web — Node stream API; also stream/web ReadableStream in fetch.
  • HighWaterMark — internal buffer threshold before backpressure kicks in (Q53).
stream-types.mjs
import { Readable, Writable, Transform } from "node:stream";

const source = Readable.from(["line1\n", "line2\n"]);
const upper = new Transform({
  transform(chunk, enc, cb) {
    cb(null, chunk.toString().toUpperCase());
  },
});
const sink = new Writable({
  write(chunk, enc, cb) {
    process.stdout.write(chunk);
    cb();
  },
});

source.pipe(upper).pipe(sink);
Question 51IntermediateConcept

How do Readable and Writable streams work in Node.js?

Direct answer

Readable emits 'data' chunks or is consumed via async iteration/read(); Writable accepts write() chunks and signals completion with end() — both pause when internal buffers exceed highWaterMark until the consumer catches up.

Readable streams produce data; Writable streams consume it. Modes matter: flowing mode auto-emits 'data'; paused mode uses read() explicitly.

ReadableWritable
on('data', fn) / for awaitwrite(chunk, cb)
on('end') when exhaustedend() / end(chunk) to finish
pause() / resume()cork() / uncork() batch writes
pipe(writable)Returns backpressure signal via write return false
  1. Create readable — fs.createReadStream or Readable.from(array).
  2. Consume — pipeline to writable or collect with async iteration.
  3. Handle errors — 'error' event on both sides; destroy stream on failure.
readable-writable.mjs
import fs from "node:fs";

const readable = fs.createReadStream("large-export.ndjson", { encoding: "utf8" });
const writable = fs.createWriteStream("filtered.ndjson");

readable.on("data", (line) => {
  if (line.includes("nodejs")) writable.write(line);
});

readable.on("end", () => writable.end());
readable.on("error", (err) => {
  writable.destroy(err);
});

// Better: await pipeline(readable, transform, writable)
Question 52IntermediateConcept

What are Duplex and Transform streams in Node.js?

Direct answer

Duplex streams are independent readable and writable sides (like a socket); Transform streams are Duplex variants that modify data in _transform — use for gzip, encryption, line parsing, or NDJSON filtering in a pipeline.

Duplex automatic pipe from write to read — TCP sends and receives independently. Transform links written input to readable output through your transform function.

  • Duplex examples — net.connect(), child_process stdin/stdout.
  • Transform examples — zlib, crypto ciphers, CSV line splitter.
  • _transform(chunk, enc, cb) — call cb(null, outputChunk) zero or more times per input.
  • _flush(cb) — emit trailing data when input ends.
transform-lines.mjs
import { Transform } from "node:stream";
import { pipeline } from "node:stream/promises";
import fs from "node:fs";

const ndjsonFilter = new Transform({
  objectMode: true,
  transform(obj, _enc, cb) {
    if (obj.topic === "Node.js") cb(null, obj);
    else cb();
  },
});

// Readable.from([...]) → ndjsonFilter → fs.createWriteStream
await pipeline(
  fs.createReadStream("export.ndjson", { encoding: "utf8" }),
  lineSplitter(), // hypothetical Transform
  fs.createWriteStream("node-only.ndjson")
);
Question 53IntermediateConcept

What is backpressure in Node.js streams?

Direct answer

Backpressure slows a fast producer when the consumer cannot keep up — writable.write() returns false, pause the readable, resume on 'drain'; pipeline handles this automatically; ignoring it causes memory spikes and OOM.

Without backpressure, a fast disk read flooding a slow network response buffers unbounded chunks in RAM — production APIs hit OOM under load.

Why interviewers ask

  1. write returns false — stop writing until writable emits 'drain'.
  2. readable.pause() — manual backpressure when not using pipe/pipeline.
  3. highWaterMark — default 16KB (object mode: 16 objects); tune for throughput vs memory.
  4. pipeline() — wires pause/resume across the chain correctly.
backpressure.mjs
function writeWithBackpressure(readable, writable) {
  readable.on("data", (chunk) => {
    const ok = writable.write(chunk);
    if (!ok) {
      readable.pause();
      writable.once("drain", () => readable.resume());
    }
  });
  readable.on("end", () => writable.end());
}

// Prefer:
// import { pipeline } from "node:stream/promises";
// await pipeline(readable, writable);
Question 54IntermediateConcept

What is a Buffer in Node.js?

Direct answer

Buffer is Node's fixed-length raw binary data type — like Uint8Array for bytes from files, sockets, and crypto; use Buffer.from/alloc, avoid deprecated constructors, and specify encoding (utf8, hex, base64) when converting to strings.

JavaScript strings are UTF-16; Buffers hold bytes for I/O that is not text — images, hashes, TLS, file chunks in streams. Before TypedArrays were universal, Buffer was the Node binary workhorse.

Why interviewers ask

APIPurpose
Buffer.from(str, 'utf8')String → bytes
buf.toString('hex')Bytes → hex display
Buffer.alloc(n)Zero-filled n-byte buffer
Buffer.concat(arr)Join buffer chunks from stream
buf.lengthByte size, not character count
  • Stream chunks — default 'data' events emit Buffer unless encoding set on readable.
  • Uint8Array interop — modern APIs accept both; Buffer is Uint8Array subclass.
  • Security — zero fill on alloc; wipe sensitive buffers when done if handling secrets.
buffer-basics.mjs
const text = "AceDevHub";
const buf = Buffer.from(text, "utf8");

console.log(buf.length);        // bytes
console.log(buf.toString("hex"));

const chunks = [Buffer.from("hel"), Buffer.from("lo")];
const joined = Buffer.concat(chunks).toString("utf8"); // "hello"
Question 55IntermediatePractical

What is stream.pipeline in Node.js?

Direct answer

stream.pipeline (and stream/promises.pipeline) connects streams with proper error forwarding, cleanup, and backpressure — prefer it over manual .pipe() chains in production code.

pipeline() solves three pipe pitfalls — unhandled errors, missing destroy on failure, and broken backpressure across multiple transforms.

  • Callback form — pipeline(a, b, c, (err) => { ... }) from node:stream.
  • Promise form — await pipeline(a, b, c) from node:stream/promises.
  • AbortSignal — optional signal destroys pipeline on timeout/cancel.
  • Return value — promise resolves with { readable, writable } refs for last streams.
pipeline.mjs
import fs from "node:fs";
import { pipeline } from "node:stream/promises";
import { createGzip } from "node:zlib";

try {
  await pipeline(
    fs.createReadStream("export.ndjson"),
    createGzip(),
    fs.createWriteStream("export.ndjson.gz")
  );
  console.log("Pipeline finished");
} catch (err) {
  console.error("Pipeline failed:", err.message);
}
Question 56IntermediateConcept

What is EventEmitter in Node.js?

Direct answer

EventEmitter is Node's pub/sub base class — on/once/emit/off for named events; streams, HTTP servers, and process extend it; decouple producers and consumers without direct callbacks.

Node's core is event-driven — EventEmitter is the primitive behind 'data', 'error', 'close', and custom domain events in services.

MethodBehavior
on(event, fn)Subscribe; fn runs on every emit
once(event, fn)Subscribe; auto-remove after first emit
emit(event, ...args)Sync invoke all listeners
off / removeListenerUnsubscribe specific fn
listenerCount(event)Debug listener buildup
  • extends EventEmitter — class JobQueue extends EventEmitter { emit('completed', id) }.
  • Sync emit — listeners run inline; heavy work in listener blocks emit caller.
  • error event — special: unhandled 'error' on emitter throws if no listener.
event-emitter.mjs
import { EventEmitter } from "node:events";

class ImportRunner extends EventEmitter {
  async run(file) {
    this.emit("start", file);
    // await upsert...
    this.emit("done", { file, count: 53 });
  }
}

const runner = new ImportRunner();
runner.on("done", ({ file, count }) => {
  console.log(`Imported ${file}: ${count} questions`);
});

await runner.run("nodejs-interview-questions.json");
Question 57IntermediateConcept

How do EventEmitter memory leaks happen in Node.js?

Direct answer

Leaks occur when listeners are added repeatedly without removal — setMaxListeners warns at 10 by default; fix with off/removeListener, once, or WeakRef patterns; hot-reload and long-lived servers are common culprits.

Each on() keeps a closure alive referencing outer scope — duplicate subscriptions on every request without cleanup accumulate until MaxListenersExceededWarning and OOM.

Why interviewers ask

  1. Repeated subscribe — db.on('connect') inside handler instead of once at startup.
  2. Missing cleanup — WebSocket close should removeListener on shared emitter.
  3. setMaxListeners — raise only when legitimately many listeners; not a fix for leaks.
  4. process warnings — MaxListenersExceededWarning signals investigation.
emitter-leak-fix.mjs
import { EventEmitter } from "node:events";

const bus = new EventEmitter();

function subscribeOnce(jobId, handler) {
  const wrapped = (payload) => {
    if (payload.jobId === jobId) {
      bus.off("progress", wrapped);
      handler(payload);
    }
  };
  bus.on("progress", wrapped);
}

// Anti-pattern: every request adds permanent listener
// function bad(req, res) {
//   bus.on("progress", () => res.write("...")); // leak
// }
Question 58IntermediateConcept

What are the basics of the Node.js crypto module?

Direct answer

node:crypto provides hashing (createHash), HMAC, symmetric ciphers, random bytes, and key pairs — use for checksums, signed tokens, and encryption at rest; never roll custom crypto; passwords belong in bcrypt/argon2, not SHA256 alone.

The crypto module wraps OpenSSL — same primitives browsers lack natively in older Node-centric backends. AceDevHub uses it indirectly via JWT libraries and TLS termination.

APIUse case
createHash('sha256')File integrity, cache keys
createHmac('sha256', secret)Webhook signature verification
randomBytes(n)Session IDs, CSRF tokens
createCipheriv / createDecipherivAES-GCM encrypted fields
generateKeyPairAsymmetric keys for signing
  • Timing attacks — compare HMACs with crypto.timingSafeEqual.
  • Salt + slow hash — passwords: bcrypt/scrypt/argon2, not plain SHA256.
  • IV uniqueness — never reuse IV/nonce with same key in GCM mode.
crypto-basics.mjs
import crypto from "node:crypto";

const hash = crypto.createHash("sha256").update("payload").digest("hex");

const secret = process.env.WEBHOOK_SECRET ?? "dev-secret";
const sig = crypto.createHmac("sha256", secret).update(body).digest("hex");

const token = crypto.randomBytes(32).toString("base64url");

// Verify webhook
const ok = crypto.timingSafeEqual(
  Buffer.from(sig, "hex"),
  Buffer.from(expected, "hex")
);
Question 59IntermediatePractical

What is util.promisify in Node.js?

Direct answer

util.promisify wraps callback-style functions (err, result) into functions returning Promises — bridge legacy fs.readFile and similar APIs to async/await; prefer native fs.promises where available.

Node's early APIs use error-first callbacks — promisify automates the (err, data) → Promise conversion so modern code avoids callback pyramids without rewriting C libraries.

  • Signature — promisify(fn) returns async function; last arg must not be user callback.
  • Custom promisify — fn[util.promisify.custom] = () => new Promise(...) for odd APIs.
  • fs.promises — built-in Promise fs API; promisify legacy fs.readFile only when needed.
  • promisify vs promisify.setImmediate — rare; default uses microtask/Promise resolution.
promisify.mjs
import { promisify } from "node:util";
import fs from "node:fs";

const readFile = promisify(fs.readFile);

async function loadConfig(path) {
  const raw = await readFile(path, "utf8");
  return JSON.parse(raw);
}

// Preferred today:
// import fs from "node:fs/promises";
// await fs.readFile(path, "utf8");
Question 60IntermediateConcept

How does async/await work in Node.js?

Direct answer

async functions return Promises; await pauses within the function until a Promise settles — syntax sugar over .then; use try/catch for errors; await serial I/O, Promise.all for independent parallel work; never block the event loop with CPU work inside async.

async/await makes Promise chains readable in route handlers and scripts — but each await still yields to the event loop; it does not create new threads.

Why interviewers ask

PatternWhen
await step1(); await step2();Sequential dependencies
await Promise.all([a(), b()])Independent parallel I/O
for (const x of items) await fn(x)Serial per item — safe for rate limits
Promise.allSettledBatch where partial failure OK
top-level awaitESM scripts — import runners, CLI tools
  • try/catch — catches rejected await; map to HTTP 4xx/5xx in handlers.
  • Floating promises — async IIFE without await at call site → unhandled rejection.
  • Fastify handlers — async (req, reply) => { await service... } — framework catches rejections.
async-await.mjs
async function importTopics(slugs) {
  const results = await Promise.all(
    slugs.map(async (slug) => {
      const topic = await loadTopic(slug);
      await upsertTopic(topic);
      return slug;
    })
  );
  return results;
}

try {
  await importTopics(["javascript-interview-questions", "nodejs-interview-questions"]);
} catch (err) {
  console.error("Import failed:", err.message);
}
Question 61IntermediateConcept

What is the difference between callbacks and Promises in Node.js?

Direct answer

Callbacks (err, result) nest and require manual error propagation at each step; Promises chain with .then/catch or async/await with centralized error handling — modern Node favors Promises and fs.promises, but libuv and streams still expose callback and event APIs underneath.

Callbacks were Node's original async style — Promises (ES2015) and async/await layered on top without removing the event-loop + callback foundation.

AspectCallbacksPromises
Error handlingif (err) at every level.catch / try/catch once
CompositionCallback hell / pyramidChain or Promise.all
CancellationManual flagsAbortSignal (modern)
Node APIsLegacy fs, some cryptofs/promises, fetch, pipeline
  • Error-first convention — (err, data) =>; omitting err check is a classic bug.
  • Callback hell — nested readFile → parse → query → respond.
  • Promisify bridge — Q59 connects legacy to modern style.
callback-vs-promise.mjs
// Callback style
fs.readFile("config.json", "utf8", (err, raw) => {
  if (err) return console.error(err);
  const config = JSON.parse(raw);
  db.connect(config.url, (err2) => {
    if (err2) return console.error(err2);
    // ...
  });
});

// Promise style
const raw = await fs.readFile("config.json", "utf8");
const config = JSON.parse(raw);
await db.connect(config.url);
Question 62IntermediateConcept

What are Node.js error handling best practices?

Direct answer

Distinguish operational errors (expected, handle and respond) from programmer errors (fix code); use try/catch in async handlers, centralized error middleware, structured logs with request IDs, and never leak stack traces to clients in production.

Production APIs need a consistent error contract — AceDevHub uses structured API envelopes so web clients show safe messages while logs retain detail.

Why interviewers ask

  1. Operational — validation fail, 404, DB timeout → return 4xx/5xx, retry if idempotent.
  2. Programmer — null reference, logic bug → fix deploy; may crash process after log.
  3. Custom errors — class AppError extends Error { statusCode, code }.
  4. Central handler — Fastify setErrorHandler; Express 4-arg middleware last.
error-handler.mjs
class AppError extends Error {
  constructor(message, statusCode = 500, code = "INTERNAL") {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
  }
}

fastify.setErrorHandler((err, req, reply) => {
  const status = err.statusCode ?? 500;
  req.log.error({ err, reqId: req.id }, err.message);

  reply.status(status).send({
    error: {
      code: err.code ?? "INTERNAL",
      message: status < 500 ? err.message : "Internal Server Error",
    },
  });
});
Question 63IntermediateConcept

What is the difference between uncaughtException and unhandledRejection?

Direct answer

uncaughtException fires on synchronous throws with no try/catch; unhandledRejection fires when a Promise rejects with no .catch — Node may exit on uncaughtException; treat both as bugs in production, log, graceful shutdown, and use --unhandled-rejections=strict in CI.

These process-level events catch errors that escaped application boundaries — last-resort hooks, not substitutes for handler try/catch.

EventTriggerTypical cause
uncaughtExceptionSync throw outside try/catchJSON.parse in top-level, bug in sync middleware
unhandledRejectionPromise rejected, no handlerMissing await, forgotten .catch on async call
warning (deprecated)Some versions warn before exitFloating promise in background task
  • Do not resume normally — after uncaughtException, process state may be corrupt; shutdown and let PM2/Docker restart.
  • Graceful shutdown — close server, drain connections, then process.exit(1).
  • Framework safety — Fastify catches async route rejections; manual setImmediate async still needs .catch.
process-errors.mjs
process.on("uncaughtException", (err) => {
  console.error("uncaughtException:", err.stack);
  shutdown(1);
});

process.on("unhandledRejection", (reason) => {
  console.error("unhandledRejection:", reason);
  shutdown(1);
});

function shutdown(code) {
  server.close(() => process.exit(code));
  setTimeout(() => process.exit(code), 10_000).unref();
}
Question 64IntermediatePractical

How do you make HTTP client requests in Node.js?

Direct answer

Use global fetch (Node 18+) or http.request/https.request for outbound calls — set timeouts with AbortSignal, check response.ok, parse JSON safely, and reuse connection pooling for high-volume service-to-service traffic.

Servers call other APIs constantly — payment webhooks, OAuth token exchange, internal microservices. Node provides fetch aligned with browser semantics plus lower-level http/https modules for streaming or custom TLS.

APIWhen to use
fetch(url, options)JSON REST, standard headers, AbortSignal timeout
http.request / https.requestFine-grained streaming, legacy integrations
undici (built into fetch)High-perf HTTP/1.1 client in modern Node
Third-party (axios, got)Interceptors, retries — optional in greenfield Fastify apps
  • AbortSignal.timeout(ms) — cancel hung upstream calls.
  • response.ok — fetch does not throw on 404/500; check status explicitly.
  • Credentials — server-to-server uses Authorization header, not browser cookies.
http-client.mjs
const API = process.env.API_URL ?? "http://localhost:4000";

async function getTopic(slug) {
  const res = await fetch(`${API}/interviews/topics/${slug}`, {
    signal: AbortSignal.timeout(5000),
    headers: { Accept: "application/json" },
  });

  if (!res.ok) {
    throw new Error(`Upstream ${res.status}: ${slug}`);
  }

  return res.json();
}

await getTopic("nodejs-interview-questions");
Question 65IntermediateConcept

What is the difference between async and sync file system APIs in Node.js?

Direct answer

Sync fs methods (*Sync) block the event loop until libuv completes disk I/O — OK for CLI startup scripts; async fs.promises or callbacks yield the loop — required on servers handling concurrent requests.

File I/O uses the libuv thread pool (Q8) even for async APIs — but sync variants wait on the main thread, freezing timers, HTTP responses, and WebSocket pings.

Why interviewers ask

APIBlocks event loop?Typical use
fs.readFileSyncYesCLI config at boot, build scripts
fs.promises.readFileNo (main thread)API handlers, import scripts
fs.readFile + callbackNoLegacy codebases
fs.createReadStreamNo (chunked)Large files, content import
  • Server rule — never readFileSync per request on Fastify API.
  • Import scripts — AceDevHub content import runs async fs + JSON parse in tsx scripts.
  • Watch mode — fs.watch for dev reload; production prefers explicit deploy artifacts.
fs-async-sync.mjs
// CLI startup — sync acceptable
import fs from "node:fs";
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));

// Server handler — async only
import fsp from "node:fs/promises";

export async function loadFixture(name) {
  const raw = await fsp.readFile(`fixtures/${name}.json`, "utf8");
  return JSON.parse(raw);
}
Question 66IntermediatePractical

How do you prevent path traversal attacks in Node.js?

Direct answer

Never pass user input directly to fs paths — resolve with path.resolve, ensure result starts with allowed base directory, reject .. segments, use path.basename for filenames, and serve static files from a fixed root.

Path traversal exploits ../ in filenames to read /etc/passwd or .env — any endpoint that reads or writes files from user-supplied names must validate paths.

  1. Fixed base — const base = path.resolve('./uploads').
  2. Resolve + check — target = path.resolve(base, userInput); if (!target.startsWith(base)) reject.
  3. basename only — path.basename(userInput) strips directory components.
  4. Allowlist extensions — .json, .png only for public asset endpoints.
path-security.mjs
import path from "node:path";
import fsp from "node:fs/promises";

const CONTENT_ROOT = path.resolve("data/interviews");

function safeInterviewPath(topicSlug) {
  const normalized = path.basename(topicSlug);
  const target = path.resolve(CONTENT_ROOT, normalized, "questions.json");

  if (!target.startsWith(CONTENT_ROOT + path.sep)) {
    throw new Error("Invalid path");
  }

  return target;
}

// Attacker input: "../../apps/api/.env" → basename strips or resolve check fails
Question 67IntermediateConcept

What are REST API basics in Node.js?

Direct answer

REST maps resources to URLs and HTTP verbs — GET read, POST create, PUT/PATCH update, DELETE remove; use nouns (/interviews/topics), status codes, JSON bodies, pagination query params, and idempotent design; implement with Fastify routes and validation schemas.

AceDevHub's API exposes resource-oriented endpoints — GET /interviews/topics/:slug, structured envelopes, consistent errors — not RPC-style action URLs for core CRUD.

VerbActionExample
GETRead collection or itemGET /interviews/topics/nodejs-interview-questions
POSTCreatePOST /courses/waitlist
PUT/PATCHUpdatePATCH /admin/courses/:id
DELETERemoveDELETE /sessions/:id
201 + LocationCreated resourcePOST returns new id
  • Status codes — 200 OK, 201 Created, 400 validation, 401 auth, 404 missing, 409 conflict, 500 server.
  • Pagination — ?page=&limit= (Q30); Link header or meta in envelope.
  • Idempotency — PUT same body twice same result; POST creates duplicates without Idempotency-Key.
  • Validation — Fastify JSON schema on body/query/params before service layer.
rest-routes.mjs
fastify.get("/interviews/topics/:slug", {
  schema: {
    params: { type: "object", properties: { slug: { type: "string" } }, required: ["slug"] },
  },
}, async (req, reply) => {
  const topic = await interviewsService.getTopicPage(req.params.slug);
  if (!topic) return reply.code(404).send({ error: { code: "NOT_FOUND" } });
  return reply.send({ data: { page: topic } });
});
3

Advanced Node.js Interview Questions

Question 68AdvancedConcept

What is the difference between worker threads and cluster in Node.js?

Direct answer

Cluster forks multiple Node processes sharing the same server port for CPU parallelism on I/O-bound HTTP; worker_threads run JavaScript in threads within one process for CPU-heavy tasks — cluster scales connections, workers scale compute without full process overhead.

Node's default single main thread handles the event loop — cluster and worker_threads are the two main ways to use multiple cores without blocking request handling.

Why interviewers ask

Aspectcluster moduleworker_threads
UnitSeparate OS processesThreads in one process
MemoryIsolated heap per workerShared process, message passing
Best forHTTP server throughputCPU-bound JS (hashing, parsing, ML)
Port sharingPrimary distributes to workersN/A — not for HTTP listen directly
Crash isolationOne worker dies, others continueThread error can affect process
  • When cluster — multi-core API server behind load balancer or cluster.fork on bare metal.
  • When worker_threads — image resize, bcrypt rounds, large JSON transform off main loop.
  • Not for I/O — extra threads don't speed async pg/redis; event loop already non-blocking.
cluster-vs-worker.mjs
import cluster from "node:cluster";
import { availableParallelism } from "node:os";

if (cluster.isPrimary) {
  for (let i = 0; i < availableParallelism(); i++) cluster.fork();
} else {
  // Each worker runs full Fastify app on shared port
  startServer({ port: 4000 });
}

// CPU task: offload to worker_threads, not cluster
Question 69AdvancedPractical

How do worker threads work in Node.js?

Direct answer

worker_threads spawn isolated V8 isolates with message passing via postMessage and SharedArrayBuffer for shared memory — import worker from worker file path, listen for message/error/exit, terminate when done.

Worker threads run JavaScript in parallel without blocking the main event loop — each worker has its own event loop but shares process resources more lightly than cluster forks.

  • new Worker('./task.js') — worker file or eval string with workerData option.
  • postMessage / parentPort — structured clone transfer; Transferable for ArrayBuffer.
  • SharedArrayBuffer — low-latency shared memory; requires careful synchronization.
  • Pool pattern — reuse workers for repeated jobs; avoid spawn cost per request.
worker-thread.mjs
import { Worker } from "node:worker_threads";

function runWorker(data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker("./hash-worker.mjs", { workerData: data });
    worker.on("message", resolve);
    worker.on("error", reject);
    worker.on("exit", (code) => {
      if (code !== 0) reject(new Error(`Worker stopped with ${code}`));
    });
  });
}

// hash-worker.mjs: parentPort.postMessage(hashResult)
Question 70AdvancedConcept

What is the child_process module in Node.js?

Direct answer

child_process spawns separate OS processes — run shell commands, CLI tools, or other Node scripts; communicate via stdin/stdout/stderr pipes or IPC; used by AceDevHub compiler runner to execute user code in isolated processes.

Unlike worker_threads (threads), child_process creates full OS processes — stronger isolation for untrusted code, native binaries, and language runtimes (python, javac).

MethodUse case
spawnStream I/O, long-running processes, large output
execBuffer entire stdout/stderr, shell one-liners
execFileDirect executable, no shell, safer args
forkNode script with IPC channel (cluster uses this)
  • stdio pipes — 'pipe', 'inherit', or custom stream for stdin/out/err.
  • Exit codes — code 0 success; non-zero error; listen on 'close' event.
  • Security — never pass unsanitized user input to shell; prefer execFile with arg array.
child-process.mjs
import { spawn } from "node:child_process";

const child = spawn("node", ["runner.mjs"], {
  stdio: ["pipe", "pipe", "pipe"],
  env: { ...process.env, NODE_ENV: "sandbox" },
});

child.stdin.write(userCode);
child.stdin.end();

child.stdout.on("data", (chunk) => process.stdout.write(chunk));
child.on("close", (code) => console.log("exit", code));
Question 71AdvancedConcept

What is the difference between spawn and exec in Node.js?

Direct answer

spawn streams stdout/stderr as data events — safe for large or long output; exec buffers entire output in memory via shell — fine for short commands but risks maxBuffer exceeded and shell injection if input is tainted.

Choosing spawn vs exec is a memory and security decision — interviews test whether you know exec runs through /bin/sh by default.

spawnexec
OutputStreamed chunksBuffered string (maxBuffer default 1MB)
ShellNo shell by defaultShell interprets command string
ArgsArray: spawn('node', ['a.js'])Single string: exec('node a.js')
Use whenLogs, compilers, tail -fQuick git rev-parse, small stdout
  • execFile — middle ground: no shell, buffered output, explicit args.
  • maxBuffer — exec throws ERR_CHILD_PROCESS_STDIO_MAXBUFFER if output too large.
  • Shell injection — exec(`grep ${userInput}`) dangerous; use spawn with args array.
spawn-vs-exec.mjs
import { spawn, execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

// Large output — spawn
const child = spawn("npm", ["test"], { stdio: "inherit" });

// Small trusted output — execFile
const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"]);
console.log(stdout.trim());
Question 72AdvancedConcept

What causes memory leaks in Node.js applications?

Direct answer

Common leaks: global caches without eviction, orphaned event listeners, closures holding request scope, timers never cleared, unbounded in-memory queues — detect with heap snapshots, process.memoryUsage, and clinic/Chrome DevTools; fix by removing listeners, TTL caches, and streaming large data.

Node processes are long-lived — small per-request leaks compound into OOM kills under PM2/Docker restart loops. Production APIs need observability on heap growth.

Why interviewers ask

CauseExampleFix
Global array/map growthcache[key] = res without TTLLRU cache with max size
Event listenersbus.on in every request (Q57)off/once, scoped emitters
ClosuresHandler captures huge req.body foreverClear refs after response
TimerssetInterval never clearedclearInterval on shutdown
Detached buffersAccumulating chunks without endUse pipeline, destroy streams
  • process.memoryUsage() — rss, heapUsed, external — log periodically in staging.
  • Heap snapshot — node --inspect, compare snapshots before/after load test.
  • WeakRef / WeakMap — optional for caches when GC should reclaim entries.
memory-leak-fix.mjs
import { LRUCache } from "lru-cache";

const topicCache = new LRUCache({
  max: 500,
  ttl: 1000 * 60 * 10, // 10 min
});

function getCachedTopic(slug) {
  if (topicCache.has(slug)) return topicCache.get(slug);
  const topic = loadTopic(slug);
  topicCache.set(slug, topic);
  return topic;
}

// Anti-pattern: globalLeaks.push(everyResponsePayload)
Question 73AdvancedPractical

How do you implement graceful shutdown in Node.js?

Direct answer

On SIGTERM/SIGINT stop accepting connections with server.close(), finish in-flight requests, close DB/Redis pools and queue workers, set a force-exit timeout, then process.exit — Kubernetes and Docker send SIGTERM on deploy.

Abrupt process.kill mid-request causes 502s and half-open DB transactions — graceful shutdown drains work before exit during rolling deploys on AceDevHub API.

Why interviewers ask

  1. Signal handlers — process.on('SIGTERM', shutdown); same for SIGINT locally.
  2. Stop listening — server.close() — no new connections; existing finish.
  3. Close resources — await pool.end(), redis.quit(), BullMQ worker.close().
  4. Force timeout — setTimeout(() => process.exit(1), 30_000).unref() as last resort.
graceful-shutdown.mjs
let shuttingDown = false;

async function shutdown(signal) {
  if (shuttingDown) return;
  shuttingDown = true;
  console.log(`${signal} received — draining`);

  await new Promise((resolve) => server.close(resolve));
  await pgPool.end();
  await redis.quit();
  process.exit(0);
}

process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
Question 74AdvancedConcept

How do you handle CPU-intensive tasks in Node.js?

Direct answer

Never run long synchronous CPU work on the main thread — offload to worker_threads, child processes, or a job queue (BullMQ worker); split work into chunks with setImmediate for minor tasks; scale horizontally for sustained CPU load.

The event loop is single-threaded — a 2-second JSON.stringify loop blocks all HTTP responses, health checks, and WebSocket pings until it finishes.

ApproachWhen
worker_threadsCPU-bound JS in same app — hashing, parsing
child_processIsolated/untrusted code, native CLIs
BullMQ workerAsync jobs off API hot path — apps/worker
setImmediate chunkingYield during long sync loops — last resort
Horizontal scaleMore containers, not bigger sync blocks
  • Anti-pattern — bcrypt.sync, heavy regex on megabyte strings in route handler.
  • libuv thread pool — fs/crypto DNS use threads but still limited pool (Q8).
  • Measure — event loop delay (perf_hooks, clinic.js) spikes when CPU blocked.
cpu-offload.mjs
import { Worker } from "node:worker_threads";

async function hashPassword(password) {
  return new Promise((resolve, reject) => {
    const w = new Worker("./bcrypt-worker.mjs", { workerData: { password } });
    w.on("message", resolve);
    w.on("error", reject);
  });
}

// Or enqueue: await emailQueue.add("send-cert", { userId });
Question 75AdvancedConcept

How does the Node.js cluster module work?

Direct answer

cluster.isPrimary forks worker processes (cluster.fork) that share server ports via OS scheduling — primary can respawn dead workers; each worker runs full Node app; alternative to multiple Docker replicas on single host.

cluster uses child_process.fork with IPC — workers are full V8 isolates, not threads. Primary distributes incoming connections (round-robin on most platforms).

  • isPrimary / isWorker — branch entry: fork N workers or start server.
  • Worker count — os.availableParallelism() or CPU cores; match container limit.
  • Respawn — cluster.on('exit', () => cluster.fork()) for crash recovery.
  • Sticky sessions — WebSocket/stateful apps may need session affinity or Redis pub/sub.
cluster.mjs
import cluster from "node:cluster";
import process from "node:process";

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid}`);
  for (let i = 0; i < 4; i++) cluster.fork();

  cluster.on("exit", (worker) => {
    console.log(`Worker ${worker.process.pid} died — restarting`);
    cluster.fork();
  });
} else {
  await startFastify({ port: 4000 });
  console.log(`Worker ${process.pid} listening`);
}
Question 76AdvancedConcept

How do you monitor Node.js application performance?

Direct answer

Track event loop lag, request latency p95/p99, error rates, memory/CPU, and DB pool saturation — use structured logs (pino), health endpoints, process.memoryUsage, and APM tools; alert on SLO breaches not just crashes.

Production observability spans logs, metrics, and traces — Node-specific signals include event loop delay and GC pauses that generic server metrics miss.

SignalTool / APIIndicates
Request durationFastify hooks + logSlow routes, N+1 queries
Event loop delayperf_hooks.monitorEventLoopDelayCPU block, sync fs
Memoryprocess.memoryUsage()Leaks (Q72), cache too large
ErrorssetErrorHandler + counter5xx spike, bad deploy
External depspg pool waitingCountDB bottleneck
  • /health — liveness (process up) vs readiness (DB connected).
  • Request ID — correlate logs across API → worker → DB.
  • Load testing — k6/autocannon before launch; watch p99 not just avg.
perf-monitor.mjs
import { monitorEventLoopDelay } from "node:perf_hooks";

const delay = monitorEventLoopDelay({ resolution: 20 });
delay.enable();

setInterval(() => {
  const mem = process.memoryUsage();
  console.log(JSON.stringify({
    heapUsedMb: Math.round(mem.heapUsed / 1024 / 1024),
    eventLoopP99Ms: delay.percentile(99) / 1e6,
  }));
  delay.reset();
}, 60_000).unref();
Question 77AdvancedConcept

What is PM2 and how is it used with Node.js?

Direct answer

PM2 is a production process manager — cluster mode, auto-restart on crash, log aggregation, startup scripts on boot, zero-downtime reload; alternative to systemd + Docker for bare-metal Node deploys.

PM2 wraps process supervision — keep Node alive, restart OOM kills, fork workers across cores — common on VPS before full container orchestration.

CommandPurpose
pm2 start app.mjs -i maxCluster mode — one worker per CPU
pm2 reload apiZero-downtime reload (graceful)
pm2 logs / monitTail logs, CPU/memory dashboard
pm2 startup + saveResurrect processes after reboot
ecosystem.config.cjsDeclarative multi-app config
  • vs Docker — AceDevHub target is Docker Compose on Hetzner; PM2 still asked in legacy VPS interviews.
  • Graceful reload — PM2 sends shutdown signal before killing old workers (Q73).
  • Log rotation — pm2-logrotate module; prefer stdout → Docker logging driver in containers.
ecosystem.config.cjs
module.exports = {
  apps: [
    {
      name: "acedevhub-api",
      script: "./apps/api/dist/server.mjs",
      instances: "max",
      exec_mode: "cluster",
      env: { NODE_ENV: "production", PORT: 4000 },
      max_memory_restart: "512M",
    },
  ],
};
Question 78AdvancedConcept

What are Node.js security best practices?

Direct answer

Validate all input, parameterize SQL, sanitize paths, use helmet headers, rate-limit auth routes, store secrets in env not code, keep dependencies patched (npm audit), httpOnly cookies for sessions, and enforce auth/entitlements on the server — never trust the client.

Node APIs face OWASP-class risks — injection, broken auth, SSRF, prototype pollution — AceDevHub enforces access in Fastify handlers, not just frontend gates.

Why interviewers ask

RiskMitigation
SQL injectionParameterized queries via pg ($1, $2)
XSSEncode output; CSP headers; sanitize HTML
Path traversalResolve + base check (Q66)
Secret leak.env gitignored; rotate keys
Dependency CVEnpm audit, lockfile, Dependabot
Broken access controlEntitlements check on every gated route
  • Least privilege — DB user read-only where possible; admin routes require role.
  • Error responses — no stack traces or internal paths to clients (Q62).
  • Prototype pollution — avoid merging untrusted objects into {} without validation.
security-handler.mjs
// Parameterized query — never string concat
await pool.query(
  "SELECT * FROM interview_topics WHERE slug = $1",
  [slug]
);

// Server-side entitlement check
if (!await entitlementsService.hasPremium(userId)) {
  throw new AppError("Premium required", 403, "FORBIDDEN");
}
Question 79AdvancedPractical

How do helmet, CORS, and rate limiting work in Node.js?

Direct answer

helmet sets secure HTTP headers (CSP, HSTS, X-Frame-Options); CORS controls browser cross-origin access (Q47); rate limiting caps requests per IP/key — combine all three on public APIs, stricter limits on login and waitlist POST.

These are defense-in-depth layers — headers reduce browser attack surface, CORS scopes frontend origins, rate limits slow brute force and scraping.

ToolProtects against
helmetClickjacking, MIME sniffing, missing HSTS
@fastify/corsUnauthorized browser origins reading API
@fastify/rate-limitBrute force, DoS, scraper abuse
CombinedPublic /courses/waitlist + /auth/google callback
  • helmet in Fastify — @fastify/helmet plugin; tune CSP for Next.js inline needs.
  • Rate limit keys — IP for anonymous; userId for authenticated routes.
  • 429 response — Retry-After header; log blocked IPs for abuse patterns.
security-middleware.mjs
await fastify.register(import("@fastify/helmet"));
await fastify.register(import("@fastify/cors"), {
  origin: ["https://acedevhub.com"],
  credentials: true,
});
await fastify.register(import("@fastify/rate-limit"), {
  max: 100,
  timeWindow: "1 minute",
});

fastify.post("/auth/login", {
  config: { rateLimit: { max: 5, timeWindow: "15 minutes" } },
}, loginHandler);
Question 80AdvancedConcept

What are JWT authentication basics in Node.js?

Direct answer

JWT is a signed token (header.payload.signature) proving identity — AceDevHub uses httpOnly cookies not localStorage; verify signature and expiry server-side; short-lived access + refresh rotation; Google OAuth establishes session via backend-issued JWT.

JWTs are stateless claims — but production auth still needs session revocation tables, refresh token storage, and server-side entitlement checks beyond the token payload.

Why interviewers ask

TopicPractice
StoragehttpOnly Secure SameSite cookie — not localStorage
VerificationCheck alg, signature, exp, iss, aud
Payloadsub/userId only — no premium boolean; use entitlements
RefreshRotate refresh tokens; revoke on logout all sessions
Google OAuthBackend exchanges code → creates session JWT
  • None algorithm attack — reject alg:none; allowlist HS256/RS256.
  • Secret management — JWT_SECRET in env; rotate with overlap window.
  • Frontend rule — credentials: 'include' on fetch; AceDevHub lib/api/client.ts pattern.
jwt-verify.mjs
import jwt from "jsonwebtoken";

function verifyAccessToken(token) {
  return jwt.verify(token, process.env.JWT_SECRET, {
    algorithms: ["HS256"],
    issuer: "acedevhub-api",
    maxAge: "15m",
  });
}

// Set cookie on login
reply.setCookie("access_token", accessJwt, {
  httpOnly: true,
  secure: process.env.NODE_ENV === "production",
  sameSite: "lax",
  path: "/",
});
Question 81AdvancedPractical

How do you test Node.js applications with Jest?

Direct answer

Jest runs unit and integration tests — describe/it, expect matchers, mock modules with jest.mock, test async with async/await; use supertest or inject() for HTTP; separate test DB; run in CI with NODE_ENV=test.

Testing Node services focuses on pure functions, handlers, and repositories — mock external I/O (pg, Redis, fetch) for fast unit tests; integration tests hit real Docker Postgres on port 5433.

LayerWhat to testTool
UnitService logic, mappersJest + mocks
HTTPRoute status/bodyFastify inject() or supertest
IntegrationSQL repositoriesTest DB + transactions rollback
E2EFull flowsSeparate suite; slower CI job
  • jest.mock('pg') — stub pool.query return values.
  • beforeEach/afterAll — reset mocks; close server and pool.
  • Coverage — CI gate on critical modules; not 100% everywhere.
jest-test.mjs
import { describe, it, expect, jest } from "@jest/globals";

describe("interviewsService", () => {
  it("returns topic page for valid slug", async () => {
    jest.spyOn(repo, "findTopicBySlug").mockResolvedValue({ slug: "nodejs-interview-questions" });

    const result = await service.getTopicPage("nodejs-interview-questions");
    expect(result.slug).toBe("nodejs-interview-questions");
  });
});

// HTTP: const res = await app.inject({ method: "GET", url: "/health" });
Question 82AdvancedConcept

What are Docker basics for Node.js applications?

Direct answer

Docker packages Node apps in images — Dockerfile multi-stage build (install → build → slim runtime), docker-compose for api/web/postgres/redis, .dockerignore node_modules, non-root USER, healthcheck on /health; AceDevHub local stack uses ports 4000/3000/5433/6379.

Containers give reproducible environments — same Node version, same env vars, same service topology from laptop to Hetzner production.

  • Multi-stage Dockerfile — builder installs deps + compiles TS; runtime copies dist only.
  • docker-compose.yml — services: api, web, worker, postgres, redis; profiles for caddy.
  • Volumes — postgres data persisted; bind mount source only in dev.
  • Networking — web calls http://api:4000 inside compose network.
Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build --workspace @acedevhub/api

FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/apps/api/dist ./dist
USER node
EXPOSE 4000
HEALTHCHECK CMD wget -qO- http://localhost:4000/health || exit 1
CMD ["node", "dist/server.mjs"]
Question 83AdvancedConcept

What are MongoDB and Mongoose basics in Node.js?

Direct answer

MongoDB is a document NoSQL database storing BSON JSON-like docs; Mongoose is an ODM adding schemas, validation, and query helpers — connect with mongoose.connect(), define Schema/Model, use find/create/update; AceDevHub uses PostgreSQL + raw SQL instead for relational entitlements and audit.

MongoDB fits flexible document shapes and horizontal scaling — interviews still test Mongoose even when your stack uses SQL; know the mapping to AceDevHub's pg approach.

ConceptMongooseAceDevHub (pg)
Connectmongoose.connect(uri)new Pool({ connectionString })
Schemanew Schema({ title: String })SQL migrations + tables
QueryModel.find({ slug })pool.query('SELECT ... WHERE slug = $1')
Relationspopulate() referencesJOINs + foreign keys
ValidationSchema validatorsZod + DB constraints
  • Collections vs tables — documents in collections; no enforced schema unless Mongoose.
  • _id — ObjectId primary key; default on insert.
  • When Mongo — rapid prototyping, nested docs, content catalogs; when SQL — payments, entitlements, joins.
mongoose-basics.mjs
import mongoose from "mongoose";

const topicSchema = new mongoose.Schema({
  slug: { type: String, required: true, unique: true },
  title: String,
  questionCount: Number,
});

const Topic = mongoose.model("Topic", topicSchema);

await mongoose.connect(process.env.MONGODB_URI);
const topic = await Topic.findOne({ slug: "nodejs-interview-questions" });
Question 84AdvancedConcept

What is the difference between SQL and NoSQL in Node.js backends?

Direct answer

SQL databases (PostgreSQL) use structured tables, schemas, and JOINs with ACID transactions; NoSQL (MongoDB, Redis) favors flexible documents or key-value with scale-out patterns — choose SQL when relations, consistency, and audit matter; NoSQL for cache, sessions, or flexible docs.

Node connects to both via drivers — pg for PostgreSQL, mongoose/mongodb driver, ioredis for Redis — the choice is data model and consistency requirements, not Node itself.

Why interviewers ask

SQL (PostgreSQL)NoSQL (MongoDB / Redis)
ModelTables, rows, FKsDocuments / key-value
SchemaMigrations enforce shapeFlexible or schema-less
QueriesSQL JOINsEmbed, populate, or denormalize
TransactionsMulti-row ACIDVaries; Redis single-key atomic
AceDevHub usePrimary business dataRedis queues, cache, sessions
  • Pick SQL — entitlements, orders, user roles, audit history (AceDevHub).
  • Pick Redis — BullMQ job queues, rate limit counters, session cache.
  • Pick Mongo — nested content trees when JOIN cost hurts; less ideal for financial consistency.
sql-vs-nosql.mjs
// SQL — parameterized join
const { rows } = await pool.query(
  `SELECT u.email, e.type
   FROM users u
   JOIN entitlements e ON e.user_id = u.id
   WHERE u.id = $1`,
  [userId]
);

// Redis — cache or queue
await redis.set(`topic:${slug}`, JSON.stringify(topic), "EX", 300);
await queue.add("send-email", { to, template });
Question 85AdvancedCode Output

What is the output order of this Node.js event loop code?

Direct answer

Output order is 1, 5, 4, 3, 2 — synchronous code first, then process.nextTick queue, then Promise microtasks, then setTimeout timers phase.

This classic snippet tests event loop phase priority — ties directly to Q4–6 on the loop, nextTick, and timers.

event-loop-order.mjs
console.log("1");

setTimeout(() => console.log("2"), 0);

Promise.resolve().then(() => console.log("3"));

process.nextTick(() => console.log("4"));

console.log("5");
Output
1
5
4
3
2
  1. Sync — 1 and 5 run immediately in current tick.
  2. nextTick queue — 4 drains before other microtasks (Node-specific, highest priority).
  3. Microtasks — Promise.then → 3.
  4. Timers phase — setTimeout callback → 2.
Question 86AdvancedCode Output

What is the output of nextTick vs setImmediate in Node.js?

Direct answer

Outside I/O: sync, nextTick, setImmediate — nextTick runs before the event loop continues to the check phase where setImmediate fires; inside an I/O callback, setImmediate often runs before setTimeout(0).

Two snippets cover top-level vs I/O callback context — interviewers may ask either variant (Q6, Q7).

nexttick-setimmediate-top.mjs
console.log("sync");

process.nextTick(() => console.log("nextTick"));
setImmediate(() => console.log("setImmediate"));

setTimeout(() => console.log("timeout"), 0);
Output
sync
nextTick
timeout
setImmediate

Note: timeout vs setImmediate order can vary slightly by Node version at top level — inside fs.readFile callback, setImmediate always runs before setTimeout(0).

nexttick-setimmediate-io.mjs
import fs from "node:fs";

fs.readFile(import.meta.url, () => {
  console.log("readFile callback");
  process.nextTick(() => console.log("nextTick"));
  setImmediate(() => console.log("setImmediate"));
  setTimeout(() => console.log("timeout"), 0);
});

console.log("sync");
Output
sync
readFile callback
nextTick
setImmediate
timeout
Question 87AdvancedCode Output

What is the output of this require cache example?

Direct answer

Output: module run, 1, 2, true — the module body runs once; both requires share the same exports object; increment mutates shared state (Q36).

This tests require.cache singleton behavior — a frequent follow-up to module caching and exports questions.

require-cache
// counter.cjs
console.log("module run");
let count = 0;
module.exports = {
  inc() { return ++count; },
};

// main.cjs
const a = require("./counter.cjs");
const b = require("./counter.cjs");

console.log(a.inc());
console.log(b.inc());
console.log(a === b);
Output
module run
1
2
true
  • module run once — second require hits cache; body does not re-execute.
  • 1 then 2 — shared count variable across both references.
  • true — a and b reference identical exports object.
Question 88AdvancedCode Output

What happens in this stream backpressure example?

Direct answer

Output: start, chunk: a, chunk: b, end, done — pipe connects readable to writable; chunks flow asynchronously after sync code; 'done' logs last because end fires after stream finishes (Q53).

Tracing pipe + data events shows when sync code runs vs when stream callbacks schedule on the event loop.

stream-trace.mjs
import { Readable, Writable } from "node:stream";

const readable = Readable.from(["a", "b"]);

const writable = new Writable({
  write(chunk, _enc, cb) {
    console.log("chunk:", chunk.toString());
    cb();
  },
});

console.log("start");

readable.pipe(writable);

readable.on("end", () => console.log("end"));
writable.on("finish", () => console.log("done"));

console.log("sync after pipe");
Output
start
sync after pipe
chunk: a
chunk: b
end
done
  • Sync first — start and sync after pipe before any chunk.
  • Chunks async — write callbacks run on subsequent ticks.
  • finish vs end — readable end then writable finish after all writes complete.
Question 89AdvancedCode Output

What is the output of nested Promise microtasks?

Direct answer

Output: 1, 5, 2, 3, 4 — sync first; microtasks drain fully (including nested .then) before macrotasks; each Promise.then queues one microtask.

promise-microtasks.mjs
console.log("1");

Promise.resolve().then(() => console.log("2"));

Promise.resolve().then(() => {
  console.log("3");
  Promise.resolve().then(() => console.log("4"));
});

setTimeout(() => console.log("timeout"), 0);

console.log("5");
Output
1
5
2
3
4
timeout

The microtask queue drains completely before the timers phase — nested Promise in the second .then schedules 4 before timeout runs.

  1. Sync — 1, 5.
  2. First microtask batch — 2 from first Promise; 3 from second.
  3. Nested microtask — 4 queued during 3's callback, runs before leaving microtask checkpoint.
  4. Macrotask — timeout last.
Question 90AdvancedScenario

How would you design a REST API for interview topics in Express?

Direct answer

Mount /interviews/topics router with GET list (pagination query), GET :slug detail, validate params, thin controllers calling service layer, JSON errors, express.json middleware, and 404/500 handlers — map same design to Fastify for AceDevHub production.

Scenario: build read-only public API for interview topic pages — mirrors GET /interviews/topics/:slug in AceDevHub.

  1. Routes — GET /interviews/topics, GET /interviews/topics/:slug.
  2. Middleware stack — cors, helmet, rate-limit, express.json (Q42–48).
  3. Service layer — interviewsService.getTopicPage(slug) with pg query.
  4. Errors — 404 unknown slug; 500 with generic message.
scenario-rest-api.mjs
import express from "express";

const app = express();
app.use(express.json());

const router = express.Router();

router.get("/", async (req, res, next) => {
  try {
    const page = Number(req.query.page ?? 1);
    const topics = await interviewsService.listTopics({ page, limit: 20 });
    res.json({ data: topics });
  } catch (err) { next(err); }
});

router.get("/:slug", async (req, res, next) => {
  try {
    const topic = await interviewsService.getTopicPage(req.params.slug);
    if (!topic) return res.status(404).json({ error: { code: "NOT_FOUND" } });
    res.json({ data: { page: topic } });
  } catch (err) { next(err); }
});

app.use("/interviews/topics", router);
Question 91AdvancedScenario

How would you handle large file uploads with streams in Node.js?

Direct answer

Pipe req (IncomingMessage) through size-limited transform or busboy/multer to fs.createWriteStream — never buffer entire file in memory; validate MIME/extension, scan path, return 413 if over limit; use pipeline for error cleanup.

Scenario: accept 100MB CSV import without OOM — stream from socket to disk or S3-compatible storage.

  • Streaming write — req.pipe(writeStream) or pipeline(req, limiter, writeStream).
  • Size cap — count bytes in Transform; destroy stream with 413 if exceeded.
  • Security — random filename, path.resolve check (Q66), virus scan async job.
  • Progress — optional job queue for post-upload processing via BullMQ.
scenario-upload-stream.mjs
import { pipeline } from "node:stream/promises";
import fs from "node:fs";
import { Transform } from "node:stream";

const MAX = 100 * 1024 * 1024;

function sizeLimiter(max) {
  let bytes = 0;
  return new Transform({
    transform(chunk, _enc, cb) {
      bytes += chunk.length;
      if (bytes > max) cb(new Error("FILE_TOO_LARGE"));
      else cb(null, chunk);
    },
  });
}

async function handleUpload(req, destPath) {
  await pipeline(
    req,
    sizeLimiter(MAX),
    fs.createWriteStream(destPath)
  );
}
Question 92AdvancedScenario

How would you implement graceful shutdown for a production Node.js server?

Direct answer

On SIGTERM: stop readiness, server.close() to drain HTTP, await in-flight requests with timeout, close pg pool and Redis, stop BullMQ worker, force exit after deadline — coordinate with K8s preStop and load balancer health checks.

Scenario: deploy new AceDevHub API version without dropping active requests — Docker sends SIGTERM; you have ~30s termination grace.

  1. Signal trap — SIGTERM + SIGINT handlers (Q73).
  2. Stop accepting — server.close(); mark /health/ready false.
  3. Drain work — track activeRequests counter; wait or timeout.
  4. Close deps — pool.end(), redis.quit(), worker.close().
  5. Force exit — process.exit(1) after 30s if stuck.
scenario-shutdown.mjs
let active = 0;
let shuttingDown = false;

fastify.addHook("onRequest", async () => { active += 1; });
fastify.addHook("onResponse", async () => { active -= 1; });

async function shutdown(signal) {
  if (shuttingDown) return;
  shuttingDown = true;
  fastify.log.info(`${signal} — shutting down (${active} active)`);

  await fastify.close();
  while (active > 0) await new Promise((r) => setTimeout(r, 100));

  await pool.end();
  await redis.quit();
  process.exit(0);
}

process.on("SIGTERM", () => shutdown("SIGTERM"));
Question 93AdvancedScenario

How would you offload a CPU-intensive task using worker threads?

Direct answer

Spawn a worker pool with worker_threads, post job data via workerData/postMessage, return result to main thread, terminate or reuse workers — keep HTTP handler async awaiting worker result without blocking the event loop.

Scenario: hash 10k passwords or parse a huge JSON export without freezing the AceDevHub API for other users (Q68–69, Q74).

  1. Worker file — isolate CPU logic in hash-worker.mjs.
  2. Pool — fixed N workers; queue jobs in main thread.
  3. Timeout — worker.terminate() if job exceeds SLA.
  4. Fallback — BullMQ job for very long tasks instead of inline worker.
scenario-worker-pool.mjs
import { Worker } from "node:worker_threads";

function runCpuJob(payload) {
  return new Promise((resolve, reject) => {
    const worker = new Worker("./cpu-worker.mjs", { workerData: payload });
    const timer = setTimeout(() => {
      worker.terminate();
      reject(new Error("WORKER_TIMEOUT"));
    }, 30_000);

    worker.on("message", (result) => { clearTimeout(timer); resolve(result); });
    worker.on("error", reject);
  });
}

// Route: const hash = await runCpuJob({ password });
Question 94AdvancedScenario

How would you implement JWT auth middleware in Express?

Direct answer

Read token from httpOnly cookie or Authorization header, verify with jwt.verify and allowed algorithms, attach req.user, call next() or return 401 — protect routes with middleware; refresh tokens via separate endpoint; never store JWT in localStorage.

Scenario: protect /dashboard and /account routes — mirrors AceDevHub Google OAuth session with httpOnly cookies (Q80).

  • Extract — req.cookies.access_token or Bearer header.
  • Verify — algorithms allowlist, exp, iss check.
  • Attach — req.user = { id: payload.sub }.
  • Optional auth — separate middleware that continues without user if no token.
scenario-jwt-middleware.mjs
import jwt from "jsonwebtoken";

function requireAuth(req, res, next) {
  const token = req.cookies?.access_token
    ?? req.headers.authorization?.replace(/^Bearer /, "");

  if (!token) return res.status(401).json({ error: { code: "UNAUTHORIZED" } });

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ["HS256"],
      issuer: "acedevhub-api",
    });
    req.user = { id: payload.sub };
    next();
  } catch {
    return res.status(401).json({ error: { code: "INVALID_TOKEN" } });
  }
}

app.get("/auth/me", requireAuth, (req, res) => res.json({ data: req.user }));
Question 95AdvancedScenario

How would you implement centralized error handling middleware?

Direct answer

Use four-argument Express error middleware or Fastify setErrorHandler last — map AppError to status/code, log stack server-side, return safe JSON envelope, handle async errors via next(err) or framework auto-catch.

Scenario: consistent { error: { code, message } } responses across all AceDevHub API routes (Q62).

  1. AppError class — statusCode, code, isOperational flag.
  2. Throw in services — throw new AppError('Not found', 404).
  3. Handler last — after all routes; four args (err, req, res, next).
  4. Unknown errors — log full stack; respond 500 generic message.
scenario-error-middleware.mjs
app.use((err, req, res, next) => {
  const status = err.statusCode ?? 500;
  const code = err.code ?? "INTERNAL";

  req.log?.error({ err, url: req.url }, err.message);

  res.status(status).json({
    error: {
      code,
      message: status < 500 ? err.message : "Internal Server Error",
    },
  });
});

// Async route
app.get("/x", async (req, res, next) => {
  try {
    await service.run();
    res.json({ ok: true });
  } catch (err) { next(err); }
});
Question 96AdvancedScenario

How would you validate environment configuration at startup?

Direct answer

Parse process.env with Zod or envalid schema at boot — fail fast with clear missing var message; export typed config object; commit .env.example; never read process.env scattered across modules.

Scenario: API must not start with missing DATABASE_URL or JWT_SECRET — catch misconfiguration before accepting traffic (Q48).

  • Single config module — apps/api/src/config.ts imported first.
  • Zod schema — z.string().url(), z.coerce.number(), enums for NODE_ENV.
  • Fail fast — process.exit(1) with formatted Zod error.
  • No secrets in logs — log which keys missing, not their values.
scenario-env-validation.mjs
import { z } from "zod";

const envSchema = z.object({
  NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
  PORT: z.coerce.number().default(4000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  REDIS_URL: z.string().url().optional(),
});

const parsed = envSchema.safeParse(process.env);

if (!parsed.success) {
  console.error("Invalid environment:", parsed.error.flatten().fieldErrors);
  process.exit(1);
}

export const config = parsed.data;
Question 97AdvancedScenario

How would you implement structured logging in a Node.js API?

Direct answer

Use pino (Fastify default) or winston with JSON lines — log level, msg, reqId, userId, duration; redact secrets; stdout for Docker; correlate request/response in hooks; avoid console.log in production.

Scenario: debug slow GET /interviews/topics/:slug in production using searchable JSON logs (Q32, Q76).

FieldPurpose
levelinfo/warn/error filtering
reqIdTrace one request across services
req.method + urlIdentify route
responseTimeLatency SLO monitoring
err.stackServer-side only on 5xx
scenario-pino.mjs
import pino from "pino";

const logger = pino({
  level: process.env.LOG_LEVEL ?? "info",
  redact: ["req.headers.authorization", "req.headers.cookie"],
});

fastify.addHook("onResponse", async (req, reply) => {
  req.log.info({
    reqId: req.id,
    method: req.method,
    url: req.url,
    statusCode: reply.statusCode,
    responseTime: reply.elapsedTime,
  }, "request completed");
});
Question 98AdvancedScenario

How would you implement rate limiting in an Express API?

Direct answer

Apply express-rate-limit or @fastify/rate-limit globally and stricter on auth/waitlist routes — key by IP or userId, return 429 with Retry-After, store counters in Redis for multi-instance deploys.

Scenario: prevent waitlist spam and login brute force on public AceDevHub endpoints (Q79).

  • Global limit — 100 req/min per IP for API.
  • Strict routes — POST /courses/waitlist: 5/hour per IP.
  • Redis store — shared state across API replicas.
  • 429 response — { error: { code: 'RATE_LIMITED' } } + Retry-After.
scenario-rate-limit.mjs
import rateLimit from "express-rate-limit";
import RedisStore from "rate-limit-redis";
import { createClient } from "redis";

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

const apiLimiter = rateLimit({
  windowMs: 60_000,
  max: 100,
  standardHeaders: true,
  legacyHeaders: false,
  store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) }),
});

app.use("/", apiLimiter);

app.post("/courses/waitlist", rateLimit({ windowMs: 3_600_000, max: 5 }), handler);
Question 99AdvancedScenario

How would you set up PostgreSQL connection pooling in Node.js?

Direct answer

Create one pg Pool at startup with max connections sized to Postgres limit ÷ API instances — reuse across requests, await pool.query with parameterized SQL, pool.end() on shutdown; monitor waitingCount for saturation.

Scenario: AceDevHub API serves concurrent interview page queries without opening a new TCP connection per request — raw pg Pool, no ORM.

  1. Singleton pool — export pool from db/plugin; module cache ensures one instance (Q36).
  2. Sizing — max: 20 per instance; Postgres max_connections ÷ replicas.
  3. Parameterized queries — pool.query('... WHERE slug = $1', [slug]).
  4. Shutdown — await pool.end() in graceful shutdown (Q73).
scenario-pg-pool.mjs
import pg from "pg";

const pool = new pg.Pool({
  connectionString: config.databaseUrl,
  max: 20,
  idleTimeoutMillis: 30_000,
  connectionTimeoutMillis: 5_000,
});

export async function findTopicBySlug(slug) {
  const { rows } = await pool.query(
    `SELECT id, slug, title FROM interview_topics WHERE slug = $1`,
    [slug]
  );
  return rows[0] ?? null;
}

// Health: await pool.query('SELECT 1')
Question 100AdvancedScenario

What is your production readiness checklist for a Node.js API?

Direct answer

Checklist: env validation, HTTPS via reverse proxy, helmet/CORS/rate-limit, structured logs, health/readiness probes, graceful SIGTERM shutdown, pg pool + Redis, npm audit CI, secrets in env, error monitoring, load test p99, Docker non-root, backup strategy — AceDevHub targets Docker Compose + Caddy on Hetzner.

Scenario: final interview question — walk through launching AceDevHub API to production systematically, not ad hoc toggles.

Production checklist

AreaItems
ConfigZod env validation, .env.example, no secrets in repo
Securityhelmet, CORS allowlist, rate limits, JWT httpOnly, SQL params
RuntimeNODE_ENV=production, cluster/replicas, graceful shutdown
Datapg pool sized, migrations applied, Redis for queues
ObservabilityJSON logs, /health + /ready, error tracking, metrics
DeployDocker multi-stage, CI lint/typecheck/build, rolling deploy
OpsPostgres backups, R2/Cloudflare, postmortem runbook
  1. Before launch — load test, npm audit, penetration basics, staging smoke.
  2. Day one — monitor 5xx rate, pool waitingCount, memory RSS.
  3. Ongoing — dependency updates, rotation of JWT secrets, restore drills.
production-smoke.sh
# Pre-deploy smoke
curl -sf http://localhost:4000/health
curl -sf http://localhost:4000/interviews/topics/nodejs-interview-questions | jq '.data.page.sections | length'
# Expect: 100 questions after full import