Node.js is a server-side JavaScript runtime built on Chrome's V8 engine, known for its non-blocking, event-driven architecture. These questions cover core concepts, async patterns, streams, performance, security, and real-world usage commonly tested in backend and full-stack engineering interviews.
Node.js is a runtime that executes JavaScript outside the browser using Chrome's V8 engine. It provides server-side APIs (fs, http, path, etc.) instead of browser APIs (DOM, window), and it can handle file systems, networking, and processes directly.
The Event Loop is the mechanism that allows Node.js to perform non-blocking I/O by offloading operations to the OS and processing callbacks when those operations complete. It cycles through phases: timers, pending callbacks, idle/prepare, poll, check, and close callbacks.
require() is the CommonJS (CJS) module system, synchronous and available by default in Node.js. import is ES Module (ESM) syntax, which is asynchronous, statically analyzable, and supported in Node.js v12+ via .mjs files or "type":"module" in package.json.
process.nextTick() fires before the next Event Loop iteration, after the current operation completes. setImmediate() fires in the check phase of the current Event Loop iteration. setTimeout(fn, 0) fires in the timers phase, which can be after setImmediate() depending on context.
The cluster module allows you to fork multiple child processes that share the same server port, enabling Node.js to take advantage of multi-core CPUs. Since Node.js is single-threaded, clustering is a common way to scale CPU-bound workloads and improve throughput.
A Readable stream is a source you consume data from (e.g., fs.createReadStream), while a Writable stream is a destination you write data to (e.g., fs.createWriteStream). They can be connected via pipe(), and Transform streams implement both interfaces simultaneously.
Backpressure occurs when a writable stream cannot process data as fast as a readable stream produces it, causing memory build-up. Node.js streams handle this by returning false from write() when the internal buffer is full and emitting a 'drain' event when it's safe to write again.
Synchronous errors are caught with try/catch. Asynchronous errors in callbacks follow the error-first convention (cb(err, data)), while Promise-based and async/await code uses .catch() or try/catch. Unhandled rejections should be caught with process.on('unhandledRejection').
Node.js provides the child_process module with four main methods: spawn() for streaming I/O, exec() for buffered shell commands, execFile() for executing a file directly, and fork() for spawning Node.js processes with an IPC channel. Use spawn() for large outputs to avoid buffer overflow.
Worker Threads (worker_threads module, stable since Node 12) run JavaScript in parallel OS threads, sharing memory via SharedArrayBuffer. They are ideal for CPU-intensive tasks (image processing, cryptography) where the Event Loop would otherwise block, unlike child processes which have separate memory spaces.
Middleware are functions with the signature (req, res, next) that execute sequentially in a request-response cycle. They can read/modify the request or response, end the cycle, or pass control to the next middleware by calling next(). Common uses include logging, authentication, and parsing.
Callback hell can be avoided by using named functions instead of anonymous nesting, adopting Promises with .then()/.catch() chaining, or using async/await for flat, readable asynchronous code. Modularizing code into smaller functions also reduces nesting depth.
When a module is first required, Node.js loads and executes it, then caches the exported object in require.cache. Subsequent require() calls for the same module return the cached instance without re-executing the file, making module singletons the default behavior.
Key practices include: validating and sanitizing all user input, using helmet for HTTP security headers, avoiding eval() and child_process with user input, keeping dependencies updated (npm audit), using environment variables for secrets, and rate-limiting APIs to prevent DoS attacks.
app.use() mounts middleware or a router for all HTTP methods and optionally a path prefix (default '/'), matching any request whose path starts with it. app.get() registers a route handler only for HTTP GET requests on an exact path match.
Node.js uses V8's garbage collector with generational collection (new/old space). Common causes of memory leaks include global variables that grow indefinitely, uncleaned event listeners, closures holding large references, and unresolved Promises. Tools like --inspect, heapdump, and Clinic.js help diagnose them.
package-lock.json records the exact resolved versions and dependency tree of all installed packages, ensuring deterministic installs across environments. It should be committed to version control; npm ci uses it exclusively to guarantee reproducible builds.
Rate limiting can be implemented using middleware libraries like express-rate-limit, which tracks requests per IP using an in-memory store or Redis for distributed systems. Redis-backed solutions (e.g., rate-limiter-flexible) are preferred in multi-instance deployments to share counters across nodes.
The async_hooks module provides an API to track the lifecycle of asynchronous resources (init, before, after, destroy callbacks). It is used to implement context propagation (e.g., request-scoped logging, distributed tracing with AsyncLocalStorage) across async boundaries without modifying function signatures.
Cold observables/streams produce data per subscriber (each consumer gets its own source), while hot ones share a single source among all consumers. Node.js Readable streams are cold by default—each pipe or read call gets its own read cycle—but can be made hot by using PassThrough or broadcasting via EventEmitter patterns.
© RM Full Stack & AI Engineer · All interview questions · Roadmaps · Open the app