🟢

Top 98 Node.js Interview Questions & Answers (2026)

98 Questions 41 Beginner 33 Intermediate 24 Advanced

About Node.js

Top 100 Node.js interview questions covering event loop, streams, modules, async programming, Express.js, performance, and production best practices. Companies hiring for Node.js roles test this knowledge at every stage — from first-round screens that check the basics to final-round conversations about real-world trade-offs.

What to Expect in a Node.js Interview

Expect a mix of conceptual and practical Node.js questions: clear definitions and core-concept checks for junior roles, hands-on scenario questions for mid-level roles, and architecture or trade-off discussions for senior roles. Interviewers usually move from foundational topics toward the kind of problems you'd actually face on the job.

How to Use This Guide

Work through the Node.js questions in order — Beginner, then Intermediate, then Advanced — so each concept builds on the last. Every question has its own page; bookmark the ones that trip you up and revisit them the day before your interview.

Curated by Tech Baithak Editorial Team  ·  Last updated: May 2026

Beginner 41 questions

Core concepts every Node.js developer must know.

01

What is Node.js?

Node.js is an open-source, cross-platform JavaScript runtime environment built on Chrome's V8 JavaScript engine. It allows developers to execute JavaScript code on the server side — outside of a browser. Node.js was created by Ryan Dahl in 2009 and introduced a non-blocking, event-driven I/O model that makes it lightweight and efficient for building scalable network applications. Before Node.js, JavaScript was confined to the browser; Node.js brought JavaScript to the backend, enabling full-stack development with a single language. Node.js uses an event loop and asynchronous APIs to handle many concurrent connections without the overhead of threads. It is widely used for building REST APIs, real-time applications (chat, gaming), microservices, CLI tools, and streaming applications.

Open this question on its own page
02

What is the V8 engine?

V8 is an open-source, high-performance JavaScript and WebAssembly engine developed by Google, written in C++. It is used in Google Chrome and Node.js. V8 compiles JavaScript directly to native machine code using Just-In-Time (JIT) compilation — rather than interpreting it line by line — making execution dramatically faster. V8 implements the ECMAScript standard and handles memory management through its garbage collector. When Node.js was created, Ryan Dahl embedded V8 into a C++ program and added APIs for file system, networking, and other OS-level operations. V8 continuously evolves to support new ECMAScript features and optimize performance, which benefits both Chrome and Node.js users simultaneously.

Open this question on its own page
03

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

Both Node.js and the browser run JavaScript using the V8 engine, but they operate in fundamentally different environments. The browser provides APIs for DOM manipulation (document, window), browser storage (localStorage, cookies), Web APIs (fetch, WebSockets), and user interaction events. Node.js has no DOM — instead it provides APIs for the operating system: fs (file system), http (networking), path, os, child_process, and more. Node.js uses CommonJS modules (require()) and now supports ES Modules; browsers primarily use ES Modules (import/export). Node.js has access to the local file system; browsers do not (for security). Global objects also differ: browser has window, Node.js has global (now globalThis in both).

Open this question on its own page
04

What is npm?

npm (Node Package Manager) is the default package manager for Node.js and the world's largest software registry, hosting over 2 million packages. It serves two roles: (1) a CLI tool for installing, updating, and managing JavaScript packages and their dependencies, and (2) an online registry at npmjs.com where developers publish and share packages. npm comes bundled with Node.js. Key commands: npm install <package> — installs a package locally; npm install -g <package> — installs globally; npm init — creates a package.json; npm run <script> — runs a script defined in package.json; npm update — updates packages. Alternatives to npm include Yarn and pnpm, which offer faster installation and better monorepo support.

Open this question on its own page
05

What is package.json?

package.json is the manifest file for a Node.js project. It is a JSON file that records important metadata about your project and manages its dependencies. Key fields include: name and version (required for published packages), description, main (entry point file), scripts (custom commands like "start": "node index.js"), dependencies (packages required in production), devDependencies (packages only needed during development like testing tools), peerDependencies (packages the consumer must install), engines (required Node.js version), and license. The scripts field is particularly useful for defining build, test, and start commands. Generate it with npm init (interactive) or npm init -y (defaults).

Open this question on its own page
06

What is the event loop in Node.js?

The event loop is the core mechanism that enables Node.js to perform non-blocking I/O operations despite JavaScript being single-threaded. It continuously monitors the call stack and the callback queues, moving callbacks to the call stack when it is empty. The event loop has multiple phases executed in order: timers (setTimeout, setInterval callbacks), pending callbacks (I/O error callbacks), idle/prepare (internal), poll (retrieve new I/O events), check (setImmediate callbacks), and close callbacks. When Node.js starts, it initializes the event loop, processes the provided script (which may register timers and I/O), then begins the event loop. As long as there are callbacks to process or I/O operations pending, the event loop keeps running. Understanding the event loop is fundamental to writing efficient, non-blocking Node.js code.

Open this question on its own page
07

What is non-blocking I/O?

Non-blocking I/O means that when Node.js initiates an I/O operation (reading a file, making a network request, querying a database), it does not wait for that operation to complete before moving on to execute other code. Instead, it registers a callback function and continues processing other tasks. When the I/O operation completes, the event loop picks up the callback and executes it. Blocking I/O (the traditional approach) would halt program execution until the operation finishes, wasting CPU cycles while waiting. Node.js's non-blocking model means a single thread can serve thousands of concurrent requests — while one request is waiting for a database query, the thread handles other requests. This makes Node.js extremely efficient for I/O-bound workloads. However, CPU-intensive tasks (image processing, heavy computation) still block the event loop since they don't involve I/O waits.

Open this question on its own page
08

What are callbacks in Node.js?

A callback is a function passed as an argument to another function, to be called when an asynchronous operation completes. Node.js uses the error-first callback convention (also called Node.js callback style or errback): the first argument of the callback is always an error object (or null if no error), and subsequent arguments are the result data. Example: fs.readFile("file.txt", "utf8", (err, data) => { if (err) throw err; console.log(data); });. This pattern allows Node.js to handle errors consistently across all asynchronous operations. The main drawback of callbacks is callback hell — deeply nested callbacks for sequential async operations — which makes code hard to read and maintain. Promises and async/await were introduced to solve this problem.

Open this question on its own page
09

What is callback hell and how do you avoid it?

Callback hell (also called the "pyramid of doom") occurs when multiple nested callbacks create code that is deeply indented and difficult to read, maintain, and debug. Example: reading a file, then parsing it, then making an API call, then writing results — each step nesting inside the previous callback. Solutions: (1) Named functions — extract each callback into a named function instead of nesting anonymously, flattening the structure. (2) Promises — chainable .then() handlers allow sequential async operations in a flat, readable chain. (3) async/await — syntactic sugar over Promises that makes async code look synchronous and sequential, dramatically improving readability. (4) Modularization — split complex async flows into small, single-purpose functions. Modern Node.js code primarily uses async/await, which is the recommended approach.

Open this question on its own page
10

What are Promises in Node.js?

A Promise is an object representing the eventual completion or failure of an asynchronous operation. A Promise is in one of three states: pending (initial state, not yet settled), fulfilled (operation completed successfully, has a value), or rejected (operation failed, has a reason/error). Promises are created with new Promise((resolve, reject) => { ... }). Chain them with .then(onFulfilled, onRejected) and .catch(onRejected). Key static methods: Promise.all([p1, p2]) — waits for all promises (fails fast on any rejection); Promise.allSettled([...]) — waits for all, regardless of rejection; Promise.race([...]) — resolves/rejects as soon as any promise settles; Promise.any([...]) — resolves when any fulfills. Promises solve callback hell by enabling flat .then() chains instead of nested callbacks.

Open this question on its own page
11

What is async/await in Node.js?

async/await is syntactic sugar built on top of Promises that allows you to write asynchronous code in a synchronous, sequential style. Mark a function with the async keyword — it automatically returns a Promise. Inside an async function, use await before any expression that returns a Promise; execution pauses at that point until the Promise settles, then resumes with the resolved value. Error handling uses standard try/catch/finally blocks, which is more intuitive than .catch() chains. Example: async function fetchUser(id) { try { const data = await db.find(id); return data; } catch (err) { console.error(err); } }. Important: await only suspends the current async function, not the entire Node.js event loop — other events continue to be processed. For parallel operations, use await Promise.all([...]); instead of sequential awaits.

Open this question on its own page
12

What is the require() function in Node.js?

require() is Node.js's built-in function for loading modules in the CommonJS module system. It takes a module identifier and returns the exported value. Module identifiers can be: (1) Built-in modules: require("fs"), require("http") — no path needed; (2) npm packages: require("express") — Node.js looks in node_modules/; (3) Local files: require("./utils") — relative path with or without .js extension. require() is synchronous — it loads the module, executes it, and returns module.exports before continuing. Node.js caches modules after the first load — subsequent require() calls for the same module return the cached export without re-executing the module file. This makes circular dependencies possible (though tricky) and module-level state work as singletons.

Open this question on its own page
13

What is module.exports in Node.js?

module.exports is the object that is returned when another file require()s a Node.js module. By default, it is an empty object {}. You can export a single value (function, class, or primitive) by assigning directly: module.exports = function greet() { ... };. You can export multiple things by adding properties: module.exports.greet = greet; module.exports.farewell = farewell; or using shorthand exports.greet = greet; (note: exports is just a reference to module.exports, so you must not reassign exports itself). ES Modules (the modern alternative) use export and import syntax instead. CommonJS (require/module.exports) is still dominant in existing Node.js code, but ES Modules are supported natively in Node.js 12+ with .mjs extension or "type": "module" in package.json.

Open this question on its own page
14

What is the fs module in Node.js?

The fs (File System) module is a core Node.js module that provides an API for interacting with the file system. It supports both synchronous (blocking) and asynchronous (non-blocking) operations. Key methods: fs.readFile(path, encoding, callback) / fs.readFileSync() — read file contents; fs.writeFile(path, data, callback) / fs.writeFileSync() — write (or overwrite) a file; fs.appendFile() — append data; fs.unlink() — delete a file; fs.mkdir() / fs.mkdirSync() — create directory; fs.readdir() — list directory contents; fs.stat() — get file metadata; fs.rename() — rename/move a file; fs.watch() — watch for file changes. The fs/promises submodule (Node.js 10+) provides Promise-based versions: const fs = require("fs/promises");. Always prefer async methods in production to avoid blocking the event loop.

Open this question on its own page
15

What is the http module in Node.js?

