All topics

Node.js Interview Questions & Answers

40 questions with detailed answers — for freshers and experienced candidates.

Want to actually learn Node.js?

Join a hands-on mini internship or training on iCampusLink and earn a certificate.

Explore programs →

Fresher Level

Q1. What is Node.js and what are its key features?

Node.js is an open-source, cross-platform JavaScript runtime environment that executes JavaScript code outside a web browser. Built on Chrome's V8 JavaScript engine, it's designed for building scalable network applications. Key features include its asynchronous, event-driven architecture, making it highly efficient for I/O-bound tasks. It uses a non-blocking I/O model, allowing it to handle many concurrent connections without creating a new thread for each. Node.js has a rich ecosystem of libraries through npm (Node Package Manager) and is popular for building web servers, APIs, microservices, and real-time applications like chat servers.

Q2. Explain the single-threaded nature of Node.js and how it achieves concurrency.

Node.js itself is single-threaded for its JavaScript execution, meaning it has one main thread that processes all application logic. However, it achieves concurrency through its event-driven, non-blocking I/O model and the Event Loop. When an I/O operation (like reading a file or making a network request) is initiated, Node.js offloads it to the underlying `libuv` library, which uses a thread pool for these operations. Once the I/O operation completes, `libuv` places a callback in the Event Loop's queue, which the main thread then picks up and executes. This allows the single JavaScript thread to continue processing other requests while waiting for I/O operations to finish, giving the illusion of concurrency.

Q3. Describe the Node.js Event Loop in simple terms.

The Node.js Event Loop is a core mechanism that enables Node.js to perform non-blocking I/O operations. It's a continuous loop that checks for tasks to perform and executes them. When Node.js starts, it initializes the Event Loop. Any asynchronous operation (like a file read or network request) registers a callback. Instead of waiting, Node.js continues processing other code. Once an asynchronous operation completes, its callback is placed in a queue. The Event Loop constantly monitors these queues and, when the main call stack is empty, it picks up pending callbacks from the queues and executes them. This allows Node.js to handle many concurrent operations efficiently with its single JavaScript thread.

Q4. How does Node.js handle I/O operations asynchronously?

Node.js handles I/O operations asynchronously by leveraging its event-driven architecture and the `libuv` library. When an I/O operation (e.g., reading a file, network request) is invoked, Node.js doesn't block the main thread waiting for it to complete. Instead, it delegates the operation to `libuv`, which uses a pool of worker threads to handle these blocking tasks in the background. Once the I/O operation finishes, `libuv` places a callback function into the Event Loop's queue. The Event Loop, running on the main thread, continuously checks this queue and executes the callback when the call stack is empty. This non-blocking approach allows Node.js to maintain responsiveness and handle multiple concurrent operations efficiently.

Q5. What is `npm` and what is its primary purpose?

`npm` stands for Node Package Manager. It is the default package manager for Node.js and the world's largest software registry. Its primary purpose is to help Node.js developers discover, install, manage, and share reusable code packages (modules). Developers use `npm` to install project dependencies, either locally (within a project) or globally (available system-wide). It also allows publishing custom packages to the npm registry for others to use. The `package.json` file is central to `npm`, defining a project's metadata and its dependencies, making dependency management straightforward.

Q6. How do you include external modules in a Node.js application?

Node.js supports two primary module systems for including external modules: CommonJS and ES Modules. For **CommonJS** (the traditional system, default for `.js` files unless `type: "module"` is set in `package.json`), you use the `require()` function. It synchronously loads the module and returns its `module.exports` object.
// CommonJS (e.g., my_module.js)
const myModule = require('./my_module');
const express = require('express'); // npm package
For **ES Modules** (the official JavaScript standard, enabled by `type: "module"` in `package.json` or `.mjs` extension), you use the `import` statement. It's asynchronous and supports static analysis.
// ES Modules (e.g., main.mjs)
import { myFunction } from './my_module.mjs';
import express from 'express'; // npm package
Node.js determines the module type based on the `package.json` `"type"` field or file extensions.

Q7. Explain the difference between `module.exports` and `exports`.

