Chapter 17 of 17
Top 50 Node.js Interview Questions
Node.js internals and production practices - the Event Loop, streams, libuv, child processes, security, and scalable architecture - organized by topic and difficulty, with explanations that go beyond a one-line definition and a runnable code example where it helps. Search or filter to find what you need, and share a direct link to any question.
Showing 50 of 50 questions
The first 15 of 50 questions are free to read. Sign up free to unlock every answer and track your progress.
Unlock all 50Node.js uses an event-driven, non-blocking I/O model. The main thread runs the Event Loop, but heavy I/O (file reads, network requests, DB queries) is offloaded to the OS kernel or libuv's thread pool. Node registers a callback and moves on immediately; once the operation completes, the callback is queued and run by the Event Loop. A common mistake is assuming single-threaded means no parallelism at all - I/O still runs concurrently via libuv - and blocking the main thread with heavy synchronous CPU work freezes the whole server. Keep the main thread free for request handling and offload CPU-intensive work to Worker Threads or separate services.
Example
console.log("1. Start");
setTimeout(() => {
console.log("2. Async Operation Done");
}, 1000);
console.log("3. End");
// Output: 1. Start -> 3. End -> 2. Async Operation DoneBoth schedule code to run asynchronously, but at different points. `process.nextTick()` fires immediately after the current operation completes, before the Event Loop moves to its next phase - it has higher priority than `setImmediate()`. `setImmediate()` fires in the Check phase, after I/O callbacks are processed. A common mistake is using `process.nextTick()` recursively, which starves the Event Loop and blocks I/O from ever running, or assuming `setImmediate()` runs instantly rather than on the next loop iteration. Use `setImmediate()` to queue work behind pending I/O, and `process.nextTick()` only when something must run before the loop proceeds (e.g. handling an error).
Example
console.log("Start");
setImmediate(() => console.log("setImmediate"));
process.nextTick(() => console.log("process.nextTick"));
console.log("End");
// Output: Start -> End -> process.nextTick -> setImmediateA stub is a dummy function or object used during testing to simulate existing code, so tests can prevent side effects (like writing to a real database) and force specific code paths (like an error) without depending on real infrastructure. A common mistake is confusing stubs (which simulate behavior) with mocks (which verify behavior/expectations), and forgetting to call `.restore()` afterward, which can leak state into later tests. Use a library like Sinon.js or Jest for clean stubbing, and always restore stubs in an `afterEach` hook.
Example
const sinon = require("sinon");
const fs = require("fs");
const readFileStub = sinon.stub(fs, "readFile");
readFileStub.yields(new Error("File not found"), null);
fs.readFile("test.txt", (err) => {
if (err) console.log("Caught expected error:", err.message);
});
readFileStub.restore();`module.exports` is the actual object returned when a module is required; `exports` is just a shorthand variable that initially points to it. Attaching properties to `exports` (e.g. `exports.add = ...`) works fine, and reassigning `module.exports` entirely also works - but reassigning `exports` itself (e.g. `exports = { ... }`) breaks the reference, so nothing gets exported. If you're exporting a single class or function, assign it directly to `module.exports`; if exporting several utilities, attaching them to `exports.funcName` is fine.
Example
// Valid: attaching to exports
exports.add = (a, b) => a + b;
// Valid: overwriting module.exports
module.exports = { add: (a, b) => a + b };
// INVALID: breaks the reference to module.exports
exports = { add: (a, b) => a + b };A Node.js convention where a callback's first argument is reserved for an error object (`null` on success) and the second for the result data. It lets callers check `if (err)` immediately rather than guessing whether an operation succeeded. A common mistake is forgetting to `return` inside the `if (err)` block, letting execution continue and crash when it touches undefined data - and throwing inside a callback, which can't be caught by a surrounding `try/catch`. Always check `if (err)` first, and consider wrapping legacy callback-based functions with `util.promisify` to use `async/await` instead.
Example
const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error reading file:', err);
return;
}
console.log('File contents:', data);
});Both come from `child_process` and create new processes. `spawn()` runs an arbitrary command (like `ls` or `git`) and streams its stdout/stderr back, with no dedicated V8 instance or IPC channel by default. `fork()` is a special case of spawn specifically for launching Node.js scripts - it creates a new V8 instance and opens an IPC channel so parent and child can `.send()` messages to each other. A common mistake is using `fork` for non-Node.js commands, or spawning too many forked processes, since each one costs its own memory (30MB+) and V8 instance. Use `spawn` for external tools/shell commands, and `fork` (or `worker_threads`) for heavy computation within Node.js.
Example
const { spawn, fork } = require("child_process");
const ls = spawn("ls", ["-lh", "/usr"]);
const child = fork("./heavy-script.js");
child.send({ start: true });
child.on("message", (msg) => console.log("From child:", msg));`package.json` is the manifest for a Node.js project: metadata (name, version, description, author), `dependencies`/`devDependencies` for runtime vs. development-only packages, `scripts` for command shortcuts (`npm start`, `npm test`), `main` for the entry point, and `engines` to pin compatible Node/npm versions. A common mistake is hand-editing version numbers without understanding SemVer symbols (`^` for compatible minor, `~` for compatible patch), or putting production packages like `express` into `devDependencies`. Use `npm install --save`/`--save-dev` to keep the file accurate, and commit `package-lock.json` for deterministic installs.
REPL stands for Read-Eval-Print Loop - the interactive shell that ships with Node.js: it Reads input, Evaluates it, Prints the result, and Loops back for the next input. You open it by running `node` with no arguments; it is great for quick syntax checks or exploring standard-library behavior. A common mistake is trying to write complex multi-file logic entirely inside it, or forgetting that variables disappear once you exit. The `_` variable holds the result of the last evaluated expression.
`Buffer` handles raw binary data in Node.js - fixed-length sequences of bytes needed for reading files, handling TCP streams, or processing images, since JavaScript strings historically weren't built for binary data. A common mistake is concatenating strings and buffers without converting properly, or using `Buffer.allocUnsafe()` without immediately filling it, which can expose old, potentially sensitive memory contents. Prefer `Buffer.alloc()` for safe, zero-filled buffers and `Buffer.from()` to convert existing data.
Example
const buf = Buffer.from('Hello');
console.log(buf); // <Buffer 48 65 6c 6c 6f>
console.log(buf.toString()); // 'Hello'
console.log(buf[0]); // 72Middleware functions have access to the request, the response, and the next middleware in the chain. They can execute code, modify req/res, end the cycle, or call `next()` to hand off control - making them ideal for cross-cutting concerns like logging, authentication, and body parsing. A common mistake is forgetting to call `next()`, which leaves the request hanging forever, or calling `res.send()` and then still calling `next()`, causing a 'headers already sent' error. Order matters: place global middleware like parsers before your route definitions.
Example
const logger = (req, res, next) => {
console.log(`${req.method} request to ${req.url}`);
next();
};
app.use(logger);
app.get('/', (req, res) => res.send('Home Page'));`dependencies` are packages the app needs at runtime in production (express, mongoose, react), installed with `npm install <package>`. `devDependencies` are needed only for development/testing (jest, eslint, nodemon), installed with `--save-dev`. Running `npm install --production` (common in CI/CD) skips `devDependencies` entirely. A common mistake is putting testing or build tools into `dependencies`, bloating the production install; keep runtime needs strictly separate from build-time needs.
Run `npm outdated` to see which packages have newer versions available, `npm update` to bump packages to the latest version allowed by the SemVer ranges in `package.json` (e.g. 1.2.1 to 1.2.9, but not 2.0.0), or `npm install <package>@latest` to force the absolute latest. A common mistake is expecting `npm update` to perform major version jumps (it respects the `^` caret), or deleting `package-lock.json` to 'fix' issues, which defeats deterministic builds. Use tools like Dependabot/Renovate for automated updates, and always re-run tests afterward.
In browsers the global object is `window`; in Node.js it's `global`. Anything attached to it (like `setTimeout`, `console`, `process`, `Buffer`) is available everywhere without an import. A common mistake is polluting `global` with custom variables, which causes hard-to-debug naming collisions, or assuming `window`/`document` exist in Node (they don't). Avoid attaching custom data to `global` - export it as a module instead.
Node ships with built-in modules that need no npm install: `fs` (file system), `http`/`https` (servers and requests), `path` (file path utilities), `events` (the EventEmitter class), `os` (operating system info), and `crypto` (hashing/encryption). A common mistake is trying to `npm install fs` (it's built-in), or concatenating path strings by hand instead of using `path.join()`, which breaks across Windows/Linux. Prefer built-ins over external libraries for simple tasks.
npm (Node Package Manager) is Node's default package manager, serving two purposes: a CLI tool to install/manage/publish packages, and a massive online registry of open-source packages at npmjs.com. A common mistake is confusing npm (the tool) with Node.js (the runtime), or using `npm install -g` for packages that should be project-local dependencies. Prefer local installs for project dependencies, and `.npmrc` files for project-level config.