The http module is a core Node.js module for creating HTTP servers and making HTTP requests. Creating a server: const server = http.createServer((req, res) => { res.writeHead(200, {"Content-Type": "text/plain"}); res.end("Hello World"); }); server.listen(3000);. The callback receives a IncomingMessage (request) object with properties like req.method, req.url, req.headers, and a ServerResponse object for sending responses. Making requests: http.get(url, callback) for simple GET requests; http.request(options, callback) for full control. The https module provides the same API with TLS/SSL support. In practice, most developers use frameworks like Express.js on top of the http module, as raw http is verbose for routing and middleware. For HTTP requests from Node.js, libraries like axios or the native fetch (Node.js 18+) are preferred.

Open this question on its own page
16

What is Express.js?

Express.js is the most popular, minimal, and flexible Node.js web application framework. It provides a thin layer of web application features on top of Node's built-in http module without obscuring Node.js features. Key features: (1) Routing — define route handlers for different HTTP methods and URL patterns: app.get("/users/:id", handler); (2) Middleware — chain functions that have access to request and response objects; (3) Template engines — integrates with EJS, Pug, Handlebars; (4) Static file servingexpress.static(); (5) Error handling — centralized error-handling middleware with 4 parameters (err, req, res, next). Express is unopinionated — it imposes minimal structure, which makes it flexible but also means you choose your own patterns for things like authentication, database access, and validation. Alternatives include Fastify, Koa, Hapi, and NestJS.

Open this question on its own page
17

What is middleware in Express.js?

Middleware in Express.js is a function that has access to the request object (req), response object (res), and the next function in the request-response cycle. Middleware functions can: execute code, modify req/res objects, end the request-response cycle, or call next() to pass control to the next middleware. Types of Express middleware: (1) Application-level: bound to app.use() or app.METHOD(); (2) Router-level: bound to an express.Router() instance; (3) Error-handling: has 4 parameters (err, req, res, next); (4) Built-in: express.json(), express.urlencoded(), express.static(); (5) Third-party: cors, morgan, helmet, passport. Middleware executes in the order it is defined. If a middleware does not end the cycle or call next(), the request will hang.

Open this question on its own page
18

What is the path module in Node.js?

The path module provides utilities for working with file and directory paths in a cross-platform way (Windows uses backslashes, Unix uses forward slashes). Key methods: path.join() — joins path segments together using the OS-specific separator: path.join("/users", "alice", "docs")"/users/alice/docs"; path.resolve() — resolves a sequence of paths to an absolute path; path.dirname(p) — returns the directory portion of a path; path.basename(p) — returns the filename portion; path.extname(p) — returns the file extension (".js"); path.parse(p) — returns an object with root, dir, base, name, ext; path.normalize(p) — normalizes a path (resolves .. and .). Use __dirname (current module's directory) and __filename (current module's full path) with path.join() to construct absolute paths relative to the current file.

Open this question on its own page
19

What is __dirname and __filename in Node.js?

__dirname is a global variable in Node.js CommonJS modules that contains the absolute directory path of the currently executing module's file. __filename contains the absolute path of the currently executing file, including the filename itself. They are extremely useful for constructing file paths relative to the current module, avoiding issues with the current working directory changing: const configPath = path.join(__dirname, "config", "settings.json");. This is preferable to relative paths like "./config/settings.json", which are resolved relative to process.cwd() (the directory from which Node.js was launched), not the module's location. Note: In ES Modules (.mjs or "type": "module"), __dirname and __filename are not available. The equivalent is: import.meta.url + new URL() + fileURLToPath().

Open this question on its own page
20

What is the os module in Node.js?

The os module provides operating system-related utility methods and properties. Key features: os.platform() — returns the OS platform: "linux", "darwin" (macOS), "win32"; os.arch() — CPU architecture: "x64", "arm64"; os.cpus() — array of CPU core info (model, speed, times); os.freemem() / os.totalmem() — free and total system memory in bytes; os.hostname() — machine hostname; os.userInfo() — current user info (username, homedir); os.homedir() — home directory path; os.tmpdir() — default temp directory path; os.networkInterfaces() — network interfaces with IP addresses; os.uptime() — system uptime in seconds; os.EOL — OS-specific line ending (\r\n on Windows, \n on Unix). Useful for writing cross-platform CLI tools and system monitoring utilities.

Open this question on its own page
21

What is process in Node.js?

process is a global object in Node.js that provides information about, and control over, the current Node.js process. Key properties and methods: process.env — environment variables (e.g., process.env.NODE_ENV, process.env.PORT); process.argv — command-line arguments array (index 0: node path, index 1: script path, index 2+: user args); process.cwd() — current working directory; process.exit(code) — exit the process (0 = success, non-zero = error); process.pid — process ID; process.version — Node.js version string; process.platform — OS platform; process.memoryUsage() — memory usage stats; process.stdin, process.stdout, process.stderr — standard streams; process.on("uncaughtException", handler) — catch unhandled errors (use with care); process.on("SIGTERM", handler) — handle OS signals for graceful shutdown.

Open this question on its own page
22

What is the events module and EventEmitter in Node.js?

The events module provides the EventEmitter class, which is the foundation of Node.js's event-driven architecture. EventEmitter allows objects to emit named events and register listener functions that respond to those events. Key methods: emitter.on("event", listener) — add a listener (persists); emitter.once("event", listener) — add a one-time listener (auto-removed after first call); emitter.emit("event", ...args) — trigger all listeners for the event synchronously; emitter.off("event", listener) / removeListener() — remove a specific listener; emitter.removeAllListeners("event") — remove all listeners. Many core Node.js classes extend EventEmitter: HTTP servers ("request"), streams ("data", "end"), and child processes ("exit"). Custom EventEmitters are useful for decoupling components in your application through a publish-subscribe pattern.

Open this question on its own page
23

What is the difference between synchronous and asynchronous code in Node.js?

Synchronous code executes sequentially — each operation blocks the execution thread until it completes before the next line runs. In Node.js, synchronous operations block the event loop, preventing other requests from being handled. Example: fs.readFileSync("file.txt") — the thread waits until the file is fully read. Asynchronous code initiates an operation and immediately continues executing other code. When the operation finishes, a callback, promise resolution, or event notifies Node.js to handle the result. Example: fs.readFile("file.txt", callback) — Node.js registers the callback and moves on; the callback runs later when the file is ready. Rule of thumb: never use synchronous I/O (readFileSync, writeFileSync) in request handlers or any code that runs per-request, as it blocks every other request. Synchronous operations are acceptable in startup/initialization code (before the server starts listening), CLI scripts, or build tools.

Open this question on its own page
24

What is the buffer in Node.js?

A Buffer in Node.js is a fixed-size chunk of memory (allocated outside the V8 heap) for handling raw binary data — bytes. Buffers are essential for working with streams, file I/O, network protocols, and binary data (images, audio, encrypted data) where JavaScript's native string handling is insufficient. Creating buffers: Buffer.alloc(size) — creates a zero-initialized buffer of the specified size (safe); Buffer.from(string, encoding) — creates a buffer from a string; Buffer.from(array) — from an array of bytes. Key methods: buf.toString(encoding) — convert to string; buf.length — byte length; buf.slice(start, end) — create a view; Buffer.concat([buf1, buf2]) — concatenate buffers. Supported encodings include utf8, base64, hex, binary, ascii. Node.js 10+ recommends Buffer.alloc() over the deprecated new Buffer() constructor.

Open this question on its own page
25

What are streams in Node.js?

Streams are Node.js's way of handling reading and writing data sequentially, piece by piece, rather than loading everything into memory at once. This is essential for large files, real-time data, and network communication. There are four types: (1) Readable — source of data (fs.createReadStream, http.IncomingMessage); (2) Writable — destination for data (fs.createWriteStream, http.ServerResponse); (3) Duplex — both readable and writable (TCP sockets); (4) Transform — duplex stream that transforms data as it passes through (zlib for compression, crypto for encryption). Streams emit events: "data", "end", "error", "finish". The key method is readable.pipe(writable), which automatically manages data flow and backpressure. Example: fs.createReadStream("large.csv").pipe(transform).pipe(fs.createWriteStream("out.csv")) processes the file without loading it all into memory.

Open this question on its own page
26

What is npm install and what does it do?

npm install (or npm i) downloads and installs packages from the npm registry into the local node_modules/ directory. Variants: npm install (no args) — installs all dependencies listed in package.json; npm install <package> — installs a package and adds it to dependencies; npm install <package> --save-dev (or -D) — adds to devDependencies (not included in production build); npm install <package> --global (or -g) — installs globally for CLI use; npm install <package>@1.2.3 — install a specific version. npm resolves the full dependency tree, downloads packages, and generates/updates package-lock.json, which locks exact versions for reproducible installs. The node_modules/ folder should be added to .gitignore — teammates run npm install to recreate it from package.json.

Open this question on its own page
27

What is package-lock.json?

package-lock.json is an automatically generated file that records the exact version of every package (direct and transitive dependency) installed in node_modules/, along with resolved URLs and integrity hashes. Its purpose is to ensure reproducible installs — running npm install on any machine or at any future time will install exactly the same package versions, not just "compatible" versions as specified by semver ranges in package.json. This prevents the "works on my machine" problem caused by different developers or CI servers resolving package ranges differently. Commit package-lock.json to version control for applications (so all team members and CI use identical dependencies). For libraries published to npm, whether to commit it is debated — the library's consumers determine their own lock. Do not manually edit package-lock.json; it is maintained by npm automatically.

Open this question on its own page
28

What is the difference between dependencies and devDependencies?

dependencies are packages required for the application to run in production — they are needed at runtime. Examples: express, mongoose, axios, jsonwebtoken. Install with npm install <package>. devDependencies are packages only needed during development and build processes — not in production. Examples: testing frameworks (jest, mocha), linters (eslint), transpilers (babel, typescript), bundlers (webpack, vite), and test utilities (supertest). Install with npm install --save-dev <package>. When deploying, run npm install --production (or set NODE_ENV=production) to install only dependencies, keeping the production installation lean. peerDependencies are a third type — they specify a compatibility requirement that the consumer of your package must install themselves (e.g., a React component library lists react as a peerDependency). optionalDependencies are installed if possible but not required for the package to work.

Open this question on its own page
29

What is nodemon?