In Node.js CommonJS modules, both `module.exports` and `exports` are used to expose functionality. The crucial difference is that `module.exports` is the actual object that gets returned when a module is `require()`d. `exports` is initially just a reference to `module.exports` (i.e., `exports = module.exports = {}`). When you add properties to `exports`, you're adding them to the object referenced by `module.exports`:
// module.js
exports.name = 'Alice'; // module.exports now has a 'name' property
exports.sayHello = () => console.log('Hello');
However, if you reassign `exports` itself, you break this reference. `module.exports` remains the original empty object, and your reassignment to `exports` will not be visible externally:
// module.js
exports = { name: 'Bob' }; // This assignment changes `exports` reference, not `module.exports`
module.exports = { name: 'Charlie' }; // This directly assigns to the object returned by require()
To export a single value (e.g., a function or a class), you *must* assign directly to `module.exports`.

Q8. What is `package.json` and what information does it contain?

`package.json` is a manifest file in Node.js projects that stores metadata about the project and its dependencies. It's essential for `npm` (Node Package Manager). Key information it contains includes: project `name`, `version`, `description`, `main` entry point, `scripts` for common tasks (e.g., `start`, `test`), `author`, `license`, and crucially, `dependencies` (packages required for production) and `devDependencies` (packages required for development/testing). It ensures project reproducibility and simplifies dependency management by allowing `npm install` to set up all required packages.

Q9. How do you create a basic HTTP server using Node.js?

A basic HTTP server in Node.js is created using the built-in `http` module. You use `http.createServer()` to instantiate a server, which accepts a callback function. This function executes for every incoming HTTP request, receiving `request` (an `IncomingMessage` object) and `response` (a `ServerResponse` object) as arguments. The `response` object is used to send data back to the client, allowing you to set HTTP headers (e.g., `Content-Type`) and write the response body. Finally, `server.listen()` starts the server on a specified port and optionally an IP address, making it accessible to clients.
const http = require('http');

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

const PORT = 3000;
server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Q10. What are callbacks in Node.js and why are they used?

Callbacks are functions passed as arguments to other functions, to be executed later once a particular operation has completed. In Node.js, callbacks are fundamental to its asynchronous, non-blocking nature. Since many operations (like file I/O, network requests, database queries) take time, Node.js doesn't wait for them to finish. Instead, it initiates the operation and provides a callback function. Once the operation completes, Node.js invokes the callback, often passing any results or errors. This allows the main thread to remain free, preventing the application from blocking and ensuring responsiveness.

Q11. Differentiate between a global and a local module installation using npm.

A **local module installation** using `npm install <package-name>` installs the package into the `node_modules` directory within your current project. It's available only to that specific project and is listed in its `package.json` dependencies. This is the default and recommended way for project dependencies. A **global module installation** using `npm install -g <package-name>` installs the package into a system-wide directory. These packages are typically command-line tools (CLIs) that you want to use from any directory in your terminal, like `nodemon` or `create-react-app`. They are not tied to a specific project and are not listed in `package.json`.

Q12. What are environment variables and how can you access them in Node.js?

Environment variables are dynamic, named values that can affect the way running processes behave on a computer. They provide a way to configure applications outside of their code, making it easy to change settings (like database credentials, API keys, or port numbers) without modifying the source. This is crucial for different deployment environments (development, staging, production). In Node.js, you can access environment variables through the global `process.env` object. For example, `process.env.PORT` would give you the value of the `PORT` environment variable. For development, packages like `dotenv` are often used to load variables from a `.env` file.

Intermediate Level

Q1. Explain Promises and their advantages over traditional callbacks.

Promises are objects representing the eventual completion or failure of an asynchronous operation. A Promise can be in one of three states: pending, fulfilled (resolved), or rejected. They provide a cleaner, more structured way to handle asynchronous code compared to nested callbacks (callback hell). Advantages include: improved readability and maintainability by flattening the code structure (chaining `.then()` calls), better error handling with a single `.catch()` block for a chain of operations, and the ability to combine multiple asynchronous operations using methods like `Promise.all()` or `Promise.race()`. Promises make asynchronous flow control more predictable and easier to reason about.

Q2. How do `async/await` syntax improve asynchronous code readability and error handling?

