Node.js Interview Questions and Answers
Node.js and Express questions for backend roles: the event loop and libuv, modules and streams, middleware, REST API design, JWT authentication and security.
How Node.js works: event loop and libuv
What is Node.js, and why is it used for backends?
Node.js is a JavaScript runtime built on Chrome's V8 engine. It runs JavaScript outside the browser, on servers and in command-line tools.
Its key design is non-blocking, event-driven I/O: instead of one thread per request waiting for the database or disk, a single JavaScript thread starts the I/O, moves on, and handles the result when it's ready. That makes it efficient for I/O-heavy work like APIs, real-time chat and streaming.
console.log("Node version starts with v:", process.version.startsWith("v"));
console.log("Platform is a string:", typeof process.platform === "string");
console.log("V8 is bundled:", typeof process.versions.v8 === "string");Node version starts with v: true
Platform is a string: true
V8 is bundled: trueAdvantages: one language for frontend and backend, a huge npm ecosystem, and good performance for many concurrent connections. Weakness: long CPU-heavy work (image processing, big calculations) blocks the single thread unless moved to worker threads or another service.
Node.js is a JavaScript runtime built on V8 that runs JavaScript on the server. It uses non-blocking, event-driven I/O on a single main thread, which handles many concurrent connections efficiently, so it suits APIs and real-time apps. It's weaker for CPU-heavy work, which blocks that thread.
Likely follow-up: Which kinds of applications would you not build in Node.js?
Is Node.js single-threaded?
Partly. Your JavaScript runs on one main thread, so two pieces of your code never run at the same moment. But Node itself uses more threads behind the scenes:
- libuv's thread pool (4 threads by default) handles file system work, DNS lookups, compression and some crypto.
- Network I/O uses the operating system's asynchronous mechanisms (epoll, kqueue, IOCP), not threads.
- V8 uses extra threads for garbage collection and compilation.
- You can create your own threads with worker_threads.
const crypto = require("node:crypto");
console.log("start");
crypto.pbkdf2("secret", "salt", 100000, 32, "sha256", () => {
console.log("hash finished on the thread pool");
});
console.log("main thread keeps going");start
main thread keeps going
hash finished on the thread poolMy JavaScript runs on a single main thread, so there are no data races in my own code. But Node uses libuv's thread pool for file system, DNS and crypto work, the operating system for network I/O, and I can create worker threads for CPU-heavy tasks.
Likely follow-up: How would you change the size of the libuv thread pool?
Modules, npm and streams
How does require() find and load a module?
require(x) resolves the name in this order:
- Core modules like
fsornode:fsload first. - Paths starting with
./,../or/load that file (trying.js,.json,.node) or folder (itspackage.jsonmain/exports, orindex.js). - Otherwise Node searches
node_modulesfolders, starting in the current directory and moving up to the root.
Modules are cached after the first load, so their top-level code runs only once and every require gets the same object.
const fs = require("node:fs");
const path = require("node:path");
const os = require("node:os");
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mod-"));
const file = path.join(dir, "counter.js");
fs.writeFileSync(file, "console.log('module code runs'); let n = 0; module.exports = { next: () => ++n };");
const a = require(file);
const b = require(file);
console.log(a.next(), b.next(), a === b);
fs.rmSync(dir, { recursive: true });module code runs
1 2 trueThe module's code ran once, and both variables share the same counter. Caching is why a module can safely hold a shared database client.
require checks core modules first, then relative or absolute paths as files or folders, then walks up through node_modules folders. Modules are cached after the first load, so their code runs once and every require returns the same exports object, which is how a module can share one database connection.
Likely follow-up: What happens with circular dependencies in CommonJS?
What is the difference between module.exports and exports?
exports starts as a reference to module.exports. require always returns module.exports.
- Adding properties to
exportsworks, because it changes the same object. - Reassigning
exports = ...breaks the link, so the new value is never exported.
const Module = require("node:module");
function load(source) {
const m = new Module("demo");
m._compile(source, "demo.js");
return m.exports;
}
console.log(load("exports.add = (a, b) => a + b;").add(2, 3));
console.log(load("exports = { add: () => 0 };"));
console.log(typeof load("module.exports = function greet() {};"));5
{}
functionRule of thumb: use module.exports = ... when exporting a single function or class, and either style when adding named properties, but never reassign exports itself.
exports is just a shortcut variable pointing to module.exports, and require returns module.exports. Adding properties through exports works, but reassigning exports breaks the link and exports nothing, so to export a single function or class I assign module.exports.
Likely follow-up: What does require return if a module never assigns anything?
Express routing and middleware
What is Express, and why use it over the plain http module?
Express is a minimal web framework on top of Node's http module. Plain http gives you a request and response and nothing else: you'd parse URLs, bodies and cookies and route requests yourself.
const http = require("node:http");
const server = http.createServer((req, res) => {
if (req.method === "GET" && req.url === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true }));
}
res.writeHead(404).end();
});
server.listen(0, async () => {
const { port } = server.address();
const res = await fetch(`http://localhost:${port}/health`);
console.log(res.status, await res.json());
server.close();
});200 { ok: true }Express adds routing with parameters, middleware, body parsing, helpers like res.json() and res.status(), and a huge ecosystem (cors, helmet, rate limiters).
import express from "express";
const app = express();
app.get("/health", (req, res) => res.json({ ok: true }));
app.listen(3000);Alternatives: Fastify (faster, schema-based validation), NestJS (structured, Angular-style, TypeScript-first) and Hono (small, runs on edge platforms).
Express is a thin framework over Node's http module that adds routing, middleware, body parsing and response helpers, so I don't hand-write URL parsing and dispatching. It has a huge ecosystem. Fastify is faster with built-in schemas, and NestJS adds structure for large teams.
Likely follow-up: When would you choose Fastify or NestJS over Express?
What is middleware in Express?
Middleware is a function that runs during the request-response cycle with access to req, res and next. It can:
- run code (logging, timing)
- change
reqorres(attach the logged-in user, parse the body) - end the request (send a 401 response)
- call
next()to pass control to the next middleware
function logger(req, res, next) {
const start = Date.now();
res.on("finish", () => {
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - start}ms`);
});
next();
}
app.use(logger); // every request
app.use(express.json()); // parse JSON bodies
app.use("/admin", requireAdmin); // only /admin routesMiddleware runs in the order it's registered. If a middleware neither sends a response nor calls next(), the request hangs until the client times out.
Middleware is a function with req, res and next that sits in the request pipeline. It can log, modify the request or response, end the request, or call next to continue. Order of registration matters, and forgetting to call next or respond leaves the request hanging.
Likely follow-up: What happens if middleware calls next() after already sending a response?
REST API design
What is REST?
REST (Representational State Transfer) is an architectural style for APIs over HTTP. Its main ideas:
- Everything is a resource with a URL:
/ebooks,/orders/42. - Use HTTP methods for actions: GET reads, POST creates, PUT/PATCH update, DELETE removes.
- Stateless: each request carries everything needed (like an auth token); the server keeps no session memory between requests.
- Use status codes to report results.
- Resources have representations, usually JSON.
- Responses can be cached where appropriate.
GET /api/ebooks list ebooks
GET /api/ebooks/react one ebook
POST /api/orders create an order
GET /api/orders/42 one order
PATCH /api/orders/42 update part of an order
DELETE /api/cart/items/7 remove an item from the cartStatelessness is what lets you run many server instances behind a load balancer: any instance can handle any request.
REST models an API as resources with URLs, uses HTTP methods for actions and status codes for results, and keeps the server stateless so each request carries its own authentication and context. Statelessness is what makes horizontal scaling easy.
Likely follow-up: How is REST different from GraphQL?
What is the difference between PUT and PATCH?
- PUT replaces the whole resource with what you send. Fields you leave out may be cleared or reset.
- PATCH partially updates: only the fields you send change.
const profile = { name: "Rohit", city: "Indore", skills: ["Node"] };
const put = (current, body) => ({ ...body });
const patch = (current, body) => ({ ...current, ...body });
console.log("PUT:", put(profile, { city: "Pune" }));
console.log("PATCH:", patch(profile, { city: "Pune" }));PUT: { city: 'Pune' }
PATCH: { name: 'Rohit', city: 'Pune', skills: [ 'Node' ] }PUT is idempotent by design: sending the same full replacement twice leaves the same result. In practice, most APIs use PATCH for edit forms where only some fields change.
PUT replaces the entire resource with the request body, so missing fields get cleared, while PATCH updates only the fields sent. PUT is idempotent by definition. I use PATCH for typical edit forms that change a few fields.
Likely follow-up: Can PATCH be idempotent?
Authentication with JWT
What is a JWT, and what are its parts?
A JSON Web Token is a compact, signed token in three base64url parts separated by dots: header.payload.signature.
- Header: the algorithm, like
{"alg":"HS256","typ":"JWT"} - Payload: claims, like the user ID (
sub), expiry (exp), issued-at time (iat) and roles - Signature: an HMAC or RSA/ECDSA signature of the header and payload
const crypto = require("node:crypto");
const b64 = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url");
const header = b64({ alg: "HS256", typ: "JWT" });
const payload = b64({ sub: "user_42", role: "student", exp: 1900000000 });
const signature = crypto.createHmac("sha256", "server-secret").update(`${header}.${payload}`).digest("base64url");
const token = `${header}.${payload}.${signature}`;
console.log("Parts:", token.split(".").length);
console.log("Anyone can read the payload:", JSON.parse(Buffer.from(token.split(".")[1], "base64url")));Parts: 3
Anyone can read the payload: { sub: 'user_42', role: 'student', exp: 1900000000 }The payload is encoded, not encrypted: anyone holding the token can read it. The signature only proves it wasn't changed and was issued by someone with the key. So never put passwords, phone numbers or other secrets in a JWT.
A JWT has a header with the algorithm, a payload of claims like the user id, expiry and role, and a signature over both, all base64url-encoded. The payload is only encoded, so anyone can read it; the signature proves it hasn't been tampered with. I never put sensitive data in it.
Likely follow-up: What is the difference between HS256 and RS256?
Error handling and security
What are operational errors and programmer errors?
- Operational errors are expected failures in a correct program: a database timeout, an invalid user input, a payment gateway being down, a file not found. Handle them: retry, return a 4xx or 503, show a friendly message.
- Programmer errors are bugs: reading a property of
undefined, passing the wrong type, forgettingawait. You can't sensibly "handle" a bug at runtime. Log it, return a 500, fix the code, and if the process state might be corrupted, restart.
class NotFoundError extends Error {
constructor(what) {
super(`${what} not found`);
this.name = "NotFoundError";
this.status = 404;
this.isOperational = true;
}
}
function toResponse(err) {
if (err.isOperational) return { status: err.status, body: err.message };
return { status: 500, body: "Something went wrong" };
}
console.log(toResponse(new NotFoundError("Order")));
console.log(toResponse(new TypeError("Cannot read properties of undefined")));{ status: 404, body: 'Order not found' }
{ status: 500, body: 'Something went wrong' }Custom error classes with a status and an isOperational flag let a single error handler decide what to show the user.
Operational errors are expected runtime failures like timeouts, invalid input or a down service, which I handle with retries, proper status codes and friendly messages. Programmer errors are bugs, which I log, answer with a generic 500 and fix. Custom error classes let the central handler tell them apart.
Likely follow-up: Why is it risky to keep a process running after an unknown programmer error?
What happens with an unhandled promise rejection?
If a promise rejects and nothing catches it, Node emits unhandledRejection. Since Node 15, the default is to crash the process with the error, the same as an uncaught exception.
process.on("unhandledRejection", (reason) => {
console.log("Caught globally:", reason.message);
});
async function sendWelcomeEmail() {
throw new Error("SMTP timeout");
}
sendWelcomeEmail(); // no await, no .catch()
setTimeout(() => console.log("process still alive because a handler exists"), 10);Caught globally: SMTP timeout
process still alive because a handler existsThe common cause is a fire-and-forget async call with no await and no .catch(). Fix the cause: await it, or add .catch(logError) for intentional background work. A global handler should log and alert, and then usually let the process restart cleanly.
Since Node 15 an unhandled rejection crashes the process by default. The usual cause is calling an async function without await or catch, often for background work like emails. I fix it at the source with await or an explicit catch, and keep a global handler only to log and alert.
Likely follow-up: How would you safely run a background task after sending a response?
All 100 questions in the ebook
How Node.js works: event loop and libuv
- What is Node.js, and why is it used for backends?
- Is Node.js single-threaded?
- What is libuv?
- What are the phases of the Node.js event loop?
- What is the difference between process.nextTick, promises, setImmediate and setTimeout?
- What does "blocking the event loop" mean?
- How does Node handle thousands of concurrent requests with one thread?
- What are worker threads, and when would you use them?
- What is the cluster module, and how does it differ from PM2?
- What is the difference between CommonJS and ES modules in Node?
- What is the difference between the browser's JavaScript and Node's?
- What is a Buffer?
- What is the difference between process.exit and letting the process end naturally?
- What are EventEmitters?
- What is the difference between readFile and readFileSync?
- How do you handle errors in asynchronous Node code?
- What are some common Node error codes?
- How do you measure performance in Node?
Modules, npm and streams
- How does require() find and load a module?
- What is the difference between module.exports and exports?
- What is package.json for?
- What do ^ and ~ mean in package versions?
- What is package-lock.json, and should you commit it?
- What is the difference between dependencies, devDependencies and peerDependencies?
- What are environment variables, and how do you use them in Node?
- What are streams in Node?
- What is backpressure?
- Why use stream.pipeline instead of pipe?
- What is a Transform stream?
- How do you read a large file line by line?
- How do you work with file paths safely?
- How do you make HTTP requests from Node?
- What is npm audit, and how do you keep dependencies secure?
- What is the difference between npm, npx, yarn and pnpm?
Express routing and middleware
- What is Express, and why use it over the plain http module?
- What is middleware in Express?
- How does the middleware chain work internally?
- What are route parameters and query strings?
- How do you parse request bodies in Express?
- What is express.Router, and how do you structure routes?
- How do you handle errors in Express?
- How do you handle 404s for unknown routes?
- What is CORS, and how do you configure it in Express?
- How do you serve static files in Express?
- What is the difference between app.use and app.get?
- How do you validate request data in Express?
- How do you add request logging and request IDs?
- How do you set up a graceful shutdown for an Express server?
- How do you write tests for an Express API?
- How do you upload files in Express?
REST API design
- What is REST?
- What is the difference between PUT and PATCH?
- What does idempotent mean, and which HTTP methods are idempotent?
- How do you make a non-idempotent operation safe to retry?
- Which HTTP status codes should an API use?
- How do you design good REST URLs?
- How do you implement pagination?
- How do you version an API?
- Why should money be stored in paise instead of rupees with decimals?
- How do you design a consistent error response format?
- How does HTTP caching work for APIs?
- How do you implement rate limiting?
- What is the difference between REST, GraphQL and gRPC?
- How do webhooks work, and how do you handle them safely?
- How would you design the API for an online ebook store?
- What is HATEOAS, and do real APIs use it?
Authentication with JWT
- What is the difference between authentication and authorisation?
- What is a JWT, and what are its parts?
- How do you verify a JWT, and what happens if someone edits it?
- Where should you store a JWT in the browser?
- What are access tokens and refresh tokens?
- How do you log out a user with JWTs?
- JWT or server-side sessions: which should you use?
- How should passwords be stored?
- How would you implement OTP login with a phone number?
- What is OAuth 2.0, and how does "Sign in with Google" work?
- How do you implement role-based access control (RBAC)?
- What is CSRF, and how do you prevent it?
- How do you protect a login endpoint from brute-force attacks?
- How should a "forgot password" flow work securely?
- How do you secure an API that third parties or mobile apps call?
- What is two-factor authentication, and how does TOTP work?
Error handling and security
- What are operational errors and programmer errors?
- What happens with an unhandled promise rejection?
- How do you add timeouts and retries to outbound calls?
- What is a circuit breaker?
- What is the OWASP Top 10?
- What is SQL injection, and how do you prevent it?
- What is NoSQL injection?
- What is XSS, and how do you prevent it?
- What security headers should a Node app set?
- What is SSRF?
- What is command injection?
- What is prototype pollution?
- How do you manage secrets in a Node application?
- How do you prevent denial-of-service problems in a Node API?
- How do you handle file downloads of paid content securely?
- What should you log and monitor in production?
- How do you keep a Node app secure in production, as a checklist?
- Why should you use the Node.js LTS version in production?