nodemon is a development utility tool that monitors your Node.js application for file changes and automatically restarts the server whenever changes are detected. Without nodemon, you would have to manually stop and restart Node.js every time you change a file during development. Install as a dev dependency: npm install --save-dev nodemon. Use it instead of node: nodemon app.js. Configure via nodemon.json or the nodemonConfig field in package.json to specify which file extensions and directories to watch, and which to ignore. Typically added to the dev script in package.json: "dev": "nodemon app.js". An alternative built into Node.js 18+ is node --watch, which provides similar file-watching functionality without an extra dependency.

Open this question on its own page
30

What is the dotenv package and how is it used?

dotenv is a zero-dependency npm package that loads environment variables from a .env file into process.env. This allows you to configure your application without hardcoding sensitive values (database passwords, API keys, secret tokens) in source code. Usage: (1) Install: npm install dotenv; (2) Create .env file: DB_PASSWORD=secret\nPORT=3000; (3) Load at app entry point: require("dotenv").config(); (must be called before accessing process.env); (4) Access: process.env.DB_PASSWORD. Always add .env to .gitignore to prevent committing secrets. Provide a .env.example file with placeholder values as documentation for teammates. Node.js 20.6+ has native --env-file flag: node --env-file=.env app.js, reducing the need for dotenv in newer projects.

Open this question on its own page
31

What is REST API and how do you build one with Node.js?

REST (Representational State Transfer) is an architectural style for designing networked APIs based on HTTP. REST APIs use HTTP methods (GET, POST, PUT, PATCH, DELETE) on resource-based URLs, stateless communication, and typically JSON for data exchange. Building a REST API with Node.js and Express: (1) Setup: const express = require("express"); const app = express(); app.use(express.json());; (2) Define routes matching resources and HTTP methods: app.get("/users", getAll), app.get("/users/:id", getOne), app.post("/users", create), app.put("/users/:id", update), app.delete("/users/:id", remove); (3) Send JSON responses: res.json({ data: users }); (4) Handle errors: res.status(404).json({ error: "Not found" }). REST conventions: use nouns for resource names (not verbs), use status codes correctly (200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 404 Not Found, 500 Internal Server Error).

Open this question on its own page
32

What is JSON.parse() and JSON.stringify() in Node.js?

JSON.stringify(value, replacer, space) converts a JavaScript value (object, array, primitive) to a JSON string. The optional space parameter adds indentation for readability: JSON.stringify(obj, null, 2). It skips undefined, functions, and symbols. JSON.parse(string) parses a JSON string and returns the corresponding JavaScript value. If the string is invalid JSON, it throws a SyntaxError — always wrap in try/catch when parsing untrusted input: try { const data = JSON.parse(raw); } catch (e) { handle error }. In Express.js, express.json() middleware automatically calls JSON.parse on request bodies and sets req.body. For working with large JSON data efficiently, consider streaming parsers like stream-json rather than parsing the entire payload into memory at once.

Open this question on its own page
33

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

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that restricts web pages from making requests to a different domain (origin) than the one that served the page. The browser enforces this; Node.js itself does not apply it to server-to-server requests. An origin is defined by protocol + domain + port. When a browser makes a cross-origin request, the server must include appropriate Access-Control-Allow-* headers. In Express.js, the easiest approach is the cors npm package: const cors = require("cors"); app.use(cors()); — this allows all origins. For production, configure it specifically: app.use(cors({ origin: "https://yourfrontend.com", methods: ["GET", "POST"], credentials: true }));. Alternatively, set headers manually: res.setHeader("Access-Control-Allow-Origin", "*");. Preflight requests (OPTIONS method) must also be handled — the cors package does this automatically.

Open this question on its own page
34

How do you handle errors in Express.js?

Express.js has a special error-handling middleware with four parameters: (err, req, res, next). It must be defined after all other routes and middleware. Any route can trigger it by calling next(err) or by throwing an error in an async function (with appropriate wrapping). Structure: (1) In route handlers, pass errors to next(err) rather than crashing or sending ad-hoc error responses; (2) Create a centralized error handler: app.use((err, req, res, next) => { console.error(err.stack); res.status(err.status || 500).json({ error: err.message }); });; (3) For async route handlers, wrap with a try/catch that calls next: app.get("/", async (req, res, next) => { try { ... } catch(e) { next(e); } });. Libraries like express-async-errors or express-async-handler remove the need for repetitive try/catch boilerplate by automatically forwarding async errors to Express's error handler.

Open this question on its own page
35

What is the query string in a URL and how do you access it in Express?

A query string is the portion of a URL after the ? character, containing key-value pairs separated by &. Example: /search?q=nodejs&page=2&limit=10. In Express.js, query parameters are automatically parsed and available in req.query as an object: const { q, page, limit } = req.query; — values are always strings, so convert types as needed: const pageNum = parseInt(page, 10) || 1;. Route parameters (different from query params) are segments of the URL path prefixed with : — e.g., /users/:id — accessible via req.params.id. Request body (for POST/PUT) is accessible via req.body after using express.json() middleware. Always validate and sanitize query parameter values before using them in queries or logic, as they come from untrusted user input.

Open this question on its own page
36

What is the util module in Node.js?

The util module provides utility functions for internal Node.js use that are also useful for application development. Key features: util.promisify(fn) — converts a callback-style function (following error-first convention) to return a Promise: const readFile = util.promisify(fs.readFile); then use with async/await; util.callbackify(fn) — the reverse; util.inspect(object, options) — returns a string representation of an object for debugging (used internally by console.log); util.format(format, ...args) — formatted string (like printf); util.inherits(constructor, superConstructor) — prototype-based inheritance (legacy, pre-class syntax); util.types — type checks like util.types.isPromise(), util.types.isRegExp(). util.promisify is particularly useful for modernizing older callback-based APIs and third-party libraries that haven't yet been updated to return Promises.

Open this question on its own page
37

What is the crypto module in Node.js?

The crypto module provides cryptographic functionality including hashing, HMAC, encryption, decryption, and random bytes generation. Key uses: Hashing: crypto.createHash("sha256").update(data).digest("hex") — one-way hash for verifying data integrity or storing passwords (use bcrypt for passwords, not SHA); HMAC: crypto.createHmac("sha256", secret).update(data).digest("hex") — keyed hash for message authentication; Random bytes: crypto.randomBytes(32) — cryptographically secure random buffer for tokens, salts, nonces; crypto.randomUUID() (Node.js 14.17+) — generate a random UUID v4; Symmetric encryption: crypto.createCipheriv(algorithm, key, iv) / createDecipheriv() — AES encryption; Password hashing: use bcrypt or argon2 npm packages (not built-in crypto) as they implement proper salting and key stretching for passwords.

Open this question on its own page
38

What is the child_process module in Node.js?

The child_process module enables spawning child processes to run external commands, scripts, or programs from within Node.js. Key methods: exec(command, callback) — runs a shell command, buffers output, invokes callback with stdout/stderr strings (limited to 200KB by default); execFile(file, args, callback) — like exec but directly executes a file without a shell (more secure); spawn(command, args) — spawns a process and streams stdin/stdout/stderr in real-time (no buffer limit, suitable for long-running processes or large output); fork(modulePath) — special case of spawn for Node.js scripts — creates a new Node.js process with an IPC (inter-process communication) channel for sending messages between parent and child. Use child_process for: running shell commands, executing Python/Ruby scripts, CPU-intensive tasks in a separate process (though Worker Threads are better for that), and running system utilities.

Open this question on its own page
39

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

setTimeout(fn, 0) schedules a callback to run after the current event loop cycle, in the timers phase of the next iteration, after a minimum delay (at least ~1ms even if 0 is specified). setImmediate(fn) schedules a callback to run in the check phase of the current event loop iteration — after I/O events and before timers in the next iteration. In practice: when both are called from the main module, their execution order is non-deterministic (depends on process performance). But when called within an I/O callback, setImmediate() always executes before setTimeout(fn, 0). process.nextTick(fn) is different from both — it fires immediately after the current operation completes, before the event loop continues to its next phase. Use process.nextTick() to ensure a callback fires before any I/O; use setImmediate() to yield to the event loop and then run.

Open this question on its own page
40

What is process.nextTick() in Node.js?

process.nextTick(callback) adds the callback to the nextTick queue, which is processed immediately after the current operation completes — before the event loop moves to the next phase, before any I/O, timers, or setImmediate callbacks. It is the highest-priority async scheduling mechanism. Use cases: (1) Allow a function to complete before a caller's callback runs; (2) Allow an EventEmitter's "error" event to be caught after the emitter is set up (avoid emitting events before listeners are attached); (3) Allow cleanup code to run before more I/O is processed. Warning: overusing process.nextTick() can starve the event loop — if nextTick callbacks keep adding more nextTick callbacks, I/O events are never processed (use setImmediate() to yield to I/O). The distinction: nextTick fires before the next event loop iteration; setImmediate fires in the next iteration's check phase.

Open this question on its own page
41

What is the cluster module in Node.js?

The cluster module enables a Node.js application to take advantage of multi-core CPUs by spawning multiple worker processes, each running the same server. A single Node.js process is single-threaded and uses only one CPU core. The cluster module forks child processes (workers) from a master process; all workers share the same server port and the OS load-balances incoming connections among them. Basic pattern: if (cluster.isMaster) { for (let i = 0; i < os.cpus().length; i++) cluster.fork(); } else { app.listen(3000); }. If a worker crashes, the master can fork a new one. In practice, PM2 (a process manager) is more commonly used in production than raw cluster code — it handles clustering, auto-restart, log management, and monitoring with simpler configuration: pm2 start app.js -i max (max = number of CPU cores).

Open this question on its own page
Intermediate 33 questions

Practical knowledge for developers with hands-on experience.

01

What is the event-driven architecture in Node.js?

Event-driven architecture is a design paradigm where program flow is determined by events — signals that something has happened (user action, data arrival, timer expiry). Node.js is fundamentally event-driven: rather than sequentially executing operations and blocking on I/O, it registers callbacks for events and handles them as they occur. The EventEmitter class is the backbone: components emit events and other components listen and react. This enables loose coupling — emitters do not need to know about listeners, and listeners do not need to know what triggered the event. In a Node.js HTTP server, each incoming request fires a "request" event. A readable stream fires "data" events as chunks arrive and "end" when complete. This architecture makes Node.js excellent for real-time systems, message queues, and microservices. The main challenge is that complex event flows can be hard to trace — tools like distributed tracing and careful naming of events help manage complexity.

Open this question on its own page
02

What is middleware chaining in Express.js?