`async/await` is syntactic sugar built on top of Promises, designed to make asynchronous code look and behave more like synchronous code, significantly improving readability. An `async` function implicitly returns a Promise, and `await` can only be used inside an `async` function. `await` pauses the execution of the `async` function until the Promise it's waiting for settles (resolves or rejects). This eliminates `.then()` chains, making the flow easier to follow. For error handling, `async/await` allows using traditional `try...catch` blocks, which is more intuitive than `.catch()` for handling errors in a sequence of asynchronous operations, further enhancing code clarity and maintainability.

Q3. Describe the different phases of the Node.js Event Loop in detail.

The Node.js Event Loop operates in distinct phases, each with its own queue of callbacks: 1. **timers**: Executes `setTimeout()` and `setInterval()` callbacks. 2. **pending callbacks**: Executes I/O callbacks deferred to the next loop iteration. 3. **idle, prepare**: Internal to Node.js. 4. **poll**: Retrieves new I/O events, executes I/O related callbacks (except close, timers, and `setImmediate()`), and checks for `setImmediate()` callbacks if empty. 5. **check**: Executes `setImmediate()` callbacks. 6. **close callbacks**: Executes `close` event callbacks (e.g., `socket.on('close', ...)`). Between each phase, Node.js checks the microtask queues (`process.nextTick()` and Promise callbacks) and executes them before moving to the next macro-task phase. This structured approach ensures efficient and predictable execution of asynchronous operations.

Q4. What are Node.js Streams? Name and briefly describe the four types.

Node.js Streams are abstract interfaces for working with streaming data. They are instances of `EventEmitter` and provide an efficient way to handle large amounts of data or data that arrives in chunks, without loading everything into memory at once. This makes them ideal for I/O operations like file handling or network communication. The four types are: 1. **Readable Streams**: For reading data (e.g., `fs.createReadStream`). 2. **Writable Streams**: For writing data (e.g., `fs.createWriteStream`). 3. **Duplex Streams**: Both Readable and Writable (e.g., `net.Socket`). 4. **Transform Streams**: Duplex streams that can modify or transform data as it's written and read (e.g., `zlib.createGzip`). Streams promote efficient resource usage and enable piping data from one stream to another.

Q5. Explain the concept of Buffers in Node.js and provide a use case.

Buffers in Node.js are a global class that handles binary data directly. They are fixed-size chunks of raw memory allocated outside the V8 JavaScript engine's heap. This means they are not resizable and hold sequences of integers, each representing a byte. Buffers are crucial for interacting with low-level I/O operations, networking protocols, and file systems, where data is often transmitted or stored in binary format. A common use case is handling image data, video streams, or cryptographic operations where raw byte manipulation is necessary.

Q6. What is the `EventEmitter` in Node.js? Provide a practical example.

The `EventEmitter` is a fundamental class in Node.js that enables event-driven architecture. It allows you to create objects that can emit named events and register multiple listener functions that will be invoked when those events occur. Many core Node.js modules, like `fs.ReadStream` or `http.ServerRequest`, inherit from or utilize `EventEmitter` to signal changes or completion of operations. It's crucial for building decoupled and responsive applications where different parts of your code need to communicate without direct dependencies.
const EventEmitter = require('events');

class MyCustomEmitter extends EventEmitter {}

const myEmitter = new MyCustomEmitter();

myEmitter.on('userLoggedIn', (username) => {
  console.log(`${username} has logged in!`);
});

myEmitter.emit('userLoggedIn', 'Alice'); // Output: Alice has logged in!
myEmitter.emit('userLoggedIn', 'Bob');   // Output: Bob has logged in!
This example shows a custom emitter emitting a `userLoggedIn` event, and a listener reacting to it.

Q7. How do you manage errors effectively in asynchronous Node.js code, especially with Promises and async/await?

Effective error management in asynchronous Node.js code is crucial. For Promises, errors are handled using the `.catch()` method, which catches rejections anywhere in a Promise chain. It's good practice to always include a `.catch()` at the end of a chain. With `async/await`, error handling becomes more synchronous-looking, using standard `try...catch` blocks around `await` calls. This allows catching both synchronous errors and rejected Promises. Uncaught Promise rejections can be handled globally using `process.on('unhandledRejection', ...)`. Additionally, using custom error classes can provide more descriptive error messages and facilitate conditional error handling.