Middleware chaining is the sequential execution of multiple middleware functions for a single request, where each function calls next() to pass control to the next function in the chain. The chain runs in the order middleware is registered with app.use() or route methods. Example chain: request logging → authentication → rate limiting → request validation → route handler → error handler. Each middleware can: (1) pass control downstream by calling next(); (2) terminate the chain by sending a response (res.json(), res.send()); (3) short-circuit with an error by calling next(error). Middleware can be applied globally (app.use(fn)), per path (app.use("/api", fn)), or per route (app.get("/", fn1, fn2, handler)). The order of app.use() calls matters — auth middleware must come before protected routes. Reusable middleware is packaged as npm modules (cors, helmet, morgan, express-rate-limit).

Open this question on its own page
03

What are Worker Threads in Node.js?

Worker Threads (introduced in Node.js 10, stable in 12) allow Node.js to execute JavaScript in parallel threads, sharing memory via SharedArrayBuffer. Unlike child_process which spawns separate processes with separate memory, Worker Threads run in the same process and can share data efficiently. Workers communicate with the main thread via postMessage() and on("message"). Use Worker Threads for CPU-intensive operations that would block the event loop: image processing, video encoding, complex calculations, ML inference, parsing large data. Example: const { Worker } = require("worker_threads"); const worker = new Worker("./task.js", { workerData: { input } }); worker.on("message", result => ...); worker.on("error", err => ...);. Do NOT use Worker Threads for I/O-bound tasks — Node.js's async I/O already handles those efficiently without threads. The thread pool (libuv) already handles some I/O operations in the background.

Open this question on its own page
04

What is libuv in Node.js?

libuv is the C library that powers Node.js's event loop and asynchronous I/O. It provides the abstraction layer between Node.js and the operating system. Key responsibilities: (1) Event loop implementation — the multi-phase loop (timers, poll, check, etc.); (2) Thread pool — a pool of threads (default 4, configurable via UV_THREADPOOL_SIZE) for operations that cannot be done asynchronously by the OS, such as file system operations, DNS lookups, and cryptography. While the event loop itself is single-threaded, these operations are offloaded to libuv's thread pool. When completed, the result callback is queued to the event loop; (3) Cross-platform I/O — abstracts OS differences (epoll on Linux, kqueue on macOS, IOCP on Windows) for consistent async I/O; (4) Timer management, signal handling, and child process utilities. Increasing UV_THREADPOOL_SIZE can improve performance for apps with heavy file system or cryptography workloads.

Open this question on its own page
05

What is the difference between cluster and Worker Threads?

Both Cluster and Worker Threads enable multi-core CPU utilization in Node.js, but they serve different purposes and work differently. Cluster spawns separate OS processes (each with its own V8 instance, memory, and event loop). They communicate via IPC (inter-process communication). The master distributes incoming connections among workers. Best for: scaling HTTP servers across cores, process isolation (a crashing worker doesn't affect others), memory-intensive apps where isolation is important. Worker Threads run separate JavaScript threads within the same process. They share memory (via SharedArrayBuffer/Atomics), have lower overhead than forking, and are suitable for parallel CPU computation. Best for: CPU-bound tasks (image processing, computation) without the memory duplication of separate processes. In practice: use Cluster (or PM2) to run multiple server instances across cores; use Worker Threads to offload specific CPU-heavy tasks from the main thread without spawning a full new process.

Open this question on its own page
06

What is backpressure in Node.js streams?

Backpressure is a mechanism in Node.js streams to prevent a fast readable source from overwhelming a slow writable destination. Without backpressure, data would pile up in memory until the process crashes with an out-of-memory error. The writable stream's write(chunk) method returns false when its internal buffer is full (exceeding the highWaterMark, default 16KB). The readable stream should pause (readable.pause()) at this signal and wait for the writable to emit the "drain" event before resuming. The pipe() method handles this automatically — it pauses the readable when the writable signals backpressure and resumes when it drains, making it the recommended approach for connecting streams. Manual backpressure management is needed when building custom Transform streams or implementing your own pipeline logic. Ignoring backpressure causes memory leaks in data-intensive applications like ETL pipelines and file processors.

Open this question on its own page
07

What is the difference between readFile and createReadStream?

fs.readFile() reads the entire file into memory as a Buffer or string before invoking the callback. For small files this is fine, but for large files (say, a 2GB log file or video), this allocates 2GB of memory at once — potentially crashing the process. fs.createReadStream() reads the file in configurable chunks (default 64KB) and emits them as "data" events, using only a small, constant amount of memory regardless of file size. Use readFile() for: small files, configuration files, templates — any file that comfortably fits in memory and needs to be processed as a whole. Use createReadStream() for: large files, serving file downloads over HTTP, CSV/JSON processing, audio/video streaming, piping data through transforms. Example: fs.createReadStream("large.mp4").pipe(res) efficiently streams a video to the HTTP response without buffering the whole file.

Open this question on its own page
08

What is JWT and how is it used in Node.js?

JWT (JSON Web Token) is an open standard (RFC 7519) for securely transmitting information between parties as a JSON object that is digitally signed. A JWT has three parts separated by dots: header.payload.signature — all Base64URL encoded. The header specifies the algorithm (HS256, RS256), the payload contains claims (user ID, roles, expiration), and the signature verifies the token was not tampered with. Usage in Node.js with the jsonwebtoken package: (1) Issue token on login: const token = jwt.sign({ userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: "24h" });; (2) Verify on protected routes: const decoded = jwt.verify(token, process.env.JWT_SECRET); — throws if invalid or expired; (3) Clients send the token in the Authorization: Bearer <token> header. JWTs are stateless — no session storage needed. Important: never store sensitive data in the payload (it is only Base64 encoded, not encrypted). Use short expiry + refresh tokens for better security.

Open this question on its own page
09

What is bcrypt and why is it used for passwords?

bcrypt is a password hashing algorithm designed specifically for storing passwords securely. Unlike general cryptographic hashes (MD5, SHA-256) which are designed to be fast, bcrypt is intentionally slow — making brute-force and rainbow table attacks computationally expensive. It automatically generates and incorporates a random salt (preventing rainbow table attacks) and has a configurable cost factor (work factor) that can be increased over time as hardware gets faster. The bcrypt npm package usage: (1) Hash on registration: const hash = await bcrypt.hash(password, 12); (12 is the salt rounds — higher = slower = more secure); (2) Verify on login: const match = await bcrypt.compare(plaintext, hash); — returns boolean. Never compare password hashes with === (timing attacks). Never store plaintext passwords. Bcrypt truncates at 72 bytes — for longer passwords, consider argon2 (the newer, recommended algorithm) which won the Password Hashing Competition.

Open this question on its own page
10

What is Mongoose and how does it relate to MongoDB?

Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It provides a schema-based solution to model your application data, with built-in type casting, validation, query building, and business logic hooks. MongoDB is a schemaless NoSQL database — documents within the same collection can have different structures. Mongoose adds a schema layer on top, enforcing structure and types. Key concepts: (1) Schema: defines document structure, types, validation, defaults: const userSchema = new Schema({ name: String, email: { type: String, required: true, unique: true } });; (2) Model: a class compiled from the schema for querying: const User = mongoose.model("User", userSchema);; (3) CRUD: User.create(), User.find(), User.findById(), User.findByIdAndUpdate(), User.deleteOne(); (4) Middleware (hooks): pre/post save, pre/post find — useful for hashing passwords or logging; (5) Population: populate() for joining documents across collections.

Open this question on its own page
11

What is connection pooling in Node.js database access?

Connection pooling is the practice of maintaining a cache of database connections that can be reused for multiple requests, rather than creating and destroying a new connection for every operation. Creating a new database connection involves TCP handshaking, authentication, and protocol negotiation — typically 20-100ms of overhead. With pooling, connections are established once at startup and reused, keeping latency low. Most database drivers for Node.js implement pooling automatically: pg (PostgreSQL) uses new Pool({ max: 20 }); Mongoose manages a connection pool internally (poolSize option); MySQL2 uses mysql.createPool(). Key pool settings: max (maximum connections — set based on DB server capacity and expected concurrency), min (minimum idle connections), acquireTimeout (wait time for a connection before error), and idleTimeoutMillis (close connections idle longer than this). Misconfiguring pool size can cause either wasted connections or request queuing under load.

Open this question on its own page
12

What is rate limiting and how do you implement it in Node.js?

Rate limiting restricts how many requests a client can make to your API within a time window, protecting against brute-force attacks, DDoS, and API abuse. Implementation in Express.js using express-rate-limit: const rateLimit = require("express-rate-limit"); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100, message: "Too many requests" }); app.use("/api/", limiter);. This allows 100 requests per 15 minutes per IP. For more sophisticated limiting (sliding window, token bucket) and distributed environments (multiple servers), use Redis-backed rate limiting with rate-limiter-flexible. Apply different limits to different routes: stricter limits on auth endpoints (/login: 5 attempts per 15 min) and looser on general API. Also consider: (1) IP-based limiting (default); (2) User/API-key based limiting for authenticated routes; (3) Setting Retry-After headers so clients know when to retry; (4) Returning 429 Too Many Requests status code.

Open this question on its own page
13

What is input validation and sanitization in Node.js?

Input validation ensures incoming data meets expected rules (required fields, correct types, valid formats) before processing. Sanitization cleans input by removing or escaping dangerous characters to prevent injection attacks. Both are essential security layers at API boundaries. Libraries: (1) Joi — powerful schema-based validation: const schema = Joi.object({ email: Joi.string().email().required(), age: Joi.number().min(18) }); const { error } = schema.validate(req.body);; (2) express-validator — middleware-based, integrates with Express route handlers; (3) Zod — TypeScript-first schema validation; (4) validator.js — string validators and sanitizers. Never trust user input: validate on the server even if you validate on the client. Common attacks prevented by validation: SQL injection (use parameterized queries too), NoSQL injection (validate types), XSS (sanitize HTML), command injection (validate shell inputs). Always return clear validation error messages to clients (400 Bad Request) without exposing internal details.

Open this question on its own page
14

What is the difference between authentication and authorization?

Authentication is the process of verifying who a user is — confirming their identity. Common mechanisms: username/password, OAuth (Google, GitHub), API keys, JWT tokens, session cookies. In Node.js, libraries like Passport.js handle authentication strategies. Authorization is the process of determining what an authenticated user is allowed to do — checking permissions and roles. A user may be authenticated but not authorized to access a specific resource. Example: all employees are authenticated (can log in), but only admins are authorized to delete user accounts. In Express.js, implement as separate middleware: (1) Auth middleware: verifies the JWT or session and attaches req.user; (2) Authorization middleware: checks req.user.role against required permissions for the route. Common authorization models: RBAC (Role-Based Access Control — roles like admin/user/moderator), ABAC (Attribute-Based — more granular, based on attributes), and ACL (Access Control Lists — per-resource permissions).

Open this question on its own page
15

What is session management in Node.js?

Session management maintains state across multiple HTTP requests for a user (HTTP is stateless by default). Server-side sessions store session data (user ID, cart, preferences) on the server, sending only a session ID cookie to the client. In Express.js, use express-session: app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, maxAge: 24 * 60 * 60 * 1000 } }));. By default, sessions are stored in memory (MemoryStore) — only suitable for development. In production, use persistent stores: Redis (connect-redis) for fast, shared session storage across multiple server instances; PostgreSQL or MongoDB (connect-mongo). Cookie security flags: httpOnly (prevents JS access, XSS protection), secure (HTTPS only), sameSite (CSRF protection). Sessions vs JWT: sessions are stateful (revocable, storage required); JWTs are stateless (no server storage, hard to revoke before expiry).

Open this question on its own page
16

What is Passport.js?

Passport.js is the most widely used authentication middleware for Node.js and Express. It has a modular strategy-based architecture — you install specific strategy packages for each authentication method: passport-local (username/password), passport-jwt (JSON Web Tokens), passport-google-oauth20 (Google OAuth), passport-github, etc. There are 500+ strategies available. Passport normalizes the authentication process: regardless of strategy, successful authentication calls done(null, user) which attaches the user to req.user. Integration with Express: app.use(passport.initialize()); (required) and app.use(passport.session()); (for session-based auth). Protect routes with passport.authenticate("jwt", { session: false }) as middleware. For OAuth, Passport handles the redirect flow and token exchange. Serialize/deserialize user to/from session: passport.serializeUser() and passport.deserializeUser().

Open this question on its own page
17

What is the difference between SQL and NoSQL databases and when to use each with Node.js?

SQL databases (PostgreSQL, MySQL, SQLite) use structured tables with a fixed schema, enforce ACID transactions, and use SQL for querying. They excel at complex queries with joins, strict data integrity, and well-defined relational data. Use SQL for: financial data, e-commerce (orders, inventory), applications needing complex reporting, or when data relationships are complex and well-understood. Node.js drivers: pg (PostgreSQL), mysql2, with ORMs like Prisma, Sequelize, TypeORM. NoSQL databases (MongoDB, Redis, Cassandra, DynamoDB) offer flexible schemas, horizontal scaling, and specific optimizations for particular data patterns. Use NoSQL for: document stores (MongoDB for content, user profiles, catalogs), key-value cache (Redis), real-time analytics (Cassandra for time-series), or when schema evolves rapidly. Node.js drivers: mongoose (MongoDB), ioredis (Redis). In practice, many applications use both — PostgreSQL for transactional data and Redis for caching and sessions.

Open this question on its own page
18

What is Redis and how is it used in Node.js applications?

Redis (Remote Dictionary Server) is an in-memory data structure store used as a database, cache, message broker, and session store. It is extremely fast (microsecond latency) because all data lives in RAM. Common Node.js use cases: (1) Caching: store expensive database query results with TTL — await client.setEx("users:list", 3600, JSON.stringify(users));; (2) Session storage: use connect-redis with express-session for distributed sessions; (3) Rate limiting: atomic increment operations make Redis ideal for counting requests per window; (4) Pub/Sub: real-time messaging between services; (5) Job queues: BullMQ uses Redis for reliable background job processing; (6) Leaderboards: sorted sets for real-time rankings. Node.js clients: ioredis (feature-rich, supports clusters and Sentinel) and redis (official client). Always set expiry (TTL) on cached keys to avoid stale data accumulation.

Open this question on its own page
19

What is GraphQL and how does it differ from REST in Node.js?

GraphQL is a query language for APIs and a runtime for executing those queries, developed by Facebook. Unlike REST which has fixed endpoints returning fixed data shapes, GraphQL exposes a single endpoint (/graphql) where clients specify exactly what data they need. Key differences: (1) Over-fetching: REST often returns more data than needed; GraphQL returns exactly what was requested; (2) Under-fetching: REST may require multiple requests for related data; GraphQL fetches all in one request; (3) Schema: GraphQL has a strongly typed schema that serves as a contract; (4) Versioning: REST often needs /v1, /v2; GraphQL evolves by adding fields (deprecated ones remain). Node.js implementations: Apollo Server (most popular), graphql-yoga, Mercurius (for Fastify). Best suited for: complex data graphs, multiple clients (mobile/web) with different data needs, rapid product iteration. REST is still better for simple CRUD, file uploads, and when HTTP caching is important.

Open this question on its own page
20

What is WebSocket and how do you implement it in Node.js?

WebSocket is a communication protocol that provides full-duplex (bidirectional), persistent connections over a single TCP connection. Unlike HTTP (request-response), WebSocket allows the server to push data to clients without a client request — essential for real-time features. The connection starts as HTTP then upgrades to WebSocket via the Upgrade header. Implementation in Node.js: (1) ws package (low-level): const wss = new WebSocket.Server({ port: 8080 }); wss.on("connection", ws => { ws.on("message", msg => ws.send(reply)); ws.send("Connected!"); });; (2) Socket.IO (higher-level): adds rooms, namespaces, reconnection, fallback to polling, and broadcast — io.to(room).emit("event", data). Use cases: chat applications, real-time dashboards, collaborative editing, live notifications, multiplayer games, financial tickers. For horizontal scaling with WebSockets, use Redis Pub/Sub as a message broker between multiple server instances (Socket.IO supports this with @socket.io/redis-adapter).

Open this question on its own page
21

What is the difference between monolithic and microservices architecture in Node.js?

A monolithic architecture packages all application functionality (auth, billing, notifications, user management) as a single deployable unit running in one process. Simpler to develop, test, and deploy initially; easier to debug (single log stream, single codebase); no network overhead between components. Drawbacks: as the app grows, it becomes harder to scale specific bottlenecks (must scale everything), slower deployments, technology lock-in, and risk of tight coupling. A microservices architecture decomposes the application into small, independently deployable services, each responsible for a single business capability. Each service has its own codebase, database, and deployment pipeline. Benefits: independent scaling, technology diversity (mix Node.js, Python, Go), isolated failures, parallel team development. Drawbacks: network latency between services, distributed transaction complexity, operational overhead (container orchestration, service discovery, monitoring). For Node.js, frameworks like NestJS and tools like gRPC, RabbitMQ, Apache Kafka, and Docker/Kubernetes support microservices. Start monolithic — migrate to microservices when clear bottlenecks or team scaling demands it.

Open this question on its own page
22

What is a message queue and why is it used in Node.js applications?

A message queue is a communication mechanism where producers add messages to a queue and consumers process them asynchronously, decoupling the sender from the receiver in time and space. Benefits: (1) Async processing — handle heavy work (email sending, PDF generation, payment processing) in background workers without blocking the HTTP request; (2) Reliability — if a worker fails, the message stays in the queue for retry; (3) Load leveling — absorb traffic spikes by queuing excess work; (4) Decoupling — services don't need to know about each other directly. Node.js solutions: BullMQ (Redis-based, most popular for Node.js — reliable, feature-rich with delayed jobs, priority, repeatable jobs); RabbitMQ with amqplib (AMQP protocol, advanced routing); Apache Kafka with kafkajs (high-throughput event streaming). Example: user registers → API immediately returns 200 → background job sends welcome email. This prevents slow email sending from delaying the API response.

Open this question on its own page
23

What is Prisma and how is it used in Node.js?

Prisma is a next-generation ORM (Object-Relational Mapper) for Node.js and TypeScript that supports PostgreSQL, MySQL, SQLite, SQL Server, MongoDB, and CockroachDB. It consists of three tools: (1) Prisma Schema — a declarative data model file (schema.prisma) that defines your database schema, relations, and client generator; (2) Prisma Client — an auto-generated, type-safe query builder: const users = await prisma.user.findMany({ where: { active: true }, include: { posts: true } });; (3) Prisma Migrate — schema migration management. Advantages over alternatives (Sequelize, TypeORM): full TypeScript type safety (auto-generated types from schema), intuitive API, powerful filtering/sorting/pagination, and an excellent developer experience. Database changes flow through migrations: (1) Modify schema.prisma; (2) Run npx prisma migrate dev to generate SQL migration and apply it; (3) Prisma Client is automatically updated. Prisma Studio provides a visual database browser.

Open this question on its own page
24

What is the Sequelize ORM in Node.js?

Sequelize is a mature, promise-based ORM for Node.js that supports PostgreSQL, MySQL, MariaDB, SQLite, and Microsoft SQL Server. It allows you to interact with databases using JavaScript objects instead of raw SQL. Key features: (1) Model definition: const User = sequelize.define("User", { name: DataTypes.STRING, email: { type: DataTypes.STRING, unique: true } });; (2) CRUD: User.create(), User.findAll(), User.findByPk(), User.update(), User.destroy(); (3) Associations: User.hasMany(Post), Post.belongsTo(User) — enables eager loading with include; (4) Migrations: versioned database schema changes; (5) Hooks: beforeCreate, afterUpdate — for side effects like password hashing; (6) Transactions: atomic operations with sequelize.transaction(async t => { ... });. While Sequelize is powerful, many newer projects prefer Prisma for its better TypeScript support and developer experience.

Open this question on its own page
25

What is TypeORM in Node.js?

TypeORM is an ORM for TypeScript and JavaScript that supports PostgreSQL, MySQL, SQLite, MongoDB, and more. It is particularly popular in TypeScript projects and with the NestJS framework. It supports two patterns: (1) Active Record: models extend BaseEntity and have CRUD methods directly — await user.save(); (2) Data Mapper: separate repository classes handle persistence — await userRepository.save(user) (more testable, recommended). Entities are defined with decorators: @Entity() class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @OneToMany(() => Post, post => post.user) posts: Post[]; }. TypeORM migrations track schema changes over time. Compared to Sequelize: better TypeScript and decorator support; compared to Prisma: TypeORM uses real TypeScript classes (Prisma generates its own types), but Prisma's query API is considered more ergonomic by many developers. TypeORM is the native ORM choice for NestJS applications.

Open this question on its own page
26

How do you implement logging in a Node.js application?