Q8. Explain the `child_process` module and differentiate between `spawn`, `exec`, and `fork`.

The `child_process` module allows Node.js to spawn new processes, executing system commands or other Node.js scripts. This enables interaction with the operating system and parallel execution of CPU-bound tasks. - `spawn()`: Spawns a new process asynchronously. It returns a `ChildProcess` object with streams (`stdin`, `stdout`, `stderr`) for communication. It's efficient for large amounts of data or long-running processes as it streams data. - `exec()`: Spawns a shell and runs a command, buffering the output before returning it. It's simpler for small outputs and when shell features (like pipes) are needed. - `fork()`: A special case of `spawn()` specifically for spawning new Node.js processes. It establishes an IPC (Inter-Process Communication) channel, allowing parent and child processes to exchange messages using `send()` and `on('message')`. It's ideal for running multiple Node.js instances to utilize multi-core CPUs.

Q9. What is the purpose of the Node.js `cluster` module and how does it utilize multiple CPU cores?

The Node.js `cluster` module enables a single Node.js application to create multiple worker processes that share the same server port. Its primary purpose is to leverage multi-core CPU systems, as a single Node.js process runs on only one core. The `cluster` module creates a master process that forks worker processes. Each worker runs an independent instance of the application. The master process can then distribute incoming connections among its workers using a round-robin approach (or other load-balancing algorithms depending on the OS). This allows the application to handle more concurrent requests and improve overall performance and resilience by distributing the load across available CPU cores.

Q10. Differentiate between CommonJS and ES Modules in Node.js.

CommonJS is Node.js's original module system, using `require()` for importing and `module.exports` or `exports` for exporting. It's synchronous, meaning modules are loaded one by one. ES Modules (ESM) are the official JavaScript standard, using `import` and `export` statements. ESM is asynchronous, allowing for static analysis, tree-shaking (removing unused code), and better interoperability with browsers. Node.js supports both. Projects can specify `"type": "module"` in `package.json` for ESM or use `.mjs` file extensions for ESM and `.cjs` for CommonJS. ESM offers better future-proofing and modern tooling integration.

Q11. How do you debug a Node.js application?

Node.js applications can be debugged using several methods. The most common approach is using the built-in Node.js debugger accessible via `node --inspect <your-app.js>`. This command starts the application and opens a WebSocket port for debugging. You can then connect to this port using Chrome DevTools (by navigating to `chrome://inspect`), VS Code's debugger, or other IDEs. This allows setting breakpoints, stepping through code, inspecting variables, and modifying runtime values. Alternatively, simple `console.log()` statements are often used for quick debugging, though they are less powerful than a dedicated debugger.

Q12. Explain the difference between `process.nextTick()` and `setImmediate()`.

`process.nextTick()` and `setImmediate()` are both used to defer execution of a function, but they operate in different phases of the Event Loop. - `process.nextTick()` callbacks are executed immediately after the current operation completes, but *before* the Event Loop proceeds to the next phase. They are considered microtasks and have higher priority than `setImmediate()` and `setTimeout` callbacks. - `setImmediate()` callbacks are executed in the 'check' phase of the Event Loop, after I/O operations and `poll` phase callbacks, but before 'close' callbacks. They are considered macrotasks. Essentially, `nextTick` runs 'now, but not yet', ensuring code runs before any I/O, while `setImmediate` runs 'later, after current I/O'.

Q13. What is middleware in the context of Express.js? Provide an example of a custom middleware.

In Express.js, middleware functions are handlers that execute in the middle of a request-response cycle. They have access to the `req` (request), `res` (response), and `next` (next middleware function) objects. Middleware can perform various tasks: executing code, modifying request/response objects, ending the cycle, or calling `next()` to pass control to the next middleware. They are used for logging, authentication, parsing data, error handling, and more.
const express = require('express');
const app = express();

// Custom logging middleware
const loggerMiddleware = (req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next(); // Pass control to the next middleware/route handler
};