Effective logging is critical for debugging and monitoring Node.js production applications. console.log() is acceptable for development but insufficient for production. Production logging requirements: structured logs (JSON format for log aggregation), log levels (error, warn, info, debug), timestamps, contextual data, and transport options (file, stdout, external services). Libraries: (1) Winston — most popular, supports multiple transports, log rotation, custom formatters: const logger = winston.createLogger({ level: "info", format: winston.format.json(), transports: [new winston.transports.Console(), new winston.transports.File({ filename: "error.log", level: "error" })] });; (2) Pino — extremely fast (10x faster than Winston due to minimal sync operations), JSON output by default, used by Fastify; (3) Morgan — HTTP request logger middleware for Express. Best practices: log at the appropriate level, never log sensitive data (passwords, tokens, PII), use correlation IDs to trace requests across services, and send logs to centralized systems (Datadog, ELK Stack, CloudWatch) in production.

Open this question on its own page
27

What is PM2 and why is it used in production?

PM2 is a production process manager for Node.js applications. It handles critical production concerns that you shouldn't manage manually. Key features: (1) Process management: start, stop, restart, reload applications — pm2 start app.js --name myapp; (2) Auto-restart: automatically restarts the app if it crashes; (3) Cluster mode: spawns one worker per CPU core with zero-downtime reload — pm2 start app.js -i max; (4) Load balancing: distributes requests across workers; (5) Log management: aggregates logs from all workers — pm2 logs; (6) Monitoring: CPU and memory usage — pm2 monit; (7) Startup scripts: pm2 startup generates a systemd/init script so PM2 restarts after server reboots; (8) Ecosystem files: ecosystem.config.js for configuration as code. In containerized environments (Docker/Kubernetes), PM2 is less necessary since the orchestrator handles restarts and scaling, but it's still useful in bare-metal/VM deployments.

Open this question on its own page
28

What is Helmet.js and why should you use it?

Helmet.js is an Express middleware collection that sets various HTTP security headers to protect your application from common web vulnerabilities. Simply add app.use(helmet()) to your Express app. It sets/configures these headers by default: Content-Security-Policy (prevents XSS by controlling which resources browsers load), X-Frame-Options (prevents clickjacking by disabling iframe embedding), Strict-Transport-Security (forces HTTPS), X-Content-Type-Options: nosniff (prevents MIME type sniffing), X-XSS-Protection (legacy XSS filter), Referrer-Policy (controls referrer information), and removes the X-Powered-By: Express header (hides technology stack). Each middleware can be configured individually or disabled: helmet({ contentSecurityPolicy: false }). Helmet is not a silver bullet — it reduces the attack surface but doesn't replace proper input validation, parameterized queries, and authentication. It's a first-line defense that should be included in every Express production application.

Open this question on its own page
29

What is dependency injection in Node.js?

Dependency injection (DI) is a design pattern where a module's dependencies are provided (injected) from the outside rather than created internally. This decouples components, making them individually testable and interchangeable. Without DI: class UserService { constructor() { this.db = new Database(); } } — UserService is tightly coupled to a specific Database implementation, making it hard to test or swap. With DI: class UserService { constructor(db) { this.db = db; } } — the database is injected, so tests can pass a mock. In plain Node.js/Express, manual DI is common: create dependencies in one place and pass them to service constructors. DI containers (IoC containers) automate this: InversifyJS (decorator-based DI container), Awilix (simple, lightweight), NestJS (has DI built-in as a core feature, similar to Angular's DI system). DI is fundamental to writing SOLID code, particularly the Dependency Inversion Principle.

Open this question on its own page
30

How do you write unit tests for Node.js applications?

Testing Node.js applications involves three main levels: unit (test individual functions in isolation), integration (test multiple components together), and end-to-end (test the full application). Popular testing frameworks: (1) Jest — all-in-one framework by Meta: test runner, assertion library, mocking: test("adds numbers", () => { expect(add(2, 3)).toBe(5); });; (2) Mocha + Chai — flexible test runner with separate assertion library; (3) Vitest — Jest-compatible, faster, ESM-native. For HTTP endpoint testing: Supertest — makes HTTP requests against Express apps without a running server: const res = await request(app).post("/users").send(data).expect(201);. For mocking: jest.fn() for function mocks, jest.spyOn() to spy on methods, jest.mock("module") to mock entire modules. Key practices: test one thing per test, use descriptive test names, arrange-act-assert structure, avoid testing implementation details — test behavior. Run with npm test. Aim for high coverage on business logic, less on boilerplate.

Open this question on its own page
31

What is the difference between PUT and PATCH in REST APIs?

Both PUT and PATCH HTTP methods update resources, but with different semantics. PUT is a full replacement — the request body must contain the complete resource representation. Any fields not included are set to null/default. PUT /users/1 { name: "Alice", email: "a@b.com", age: 30 } replaces the entire user record. If only name is sent, email and age become null. PUT must be idempotent — making the same request multiple times produces the same result. PATCH is a partial update — only the fields included in the request body are updated; others remain unchanged. PATCH /users/1 { name: "Alice" } only updates the name field. PATCH is more efficient for updating a single field in a large document. In Express: handle both with separate route handlers, with PATCH using spread/merge logic: const updated = { ...existing, ...req.body };. Most practical REST APIs use PATCH for partial updates and PUT for full replacements, though many APIs use PATCH exclusively for simplicity.

Open this question on its own page
32

What is HTTP status codes and which ones are most important in Node.js APIs?