app.use(loggerMiddleware); // Apply middleware globally

app.get('/', (req, res) => {
  res.send('Hello from Express!');
});

app.listen(3000, () => console.log('Server running on port 3000'));
Here, `loggerMiddleware` logs request details before the actual route handler processes the request.

Q14. Describe the `fs` module and common operations like reading and writing files.

The Node.js `fs` (File System) module provides an API for interacting with the file system, offering both synchronous and asynchronous methods. Asynchronous methods are generally preferred to prevent blocking the Event Loop. **Common operations:** - **Reading files**: `fs.readFile()` reads the entire file into memory. For large files, `fs.createReadStream()` is used to read data in chunks.
    fs.readFile('file.txt', 'utf8', (err, data) => { /* handle data */ });
    
- **Writing files**: `fs.writeFile()` writes data, overwriting existing content. `fs.appendFile()` adds data to the end. `fs.createWriteStream()` is for writing large amounts of data efficiently.
    fs.writeFile('new_file.txt', 'Hello Node!', (err) => { /* handle err */ });
    
- **Deleting files**: `fs.unlink(path, callback)` deletes a file. - **Checking existence**: `fs.access(path, mode, callback)` checks file existence and permissions asynchronously. - **Directory operations**: `fs.mkdir()`, `fs.readdir()`, `fs.rmdir()` (or `fs.rm` for recursive) manage directories. The asynchronous nature of `fs` operations is vital for maintaining application responsiveness.

Q15. What are some common security vulnerabilities in Node.js applications and how can you mitigate them?

Common security vulnerabilities in Node.js applications include: 1. **Injection Attacks (SQL, NoSQL, Command)**: Mitigate by using parameterized queries, ORMs, and input sanitization. 2. **Cross-Site Scripting (XSS)**: Mitigate by sanitizing user input and encoding output. 3. **Cross-Site Request Forgery (CSRF)**: Mitigate with CSRF tokens. 4. **Insecure Dependencies**: Regularly update npm packages, use tools like `npm audit` or Snyk. 5. **Authentication/Authorization Flaws**: Implement robust authentication (e.g., JWT with proper signing), enforce strong passwords, and proper access control checks. 6. **Information Disclosure**: Avoid exposing sensitive data in error messages or logs. Use environment variables for secrets. 7. **Denial of Service (DoS)**: Implement rate limiting, input validation, and efficient resource management. Best practices include input validation, output encoding, using secure headers, keeping dependencies updated, and following the principle of least privilege.

Q16. How does Node.js handle database connections and transactions in an asynchronous manner?

Node.js handles database connections and transactions asynchronously using non-blocking I/O. Database drivers (like `pg` for PostgreSQL or `mongoose` for MongoDB) are designed to return Promises or accept callbacks. When a query is executed, Node.js sends the request to the database and immediately returns control to the Event Loop without waiting for the database response. The database driver, often leveraging `libuv`'s thread pool, handles the actual blocking I/O with the database. Once the database responds, the callback or Promise resolution is queued in the Event Loop. For transactions, drivers provide methods (e.g., `beginTransaction`, `commit`, `rollback`) that are also asynchronous, allowing multiple queries within a transaction to be executed non-blockingly, typically within an `async/await` block for clear sequential logic.

Advanced Level

Q1. Deep dive into the Event Loop: Explain the roles of microtask and macrotask queues.

The Event Loop manages two types of queues: microtask and macrotask queues. - **Macrotasks** (or tasks) are processed in phases of the Event Loop (timers, poll, check, etc.). Examples include `setTimeout()`, `setInterval()`, `setImmediate()`, and I/O callbacks. The Event Loop processes one macrotask per cycle. - **Microtasks** have a higher priority. After each macrotask phase completes, and before the Event Loop moves to the next phase, all pending microtasks are executed. Examples include `process.nextTick()` and Promise callbacks (`.then()`, `.catch()`, `.finally()`). If a microtask queues more microtasks, they will also be executed within the same microtask phase. This ensures that Promise resolutions are handled promptly, often before other scheduled macrotasks.

Q2. Discuss strategies for optimizing the performance of a Node.js application.

Optimizing Node.js performance involves several strategies: 1. **Asynchronous Operations**: Ensure all I/O-bound operations are non-blocking. Use Promises and `async/await` effectively. 2. **CPU-bound Tasks**: Offload heavy computations to worker threads (`worker_threads` module) or separate microservices to prevent blocking the Event Loop. 3. **Clustering**: Utilize the `cluster` module to distribute load across multiple CPU cores. 4. **Database Optimization**: Optimize queries, use indexing, connection pooling, and caching. 5. **Caching**: Implement in-memory caches (e.g., Redis, Memcached) for frequently accessed data. 6. **Stream Processing**: Use streams for large data operations to reduce memory footprint. 7. **Profiling and Monitoring**: Use tools like Node.js Inspector, `clinic.js`, or APM services to identify bottlenecks. 8. **Load Balancing**: Distribute traffic across multiple Node.js instances. 9. **Gzip Compression**: Compress HTTP responses to reduce network latency. 10. **Minimize Dependencies**: Use only necessary packages to reduce bundle size and startup time.

Q3. Explain memory management in Node.js, specifically focusing on the V8 garbage collector and potential memory leaks.

Node.js relies on the V8 JavaScript engine for memory management, which uses a generational garbage collector (GC). V8 divides the heap into 'new space' (for young objects, frequently collected) and 'old space' (for long-lived objects, less frequent but more expensive full GC). The GC automatically reclaims memory occupied by objects no longer referenced. **Memory leaks** occur when objects are unintentionally kept in memory, preventing the GC from reclaiming them. Common causes include: - **Global variables**: Objects referenced by globals are never collected. - **Closures**: If a closure captures a large object and is kept alive, the object won't be collected. - **Event Emitters**: Not removing listeners after use can lead to objects being retained. - **Caches**: Unbounded caches can grow indefinitely. - **Timers**: `setInterval` or `setTimeout` not cleared can keep callbacks and their captured scope alive. Debugging memory leaks involves profiling tools like Node.js Inspector, `heapdump`, or `clinic.js` to analyze heap snapshots.

Q4. What is backpressure in Node.js Streams and how can it be handled effectively?

Backpressure in Node.js Streams occurs when a Writable stream (consumer) cannot process data as quickly as a Readable stream (producer) is generating it. If not handled, this can lead to memory exhaustion as the Writable stream's internal buffer overflows. Effective handling involves: 1. **Piping**: The `pipe()` method automatically handles backpressure. When the destination stream's buffer is full, it emits a `drain` event, and `pipe()` pauses the source stream until the `drain` event is emitted. 2. **Manual Control**: For more granular control, listen for the `drain` event on the Writable stream. When `writable.write(chunk)` returns `false`, it signals backpressure. The Readable stream should then `pause()` until the Writable stream emits `drain`, upon which the Readable stream can `resume()`.

Q5. When and why would you use Node.js Worker Threads instead of `child_process` or the `cluster` module?

Worker Threads, introduced in Node.js v10.5.0, are ideal for CPU-bound tasks that need to run in parallel without blocking the Event Loop, while sharing memory. - Use **Worker Threads** when you need to run heavy computational tasks (e.g., complex calculations, image processing, data encryption) within the same Node.js process, and potentially share `ArrayBuffer`s or `SharedArrayBuffer`s for efficient data exchange. They are suitable for tasks that are too intensive for the main thread but don't require spawning entirely separate processes, offering better resource efficiency than `child_process` for such cases. - Use **`child_process`** (especially `fork`) for spawning separate Node.js processes, suitable for executing external scripts or long-running, independent tasks that don't need to share memory directly with the parent. - Use the **`cluster` module** for load balancing incoming network connections across multiple Node.js processes to utilize all CPU cores for I/O-bound tasks, providing high availability and scalability at the application level.

Q6. How would you implement real-time communication using WebSockets in Node.js?