HTTP status codes communicate the result of a request. Essential codes for REST APIs: 2xx (Success): 200 OK (general success), 201 Created (resource created — return Location header), 204 No Content (success, no body — used for DELETE). 3xx (Redirection): 301 Moved Permanently, 302 Found (temporary redirect). 4xx (Client Error): 400 Bad Request (invalid input — validation failure), 401 Unauthorized (not authenticated — missing/invalid credentials), 403 Forbidden (authenticated but not authorized), 404 Not Found (resource doesn't exist), 409 Conflict (duplicate resource — unique constraint violation), 422 Unprocessable Entity (semantically invalid — often used for validation), 429 Too Many Requests (rate limited). 5xx (Server Error): 500 Internal Server Error (unexpected server error — log the details, show generic message to client), 502 Bad Gateway (upstream service error), 503 Service Unavailable (overloaded or maintenance). Return consistent error response shapes with status code, error code, and human-readable message: { "error": "RESOURCE_NOT_FOUND", "message": "User not found" }.

Open this question on its own page
33

What is environment-based configuration in Node.js?

Environment-based configuration separates configuration values (database URLs, API keys, feature flags, ports) from code, loading different values per environment (development, testing, staging, production). This follows the Twelve-Factor App methodology. Approaches: (1) Environment variables via process.env — the most portable, supported by all deployment platforms; (2) dotenv — loads .env files into process.env for local development; (3) Configuration modulesconfig npm package supports JSON/YAML/JS config files per environment with inheritance and CLI overrides; (4) Secrets managers — AWS Secrets Manager, HashiCorp Vault, Kubernetes secrets for production credentials (never hardcode in files). Best practices: validate required env vars on startup (fail fast if DB_URL is missing); provide type-safe config by parsing env vars into a config object at startup; use .env.example as documentation; never commit .env or secrets; use different JWT secrets, database URLs, and API keys per environment.

Open this question on its own page
Advanced 24 questions

Deep expertise questions for senior and lead roles.

01

How does Node.js handle concurrency without multiple threads?

Node.js achieves concurrency through its event loop and non-blocking I/O without using multiple OS threads for request handling. The key insight is that most server work is I/O-bound (waiting for databases, file system, network) rather than CPU-bound. While waiting for I/O, a thread in traditional models is blocked doing nothing. Node.js instead uses the OS's asynchronous I/O capabilities (via libuv): when an I/O operation is initiated, Node.js registers a callback and immediately returns to the event loop to handle other work. The OS notifies Node.js when I/O completes (via epoll, kqueue, or IOCP depending on the OS), and the callback is queued for the event loop to execute. This means a single thread can juggle thousands of concurrent I/O operations simultaneously — each "in flight" while the thread handles other requests. The cost: CPU-intensive operations (complex computations, synchronous operations) block the single thread, halting all other requests. Solutions: Worker Threads, child_process, or offloading CPU work to external services.

Open this question on its own page
02

What is the Node.js memory model and how does garbage collection work?

Node.js memory is divided into several regions. The V8 heap stores JavaScript objects and is where most memory management occurs. The heap has two main areas: New Space (Young Generation) — small (1-8MB), short-lived objects; collected frequently by the fast Scavenge algorithm (minor GC). Objects that survive multiple collections are promoted to Old Space (Old Generation) — larger, longer-lived objects; collected less frequently by the Mark-Sweep-Compact algorithm (major GC). Large Object Space — objects exceeding 256KB; never moved by GC. Code Space — JIT-compiled code. Memory outside the V8 heap includes Buffer allocations (C++ layer) and native bindings. Garbage collection pauses JavaScript execution (stop-the-world). V8 uses incremental marking and concurrent sweeping to minimize pause times. Monitor memory: process.memoryUsage() returns heapTotal, heapUsed, rss, external. Common leaks: forgotten event listeners, closures holding references, unbounded caches, circular references in pre-modern V8. Use Chrome DevTools heap snapshots or clinic.js to diagnose leaks.

Open this question on its own page
03

What are memory leaks in Node.js and how do you detect them?

A memory leak occurs when memory that is no longer needed is not released, causing the process's memory consumption to grow over time until it crashes or becomes severely degraded. Common causes in Node.js: (1) Unremoved event listeners: adding listeners with on() without ever calling off(); (2) Unbounded caches: storing data in objects/Maps without eviction; (3) Closures holding large objects: a callback closes over a large variable that can't be garbage collected; (4) Global variables: accidentally assigning to global scope; (5) Timers not cleared: setInterval without clearInterval; (6) Streams not properly closed. Detection: (1) process.memoryUsage().heapUsed — monitor over time; (2) --inspect flag + Chrome DevTools heap snapshots — take two snapshots and diff them to see what grew; (3) clinic.js (npm install -g clinic) — specialized Node.js performance toolkit; (4) heapdump npm package — generate heap snapshots programmatically; (5) node --expose-gc + V8 API for forced GC in tests.

Open this question on its own page
04

What is the difference between process.exit() and throwing an uncaught exception?

process.exit(code) immediately terminates the Node.js process with the given exit code (0 = success, non-zero = error). It bypasses the normal event loop drain — pending callbacks, I/O operations, and promises are abandoned. Before calling process.exit(), emit a "beforeExit" event yourself or use process.exitCode property (lets the event loop drain naturally first). Use process.exit() explicitly only in CLI tools after completing work, or in fatal error handlers after cleanup. Throwing an uncaught exception (an exception not caught by any try/catch) triggers the uncaughtException event on process. If no listener is registered, Node.js prints the stack trace and exits with code 1. If you register a listener, the process can continue (but the application is in an undefined state — this is dangerous). The recommended pattern: listen to uncaughtException and unhandledRejection for logging/cleanup, then call process.exit(1). Do not attempt to continue after uncaught exceptions. For Promise rejections: unhandledRejection will cause process exit by default in Node.js 15+.

Open this question on its own page
05

What is the N+1 query problem and how do you solve it in Node.js?

The N+1 query problem occurs when fetching a list of N records and then making one additional query per record to fetch related data — resulting in N+1 total queries instead of 1-2 efficient queries. Example: fetch 100 blog posts (1 query), then loop and fetch the author for each post (100 more queries) = 101 queries total. Solutions: (1) JOIN/populate in the initial query: fetch posts WITH author data in one query — SELECT posts.*, users.name FROM posts JOIN users ON posts.author_id = users.id or Mongoose Post.find().populate("author"); (2) Batch loading (DataLoader): the DataLoader library (by Facebook, used in GraphQL) batches individual lookups from multiple resolvers into a single query per event loop tick: const userLoader = new DataLoader(ids => User.findByIds(ids)); await userLoader.load(userId) — automatically batches parallel loads into one DB query; (3) Eager loading: in ORMs like Prisma/Sequelize, use include to load relations upfront; (4) Caching: cache frequently accessed lookup data in Redis.

Open this question on its own page
06

What is circuit breaking pattern in Node.js microservices?

The circuit breaker pattern prevents cascading failures in distributed systems by detecting when a downstream service is failing and stopping requests to it temporarily, allowing it time to recover. Named after electrical circuit breakers. States: (1) Closed (normal) — requests flow through, failures are counted; (2) Open — after reaching a failure threshold, the circuit "trips" — requests immediately fail without calling the downstream service (fast failure); (3) Half-Open — after a timeout, a limited number of test requests are allowed through; if they succeed, the circuit closes; if they fail, it opens again. Benefits: fast failure instead of waiting for timeouts, system stability, self-healing. Node.js implementations: opossum — popular Node.js circuit breaker library: const breaker = new CircuitBreaker(asyncFunction, { timeout: 3000, errorThresholdPercentage: 50, resetTimeout: 30000 }); breaker.fire(args); cockatiel — policy-based resilience library with circuit breaking, retry, timeout, and bulkhead patterns. Combine with retry patterns (exponential backoff with jitter) for comprehensive resilience.

Open this question on its own page
07

What is graceful shutdown in Node.js and how do you implement it?

Graceful shutdown is the process of cleanly stopping a Node.js server by: (1) stopping acceptance of new connections, (2) waiting for in-flight requests to complete, (3) closing database connections, message queue connections, and other resources. Without it, mid-flight requests are killed, causing errors for users and potential data corruption. Implementation: listen to shutdown signals — process.on("SIGTERM", shutdown) and process.on("SIGINT", shutdown) (Ctrl+C). In the shutdown handler: (1) call server.close(callback) — stops accepting new connections, waits for active connections; (2) close database pools: await pool.end();; (3) close Redis clients; (4) flush log buffers; (5) call process.exit(0) after cleanup. Add a timeout (e.g., 30s) to force exit if cleanup hangs: setTimeout(() => process.exit(1), 30000);. In Kubernetes, containers receive SIGTERM before SIGKILL (default 30s grace period) — configure this to allow enough time for your app's shutdown. Libraries like terminus simplify health checks and graceful shutdown for Express/Fastify.

Open this question on its own page
08

What is connection pooling for HTTP requests in Node.js?

HTTP connection pooling (keep-alive connections) reuses existing TCP connections for multiple HTTP requests to the same server, avoiding the overhead of TCP handshake and TLS negotiation for every request. Node.js's built-in http/https modules support this via http.Agent. By default, Node.js uses a global agent with keep-alive disabled (or limited). For high-throughput outbound HTTP requests: const agent = new https.Agent({ keepAlive: true, maxSockets: 50, maxFreeSockets: 10, timeout: 60000 }); const response = await fetch(url, { agent });. The axios library uses keep-alive by default. undici — the newer, high-performance HTTP client in Node.js core — uses connection pools internally and is much faster than the older http module. Key settings: maxSockets (max concurrent connections per host — higher increases throughput but consumes server resources), keepAlive (reuse TCP connections), and timeout (idle socket timeout). For microservices making many outbound calls, proper pool tuning can significantly reduce latency and CPU usage.

Open this question on its own page
09

What are common Node.js security vulnerabilities and how do you prevent them?

Common Node.js security vulnerabilities and mitigations: (1) Injection attacks: SQL injection — always use parameterized queries or ORMs, never concatenate user input into SQL; NoSQL injection — validate input types before MongoDB queries; Command injection — never pass user input to exec(); (2) XSS (Cross-Site Scripting): escape/sanitize output, use CSP headers (helmet), avoid res.send(userInput); (3) CSRF: use CSRF tokens or SameSite cookie attribute; (4) Insecure dependencies: run npm audit regularly, use Snyk or Dependabot to detect vulnerabilities; (5) Prototype pollution: validate object shapes, use Object.create(null) for dictionaries, avoid merge with untrusted input; (6) ReDoS (Regex DoS): avoid catastrophic backtracking regexes — use safe-regex to detect; (7) Timing attacks: use crypto.timingSafeEqual() for sensitive comparisons; (8) Directory traversal: validate file paths, use path.resolve() and check the result starts within the intended directory; (9) Secrets exposure: never log env vars or tokens; use secrets managers in production.

Open this question on its own page
10

What is the difference between horizontal and vertical scaling in Node.js?

Vertical scaling (scaling up) means increasing the resources of a single server — more CPU cores, more RAM, faster storage. For Node.js, this helps limited because a single Node.js process only uses one core. To utilize multiple cores, you must run multiple processes (cluster mode or PM2). Vertical scaling has a ceiling (you can only buy so much hardware) and involves downtime for hardware changes. Horizontal scaling (scaling out) means adding more servers/instances running the same application, distributing load with a load balancer (Nginx, HAProxy, AWS ALB). This provides near-unlimited scale and high availability — if one instance fails, others continue serving traffic. Node.js is well-suited for horizontal scaling because its stateless design works naturally with load balancing. Challenges of horizontal scaling: (1) Shared state: session data and caches must be in a shared external store (Redis) rather than process memory; (2) File uploads: uploaded files must go to shared storage (S3); (3) WebSocket affinity: sticky sessions or Redis pub/sub needed for Socket.IO; (4) Database bottleneck: scale the database too (read replicas, sharding). Design your app to be stateless from day one.

Open this question on its own page
11

What is Node.js streams backpressure and how does pipe() handle it?

When a fast readable stream produces data faster than a slow writable stream can consume it, data accumulates in memory — this is the backpressure problem. Without handling it, the process runs out of memory. The writable.write(chunk) method returns false when its internal buffer exceeds highWaterMark — signaling the source to slow down. The writable emits "drain" when it is ready for more data. Manual handling: const ok = writable.write(chunk); if (!ok) { readable.pause(); writable.once("drain", () => readable.resume()); }. readable.pipe(writable) handles this automatically and correctly: it subscribes to the writable's "drain" event and pauses/resumes the readable stream accordingly. For complex pipelines, use stream.pipeline(src, transform, dest, callback) (Node.js 10+) instead of pipe() — it properly handles errors from any stage and cleans up all streams, whereas pipe() leaves streams open on error. Always prefer pipeline() over pipe() in production code.

Open this question on its own page
12

What is the Twelve-Factor App methodology as it applies to Node.js?

The Twelve-Factor App is a methodology for building scalable, maintainable, cloud-native applications. Applied to Node.js: (1) Codebase: one codebase in version control; (2) Dependencies: explicitly declared in package.json — never rely on global packages; (3) Config: store config in environment variables (process.env) — not hardcoded; (4) Backing services: treat databases, queues as attached resources via URLs in config; (5) Build, release, run: separate stages — npm run build, tag release, node dist/app.js; (6) Processes: execute as stateless processes — no sticky sessions, no local file storage; (7) Port binding: self-contained HTTP service — app.listen(process.env.PORT); (8) Concurrency: scale via process model — cluster/PM2; (9) Disposability: fast startup, graceful shutdown on SIGTERM; (10) Dev/prod parity: minimize differences between environments — use Docker; (11) Logs: treat logs as event streams — write to stdout, let infrastructure route them; (12) Admin processes: run one-off admin tasks (migrations, scripts) in the same environment as the app.

Open this question on its own page
13

What is serverless computing and how does Node.js fit in?

Serverless computing (Functions as a Service — FaaS) allows you to deploy individual functions that run in response to events, without managing servers. The cloud provider (AWS Lambda, Google Cloud Functions, Azure Functions, Vercel, Netlify) handles provisioning, scaling, and infrastructure. Node.js is the most popular runtime for serverless functions due to its fast startup time, small footprint, and JavaScript's suitability for event-driven architectures. Benefits: no server management, automatic scaling (scales to zero when idle — saving cost), pay-per-invocation pricing, high availability built-in. Limitations: cold starts — first invocation after idle period has higher latency as the runtime initializes (Node.js cold starts are faster than Java/Python); execution time limits (AWS Lambda: 15 minutes max); stateless (no persistent connections — use connection proxies like RDS Proxy for databases); vendor lock-in. Best for: API endpoints, webhook handlers, background jobs, scheduled tasks, event processors. Less suitable for: long-running processes, WebSocket servers (though possible with API Gateway), heavy stateful computation. Frameworks: Serverless Framework, AWS SAM, SST for structured serverless development.

Open this question on its own page
14

What is the Observer pattern and how does it relate to Node.js EventEmitter?

The Observer pattern (also called Publish-Subscribe or Pub-Sub) is a behavioral design pattern where an object (subject/publisher) maintains a list of dependents (observers/listeners) and notifies them automatically when its state changes. Node.js's EventEmitter is a direct implementation of this pattern. The EventEmitter is the publisher; emitter.on("event", listener) registers observers; emitter.emit("event", data) notifies all observers synchronously. This is the backbone of Node.js's entire architecture — HTTP servers, streams, file watchers, and TCP sockets all extend EventEmitter. Advantages: loose coupling (publisher doesn't know who is listening), extensibility (add behavior without modifying the publisher), supports multiple handlers for the same event. Considerations: (1) Memory leaks from forgotten listeners — use once() for one-time handlers, off() to clean up; (2) Default listener limit is 10 per event (warnings in console) — increase with emitter.setMaxListeners(n); (3) Errors emitted via emit("error", err) will crash the process if no error listener is registered — always add one: emitter.on("error", handler).

Open this question on its own page
15

How do you optimize a slow Node.js API endpoint?

Optimizing a slow Node.js API endpoint requires systematic profiling before making changes. Steps: (1) Profile first: use Node.js's built-in profiler (node --prof + node --prof-process), Chrome DevTools (attach via node --inspect), or clinic.js to identify the actual bottleneck — don't guess; (2) Database queries: most slowness is here — add indexes on frequently queried fields, use EXPLAIN/EXPLAIN ANALYZE to understand query plans, batch queries (eliminate N+1), add caching for expensive queries; (3) Caching: cache database results in Redis with appropriate TTL — const cached = await redis.get(key) || await db.fetch(key); (4) Async optimization: run independent async operations in parallel with Promise.all() instead of sequential awaits; (5) Payload size: paginate large lists, select only needed fields, use compression (gzip/brotli with compression middleware); (6) CPU-bound code: offload to Worker Threads; (7) HTTP caching: set Cache-Control headers for public data; (8) CDN: serve static assets via CDN; (9) Connection pooling: ensure database pool is properly sized; (10) Load test: use k6, Artillery, or autocannon to verify improvements under load.

Open this question on its own page
16

What is NestJS and how does it differ from Express?

NestJS is a progressive, opinionated Node.js framework for building scalable, maintainable server-side applications. It is built on top of Express (or Fastify) but adds an architectural layer inspired by Angular. Key differences from Express: (1) Structure: Express is minimal and unopinionated — you decide the folder structure. NestJS enforces a module-based architecture (Controllers, Services, Modules, Guards, Interceptors, Pipes) that scales for large teams; (2) Dependency injection: NestJS has a built-in IoC container — injectable services are declared with @Injectable(); (3) TypeScript first: NestJS is built for TypeScript with full type safety throughout; (4) Decorators: @Controller("/users"), @Get(), @Post(), @Body(), @Param() — declarative route definition; (5) Built-in features: validation (class-validator integration), serialization, exception filters, guards (auth), interceptors (logging, transform), microservices support, GraphQL, WebSockets, CQRS. NestJS shines for enterprise applications with large teams. Express is better for simple APIs or when you want minimal overhead and full control.

Open this question on its own page
17

What is async context tracking in Node.js?

Async context tracking allows you to maintain contextual data (like a request ID, user info, or trace ID) across asynchronous boundaries without passing it as function parameters. In synchronous code, you could use a global variable, but async code jumps between different contexts. AsyncLocalStorage (introduced in Node.js 12.17 as a stable API) solves this: const { AsyncLocalStorage } = require("async_hooks"); const storage = new AsyncLocalStorage();. Start a context: storage.run({ requestId: uuid() }, () => { /* all async code here can access the store */ });. Access anywhere in the async chain: const ctx = storage.getStore(); ctx.requestId;. Common uses: (1) Request-scoped logging: automatically include request ID in every log line without thread-locals; (2) Distributed tracing: propagate trace context through async operations; (3) Per-request database transactions. AsyncLocalStorage replaced the older, lower-level async_hooks API which was complex and had significant performance overhead. Express.js example: in a middleware, create the store with storage.run({ requestId: req.id }, () => next()).

Open this question on its own page
18

What is OpenTelemetry and how is it used in Node.js?

OpenTelemetry (OTel) is an open-source observability framework providing APIs, SDKs, and tools for generating, collecting, and exporting telemetry data: traces (request flows across services), metrics (counters, histograms, gauges), and logs. It is vendor-neutral — export to Jaeger, Zipkin, Datadog, New Relic, or any OTel-compatible backend. Node.js setup: (1) Install: @opentelemetry/sdk-node, @opentelemetry/auto-instrumentations-node; (2) Initialize before loading app code: const sdk = new NodeSDK({ traceExporter: new OTLPTraceExporter(), instrumentations: [getNodeAutoInstrumentations()] }); sdk.start();; (3) Auto-instrumentation automatically instruments Express routes, HTTP calls, database queries (pg, mongodb, redis) without code changes. Traces show the full lifecycle of a request across services, including which function was slow. Metrics track request rates, error rates, and latency percentiles (p50, p95, p99). Baggage propagates key-value data (user ID, feature flags) alongside trace context. OTel is essential for debugging distributed systems and understanding production performance.

Open this question on its own page
19

What is the Module Federation and how does it apply to Node.js?

Module Federation is a Webpack 5 feature primarily used in frontend microfrontend architectures — it allows separately built JavaScript applications to share code at runtime. While it originated on the frontend, it applies to Node.js in specific scenarios. In Node.js microservices, you can use Module Federation to share common code (shared utilities, validation schemas, TypeScript types, API client SDKs) between services at runtime without duplicating the code in each service's bundle. A host application can dynamically load remote modules from other services at runtime: const remoteModule = await import("remoteApp/sharedUtils");. This avoids the need to publish and version shared packages to npm for internal modules that change frequently. However, for most Node.js applications, npm workspaces or monorepos (Turborepo, Nx) are more practical approaches to sharing code between services without the runtime complexity of Module Federation. Module Federation in Node.js is most relevant in large, rapidly evolving microservice ecosystems where teams need truly independent deployments with shared runtime dependencies.

Open this question on its own page
20

How does HTTPS work and how do you set up TLS in Node.js?

HTTPS adds TLS (Transport Layer Security) on top of HTTP, providing encryption, data integrity, and server authentication. TLS uses asymmetric cryptography (public/private key pair) for key exchange and symmetric encryption for data transfer. Setting up HTTPS in Node.js natively: (1) Obtain a TLS certificate — Let's Encrypt (free, automated via Certbot), or generate self-signed for development: openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes; (2) Create HTTPS server: const https = require("https"); const options = { key: fs.readFileSync("key.pem"), cert: fs.readFileSync("cert.pem") }; https.createServer(options, app).listen(443);. In production, TLS termination is usually handled by a reverse proxy (Nginx, HAProxy, AWS ALB) which decrypts HTTPS and forwards plain HTTP to Node.js — simpler certificate management and better performance. For Express apps deployed on Heroku, Render, or similar PaaS platforms, HTTPS is handled automatically at the platform level. Always redirect HTTP to HTTPS: listen on port 80 and respond with 301 redirect to https://.

Open this question on its own page
21

What is HTTP/2 and does Node.js support it?

HTTP/2 is the second major version of the HTTP protocol, providing significant performance improvements over HTTP/1.1: (1) Multiplexing: multiple requests/responses over a single TCP connection simultaneously (HTTP/1.1 allows only one at a time per connection, leading to head-of-line blocking); (2) Header compression (HPACK): HTTP headers are compressed, reducing overhead significantly for repeated headers; (3) Server Push: server can proactively send resources (CSS, JS) before the client requests them; (4) Binary protocol: more efficient parsing than HTTP/1.1's text protocol. Node.js has a built-in http2 module: const server = http2.createSecureServer({ key, cert }, (req, res) => { res.end("Hello HTTP/2"); }); — requires TLS. Express.js does not natively support HTTP/2 (it uses Node's http module). For HTTP/2 with Express, use spdy package or switch to Fastify (supports HTTP/2 natively). Most production deployments use Nginx or a load balancer as an HTTP/2 terminator, forwarding HTTP/1.1 to Node.js internally — simpler than enabling HTTP/2 in Node.js directly.

Open this question on its own page
22

What is the difference between Fastify and Express?

Express and Fastify are both Node.js web frameworks but with different design philosophies. Express (2010): minimal, widely adopted, huge ecosystem, synchronous middleware model, no built-in schema validation, no built-in serialization — it's the "safe" choice with the most tutorials, plugins, and community support. Fastify (2016): designed for performance from the ground up. Key differences: (1) Performance: Fastify is 2-4x faster than Express in benchmarks — it uses fast-json-stringify for serialization and find-my-way for routing; (2) Schema validation: Fastify has first-class JSON Schema support for request validation and response serialization (faster + safer); (3) Async/await: Fastify natively supports async route handlers without the wrapper pattern Express requires; (4) Plugins: Fastify uses an encapsulated plugin system with fastify-plugin for dependency scoping; (5) TypeScript: Fastify has excellent TypeScript support out of the box; (6) Error handling: more consistent async error propagation. Choose Express for familiarity/ecosystem; choose Fastify for performance-critical services or greenfield projects.

Open this question on its own page
23

What is a Proxy in Node.js and how is it used?

A Proxy in JavaScript (and therefore Node.js) is an ES6 object that wraps another object (the target) and intercepts operations on it using handler functions called traps. The Proxy constructor takes a target object and a handler: const proxy = new Proxy(target, handler);. Common traps: get(target, prop) — intercepts property access; set(target, prop, value) — intercepts property assignment; has(target, prop) — intercepts in operator; apply(target, thisArg, args) — intercepts function calls; construct(target, args) — intercepts new. Use cases in Node.js: (1) Validation: validate property assignments automatically; (2) Lazy loading: defer expensive object initialization until first access; (3) Mocking in tests: intercept method calls without modifying the original object; (4) Observable objects: track changes to state (used internally by Vue 3's reactivity); (5) API client generation: generate method calls dynamically from property access patterns; (6) Logging/debugging: log all property accesses. Reflect API complements Proxy by providing default implementations of each trap.

Open this question on its own page
24

What is Domain-Driven Design (DDD) and how is it applied in Node.js?

Domain-Driven Design (DDD) is a software design approach that focuses on modeling software to match the business domain, with collaboration between technical and domain experts. Key concepts applied to Node.js: (1) Domain models: rich objects with business logic (not anemic data containers) — a User class with methods like user.changeEmail() that enforce business rules; (2) Value Objects: immutable objects defined by their attributes, no identity — Money { amount, currency }, Email { value }; (3) Aggregates: cluster of entities treated as a single unit — Order (aggregate root) contains OrderLine entities; (4) Repositories: abstract data access behind an interface — UserRepository.findById() — separating domain from infrastructure; (5) Domain Events: record things that happened — UserRegistered, OrderPlaced; (6) Application Services: orchestrate domain objects for use cases; (7) Bounded Contexts: clear boundaries between subdomains (auth context, billing context). In Node.js, DDD works well with TypeScript (for type-safe domain models) and NestJS (for clear architectural boundaries). It is most valuable for complex business domains, not simple CRUD apps.

Open this question on its own page
Back to All Topics 98 questions total