To implement real-time communication with WebSockets in Node.js, you typically use a library like `ws` or `socket.io`. 1. **`ws` library**: Provides a barebones WebSocket server and client implementation. You'd create an `http` server, then attach a `WebSocket.Server` to it. Listen for `connection` events to handle new clients, `message` events for incoming data, and `close` or `error` for disconnections. You'd use `ws.send()` to send data to a specific client and iterate through connected clients to broadcast messages. 2. **`socket.io` library**: A more feature-rich library that builds on WebSockets, adding fallback options (like long polling) for older browsers, automatic reconnection, multiplexing, and rooms. It simplifies event handling and broadcasting significantly. You'd integrate it with an `http` server, then listen for `connection` events on the `io` instance, and custom events on the client `socket` object. `socket.emit()` sends to a specific client, `io.emit()` broadcasts to all, and `io.to('room').emit()` sends to a specific room.

Q7. Describe an architectural approach for scaling a Node.js application horizontally and vertically.

Scaling a Node.js application involves both horizontal and vertical strategies: **Vertical Scaling (Scaling Up)**: Increasing resources (CPU, RAM) of a single server. This is limited by hardware capacity and eventually hits diminishing returns. It's often the first step but not a long-term solution for high traffic. **Horizontal Scaling (Scaling Out)**: Adding more servers/instances to distribute the load. This is the preferred method for Node.js due to its single-threaded nature. 1. **Clustering**: Use Node.js's `cluster` module to fork multiple worker processes, utilizing all CPU cores on a single machine. The master process acts as a load balancer. 2. **Load Balancers**: Place a reverse proxy (e.g., Nginx, HAProxy) or a cloud-based load balancer (e.g., AWS ALB) in front of multiple Node.js instances (each potentially running a cluster). This distributes requests across instances. 3. **Microservices**: Break down the monolithic application into smaller, independent services. Each service can be scaled independently based on its specific load requirements, potentially using different technologies. 4. **Statelessness**: Design applications to be stateless, meaning no session data is stored on the server. Session data should be externalized to a shared store (e.g., Redis) or managed client-side (e.g., JWT). This allows any instance to handle any request. 5. **Database Scaling**: Scale the database independently (read replicas, sharding).

Q8. What are some common anti-patterns in Node.js development, and how can they be avoided?

Common Node.js anti-patterns and their avoidance: 1. **Callback Hell (Pyramid of Doom)**: Deeply nested callbacks making code hard to read and maintain. **Avoid by**: Using Promises, `async/await`, or named functions. 2. **Blocking the Event Loop**: Performing CPU-intensive synchronous operations on the main thread. **Avoid by**: Offloading CPU-bound tasks to Worker Threads or separate processes (`child_process`), or using asynchronous libraries. 3. **Ignoring Error Handling**: Not handling errors in asynchronous operations, leading to crashes or unpredictable behavior. **Avoid by**: Always using `.catch()` with Promises, `try...catch` with `async/await`, and proper error-first callbacks. 4. **Over-reliance on Global Variables**: Storing state in global variables, leading to concurrency issues and memory leaks. **Avoid by**: Encapsulating state within modules or passing data explicitly. 5. **Unbounded Event Listeners**: Adding event listeners without removing them, causing memory leaks. **Avoid by**: Removing listeners when no longer needed (`emitter.removeListener()`) or using `once()`. 6. **Not Using Streams for Large Data**: Loading entire large files into memory. **Avoid by**: Using `fs.createReadStream()` and `fs.createWriteStream()` for efficient data processing. 7. **Synchronous File I/O in Request Handlers**: Using `fs.readFileSync()` in an HTTP request handler. **Avoid by**: Always using asynchronous `fs` methods.

Q9. How do you ensure high availability and fault tolerance in a Node.js microservices architecture?

Ensuring high availability (HA) and fault tolerance (FT) in a Node.js microservices architecture involves several strategies: 1. **Redundancy & Load Balancing**: Deploy multiple instances of each microservice behind a load balancer. If one instance fails, traffic is routed to healthy ones. 2. **Containerization & Orchestration**: Use Docker for containerization and Kubernetes for orchestration. Kubernetes can automatically restart failed containers, scale services, and manage deployments. 3. **Circuit Breakers**: Implement circuit breakers (e.g., with `opossum`) to prevent cascading failures. If a service is unresponsive, the circuit breaks, failing fast instead of waiting, and allowing the service to recover. 4. **Health Checks**: Implement `/health` endpoints for each service to allow load balancers and orchestrators to monitor their status and remove unhealthy instances from rotation. 5. **Graceful Shutdown**: Ensure services can shut down gracefully, completing ongoing requests before terminating. 6. **Distributed Tracing & Monitoring**: Use tools like OpenTelemetry, Prometheus, and Grafana to monitor service health, performance, and trace requests across services. 7. **Idempotent Operations**: Design APIs to be idempotent where possible, allowing safe retries without unintended side effects. 8. **Asynchronous Communication**: Use message queues (e.g., RabbitMQ, Kafka) for inter-service communication to decouple services and provide resilience against temporary outages.

Q10. Discuss advanced error handling patterns in Node.js, such as centralized error handling and domain-specific errors.

Advanced error handling in Node.js aims for robustness and maintainability: 1. **Centralized Error Handling**: Implement a global error handler for Express.js using a middleware function that takes four arguments `(err, req, res, next)`. This catches errors propagated by `next(err)` and handles uncaught exceptions using `process.on('uncaughtException', ...)` and `process.on('unhandledRejection', ...)`. This ensures a consistent response format and logging for all errors. 2. **Domain-Specific Errors (Custom Errors)**: Create custom error classes that extend `Error` (e.g., `class ValidationError extends Error { ... }`). This allows for more granular error categorization, making it easier for the centralized handler to determine the appropriate HTTP status code and response message based on the error type. It also improves code readability and debuggability. Custom errors should include a `statusCode` or `isOperational` property to distinguish between trusted (expected) and untrusted (programming) errors.

Q11. Explain how `libuv` plays a crucial role in Node.js's non-blocking I/O and event-driven model.

`libuv` is a multi-platform support library written in C that provides Node.js with its asynchronous I/O capabilities and implements the Event Loop. It acts as an abstraction layer over the operating system's underlying asynchronous I/O mechanisms (e.g., epoll on Linux, kqueue on macOS, I/O Completion Ports on Windows). Its crucial roles include: 1. **Event Loop Implementation**: `libuv` manages the entire Event Loop, scheduling callbacks for I/O and timers. 2. **Asynchronous I/O**: It handles non-blocking network and file I/O operations. When Node.js requests an I/O operation, `libuv` takes over, using OS-specific asynchronous APIs or its internal thread pool for potentially blocking operations (like file system access). 3. **Thread Pool**: For operations that cannot be made truly non-blocking by the OS (e.g., CPU-bound tasks or certain file system operations), `libuv` maintains a pool of worker threads. It offloads these tasks to the thread pool, preventing the main Node.js thread from blocking. 4. **Cross-Platform Consistency**: `libuv` provides a consistent API across different operating systems, abstracting away platform-specific differences in I/O and concurrency models. In essence, `libuv` is the backbone that allows Node.js's single JavaScript thread to perform concurrent I/O operations without blocking, making it highly efficient for network applications.

Q12. How would you implement a graceful shutdown mechanism for a Node.js server to prevent data loss or service disruption?

Implementing a graceful shutdown mechanism is crucial to prevent data loss and ensure service continuity during server restarts or deployments. The goal is to stop accepting new requests, finish existing ones, and then close resources. Steps: 1. **Listen for Termination Signals**: Trap `SIGTERM` (sent by process managers like PM2, Kubernetes) and `SIGINT` (Ctrl+C). 2. **Stop Accepting New Connections**: Call `server.close()` on the HTTP server. This stops the server from accepting new connections but allows existing connections to complete. 3. **Wait for Active Connections to Finish**: Keep track of open connections (e.g., by incrementing a counter on `connection` and decrementing on `close`). Use a timeout to force shutdown if connections persist too long. 4. **Close Database Connections**: Ensure all database connections and other external resources are properly closed. 5. **Exit Process**: Once all tasks are complete, or the timeout is reached, call `process.exit(0)`. This ensures that client requests in flight are completed, database transactions are committed, and resources are released cleanly.
Prepared by iCampusLink. 40 Node.js interview questions.
Top 40 Node.js Interview Questions & Answers (2026) | iCampusLink