Fundamentals of Node.js — Interview Questions & Answers

50 essential Node.js interview questions covering the event loop, asynchronous programming, modules, streams, Express, and error handling.

Meritshot19 min read
Node.jsJavaScriptBackendInterview QuestionsWeb Development
Back to Interview Guides

Node.js Basics

1. What is Node.js?

Node.js is an open-source, cross-platform JavaScript runtime built on Google Chrome's V8 engine that lets you execute JavaScript outside the browser, typically on the server. It uses an event-driven, non-blocking I/O model that makes it lightweight and efficient for building scalable network applications. Node.js is especially well suited for I/O-heavy workloads such as APIs, real-time services, and streaming applications.

2. Is Node.js a programming language or a framework?

Node.js is neither a programming language nor a framework; it is a runtime environment that executes JavaScript code on the server side. The programming language you write is JavaScript (or TypeScript that compiles to JavaScript), while frameworks like Express, Fastify, or NestJS are built on top of Node.js. Understanding this distinction helps clarify that Node.js provides the engine and core libraries, not the application structure.

3. What is the V8 engine and how does Node.js use it?

V8 is Google's high-performance, open-source JavaScript and WebAssembly engine written in C++ that compiles JavaScript directly to native machine code using just-in-time (JIT) compilation. Node.js embeds V8 to parse and execute JavaScript while adding its own bindings for system-level features such as the file system, networking, and timers through C++ APIs. This combination gives Node.js fast execution speeds along with access to operating-system resources that browsers do not expose.

4. What are the main features of Node.js?

Node.js is known for its asynchronous and non-blocking I/O, single-threaded event loop, and exceptional speed thanks to the V8 engine. It includes a rich standard library, the npm package ecosystem, and strong support for building real-time and microservice architectures. It is also cross-platform, runs on Windows, macOS, and Linux, and benefits from a large, active open-source community.

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

Browser JavaScript runs inside a sandboxed environment with access to the DOM, window, and browser APIs, but no direct access to the file system or operating system. Node.js runs JavaScript on the server with access to system resources through modules like fs, http, and os, but it has no DOM or window object. Both share the same core language and V8 engine, yet they target fundamentally different environments and use cases.

6. What is the global object in Node.js?

In Node.js, the global object is the top-level scope, analogous to window in the browser, and it holds globally available functions and properties such as setTimeout, console, and process. Variables declared with var at the module level are not added to global because each file is treated as a module with its own scope. You should avoid polluting the global object, as it can lead to naming conflicts and hard-to-track bugs.

7. What is the process object in Node.js?

The process object is a global that provides information about and control over the current Node.js process. It exposes properties and methods such as process.env for environment variables, process.argv for command-line arguments, process.cwd() for the working directory, and process.exit() to terminate the process. It also emits events like exit, uncaughtException, and SIGINT, making it central to configuration and lifecycle management.

8. How do you read environment variables in Node.js?

Environment variables are accessed through the process.env object, for example process.env.PORT or process.env.DATABASE_URL. They are commonly used to store configuration and secrets so that values are not hardcoded into the source. Tools like the dotenv package can load variables from a .env file into process.env during local development, keeping sensitive data out of version control.

9. What is REPL in Node.js?

REPL stands for Read-Eval-Print Loop, an interactive shell that reads user input, evaluates it, prints the result, and loops to await the next command. You can start it by simply running node in your terminal without a file argument, which is useful for quickly testing snippets, experimenting with APIs, or debugging. It supports multiline expressions, command history, and special dot commands such as .help and .exit.

10. How do you check the version of Node.js installed?

You can check the installed Node.js version by running node -v or node --version in the terminal, and the npm version with npm -v. Within a script, the version is available through process.version and detailed information through process.versions. Tools like nvm (Node Version Manager) help you install and switch between multiple Node.js versions on the same machine.

Event Loop & Asynchronous Programming

11. What is the event loop in Node.js?

The event loop is the mechanism that allows Node.js to perform non-blocking I/O despite JavaScript being single-threaded. It continuously checks the call stack and, when it is empty, moves queued callbacks from completed asynchronous operations into the stack for execution. This design enables Node.js to handle many concurrent connections efficiently without spawning a thread per request.

12. What are the phases of the Node.js event loop?

The event loop runs in several ordered phases: timers (executing setTimeout and setInterval callbacks), pending callbacks, idle/prepare (internal use), poll (retrieving new I/O events), check (executing setImmediate callbacks), and close callbacks. Between phases, the microtask queue containing promise callbacks and process.nextTick callbacks is drained. Understanding these phases helps explain the ordering of asynchronous callbacks in complex applications.

13. Is Node.js single-threaded or multi-threaded?

Node.js executes JavaScript on a single main thread using the event loop, which is why it is commonly called single-threaded. However, under the hood it relies on the libuv library, which uses a thread pool to handle certain operations like file system access and DNS lookups in parallel. You can also create additional threads explicitly with the worker_threads module for CPU-intensive tasks.

14. What is libuv?

libuv is a multi-platform C library that provides Node.js with its asynchronous I/O capabilities, including the event loop, thread pool, file system operations, and networking. It abstracts platform-specific differences so Node.js can run consistently across operating systems. The thread pool in libuv, which defaults to four threads, handles operations that cannot be done asynchronously at the OS level.

15. What is the difference between synchronous and asynchronous code?

Synchronous code executes sequentially, blocking the thread until each operation finishes before moving to the next line. Asynchronous code initiates an operation and continues executing subsequent lines, handling the result later through a callback, promise, or async/await. In Node.js, favoring asynchronous APIs is critical because synchronous (blocking) calls can stall the event loop and degrade the throughput of the entire application.

16. What are callbacks and what is callback hell?

A callback is a function passed as an argument to another function and invoked once an asynchronous operation completes. Callback hell, sometimes called the "pyramid of doom," occurs when multiple nested callbacks make code deeply indented, hard to read, and difficult to maintain. It is typically resolved by using promises, async/await, or modularizing logic into named functions.

17. What are Promises in Node.js?

A Promise is an object representing the eventual completion or failure of an asynchronous operation, holding one of three states: pending, fulfilled, or rejected. Promises allow you to chain operations with .then() and handle errors with .catch(), producing flatter and more readable code than nested callbacks. They form the foundation for the async/await syntax and are widely used across modern Node.js libraries.

18. What is async/await and how does it work?

async/await is syntactic sugar built on top of promises that lets you write asynchronous code in a style that looks synchronous. Marking a function with async makes it return a promise, and the await keyword pauses execution within that function until the awaited promise settles. This improves readability and lets you use familiar try/catch blocks for error handling instead of .catch() chains.

19. What is the difference between setTimeout, setImmediate, and process.nextTick?

setTimeout schedules a callback to run after a minimum delay during the timers phase, while setImmediate schedules a callback to run in the check phase, immediately after the current poll phase completes. process.nextTick is not part of the event loop phases; its callbacks run after the current operation completes but before the event loop continues, giving it the highest priority. Overusing process.nextTick can starve the event loop, so it should be used carefully.

20. What is the microtask queue versus the macrotask queue?

The microtask queue holds callbacks from promises and process.nextTick, and it is fully drained after each operation and between event loop phases, giving microtasks higher priority. The macrotask queue (or task queue) holds callbacks from timers, I/O, and setImmediate, which are processed during their respective event loop phases. Because microtasks run before the next macrotask, promise callbacks generally execute before setTimeout callbacks scheduled at the same time.

21. How does Node.js handle concurrency with a single thread?

Node.js achieves concurrency through its event loop and asynchronous, non-blocking I/O rather than through multiple threads handling each request. When an I/O operation is requested, it is offloaded to libuv or the operating system, and the main thread is free to handle other work until the operation completes and its callback is queued. For CPU-bound tasks that would block the loop, Node.js offers the cluster module and worker_threads to use multiple cores.

22. What is the cluster module used for?

The cluster module allows you to spawn multiple worker processes that share the same server port, enabling a Node.js application to take advantage of multi-core systems. The master process manages and distributes incoming connections among the workers, improving throughput and providing some resilience if a worker crashes. Each worker is a separate process with its own memory and event loop, so state is not automatically shared between them.

Modules & npm

23. What is a module in Node.js?

A module is a reusable, self-contained block of code that encapsulates related functionality and exposes selected parts through exports. Node.js treats each file as a separate module with its own scope, preventing variables from leaking into the global namespace. Modules fall into three categories: core modules built into Node.js, local modules you create, and third-party modules installed via npm.

24. What is the difference between CommonJS and ES Modules?

CommonJS is the original module system in Node.js, using require() to import and module.exports to export, and it loads modules synchronously. ES Modules (ESM) is the standardized JavaScript module system using import and export statements, supporting asynchronous loading and static analysis. You can enable ESM by using the .mjs extension or setting "type": "module" in package.json, while CommonJS remains the default for .js files unless configured otherwise.

25. How does require() work in Node.js?

When you call require(), Node.js resolves the module path, loads and wraps the file in a function to give it its own scope, executes it, and caches the resulting module.exports. Subsequent require() calls for the same module return the cached export rather than re-executing the file, which makes modules effectively singletons. The resolution algorithm checks core modules first, then local files, and finally the node_modules directories up the folder hierarchy.

26. What is module.exports versus exports?

module.exports is the actual object returned when a module is required, while exports is simply a reference variable pointing to the same object initially. You can attach properties to exports, but if you reassign exports to a new value, it breaks the reference and has no effect on what is exported. For clarity and to avoid bugs, it is common to assign directly to module.exports when exporting a single value such as a class or function.

27. What is npm?

npm (Node Package Manager) is the default package manager for Node.js, providing both a command-line tool and the world's largest registry of open-source JavaScript packages. It lets you install dependencies with npm install, manage project metadata in package.json, and run scripts defined under the scripts field. Alternatives like Yarn and pnpm offer similar functionality with different performance and disk-usage characteristics.

28. What is the purpose of package.json?

The package.json file is the manifest for a Node.js project, describing metadata such as the name, version, description, entry point, and author. It lists dependencies and devDependencies along with their version ranges, and defines runnable scripts like start, test, and build. This file makes a project reproducible, so anyone can run npm install to recreate the exact dependency tree.

29. What is the difference between dependencies and devDependencies?

Dependencies are packages required for the application to run in production, such as express or a database driver, and they are installed by default with npm install. devDependencies are packages needed only during development or build time, such as testing frameworks, linters, and bundlers, installed with npm install --save-dev. When deploying to production with npm install --production, devDependencies are omitted to keep the footprint small.

30. What is the package-lock.json file?

package-lock.json records the exact version of every installed package and its entire dependency tree, ensuring deterministic and reproducible installs across different machines and environments. While package.json may specify version ranges, the lock file pins precise versions so that npm install produces identical results every time. It should be committed to version control so teammates and CI systems install the same dependency versions.

31. What is the difference between local and global package installation?

A local installation (npm install <package>) places the package in the project's node_modules directory and is intended for use within that specific project. A global installation (npm install -g <package>) installs the package system-wide, typically for command-line tools you want to run from any directory, such as nodemon or typescript. As a best practice, application dependencies should be local while only CLI utilities are installed globally.

32. What are semantic versioning symbols like ^ and ~?

Semantic versioning (semver) uses the MAJOR.MINOR.PATCH format, and the caret (^) allows updates that do not change the leftmost non-zero digit, typically permitting minor and patch updates. The tilde (~) is more restrictive, generally allowing only patch-level updates within the specified minor version. These symbols in package.json control how much a dependency can be upgraded automatically while maintaining backward compatibility.

33. What are some commonly used core modules in Node.js?

Node.js ships with several built-in core modules that require no installation, including fs for file system operations, http and https for creating servers and clients, and path for handling file paths. Other widely used modules include os for operating-system information, events for the EventEmitter class, crypto for cryptographic functions, and stream for working with streaming data. These modules form the foundation upon which most Node.js applications and frameworks are built.

Streams & Buffers

34. What are streams in Node.js?

Streams are objects that let you read or write data piece by piece (in chunks) rather than loading it all into memory at once, which is essential for handling large files or continuous data flows. They are instances of the EventEmitter and emit events such as data, end, and error as data flows through them. Using streams improves memory efficiency and reduces latency because processing can begin before the entire dataset is available.

35. What are the different types of streams?

Node.js provides four fundamental stream types: Readable streams for reading data (such as fs.createReadStream), Writable streams for writing data (such as fs.createWriteStream), Duplex streams that are both readable and writable (such as a TCP socket), and Transform streams that modify data as it passes through (such as zlib compression). Each type follows a common interface but serves a different role in moving and processing data. Choosing the right type depends on whether you are consuming, producing, or transforming data.

36. What is piping in streams?

Piping connects the output of a readable stream directly to the input of a writable stream using the .pipe() method, automatically managing the flow of data and backpressure. For example, readStream.pipe(writeStream) efficiently copies data without buffering the entire source in memory. The modern pipeline() function from the stream module is preferred over .pipe() because it handles errors and cleanup more robustly.

37. What is backpressure in streams?

Backpressure occurs when a writable stream cannot consume data as fast as a readable stream produces it, risking memory buildup if unmanaged. Node.js streams handle this automatically when using .pipe() or pipeline() by pausing the readable stream until the writable side signals it can accept more data. Properly managing backpressure prevents excessive memory usage and keeps applications stable under heavy load.

38. What is a Buffer in Node.js?

A Buffer is a fixed-size chunk of memory allocated outside the V8 heap, designed to handle raw binary data such as bytes from files, network packets, or streams. Because JavaScript strings are not ideal for binary data, Buffers provide methods to read, write, and manipulate bytes directly. They are commonly encountered when working with streams, the fs module, TCP sockets, and cryptographic operations.

39. What is the difference between a Buffer and a Stream?

A Buffer is a temporary storage area holding a chunk of binary data in memory, whereas a stream is an abstraction for moving data sequentially over time. Streams often work with Buffers, emitting Buffer chunks as data flows through them. The key distinction is that a Buffer represents data at rest in memory, while a stream represents the continuous flow and processing of that data.

40. How do you read a file in Node.js?

You can read a file using the fs module, either asynchronously with fs.readFile() and a callback, with promises via fs.promises.readFile(), or synchronously with fs.readFileSync(). For large files, it is better to use fs.createReadStream() so the file is read in chunks rather than loaded entirely into memory. Choosing the asynchronous, non-blocking approach is generally recommended to avoid stalling the event loop.

Express & Building APIs

41. What is Express.js?

Express.js is a minimal, flexible, and widely used web application framework for Node.js that simplifies building servers and APIs. It provides a thin layer of features over Node's built-in http module, including routing, middleware support, and convenient request and response handling. Its unopinionated design and large ecosystem of middleware make it a popular choice for REST APIs and web applications.

42. What is middleware in Express?

Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the request-response cycle. They can execute code, modify the request and response, end the cycle, or call next() to pass control to the following middleware. Middleware is used for tasks like logging, authentication, body parsing, and error handling, and it executes in the order it is registered.

43. How do you define routes in Express?

Routes are defined by associating an HTTP method and a URL path with a handler function, for example app.get('/users', handler) or app.post('/users', handler). Express also provides app.route() for chaining handlers for the same path and the express.Router() class for grouping related routes into modular, mountable route handlers. This routing system maps incoming requests to the appropriate logic in your application.

44. What is the difference between app.use() and app.get()?

app.use() registers middleware that runs for all HTTP methods at a given path (or all paths if none is specified), making it ideal for cross-cutting concerns. app.get() registers a handler that runs only for HTTP GET requests at a specific route, defining a precise endpoint. In short, app.use() is for broad middleware and mounting, while app.get() (and its siblings like app.post()) is for method-specific route handling.

45. How do you handle query parameters and route parameters in Express?

Route parameters are named segments defined in the path with a colon, such as /users/:id, and accessed through req.params, for example req.params.id. Query parameters appear after the question mark in a URL, such as /users?role=admin, and are accessed through req.query, for example req.query.role. Distinguishing between the two is important: route parameters identify a specific resource, while query parameters typically filter, sort, or paginate results.

46. How do you parse request bodies in Express?

In modern Express, you parse incoming request bodies using built-in middleware such as express.json() for JSON payloads and express.urlencoded() for form-encoded data. Once registered with app.use(express.json()), the parsed body becomes available on req.body. For older versions or specialized needs, the standalone body-parser package provides equivalent functionality, and additional middleware like multer handles multipart form data and file uploads.

Error Handling & Performance

47. How do you handle errors in asynchronous Node.js code?

In callback-based code, Node.js follows the error-first convention where the first argument of a callback is an error object that you must check before using the result. With promises, errors are handled using .catch(), and with async/await, you wrap the awaited calls in try/catch blocks. It is important to handle errors at the appropriate level and avoid swallowing them silently, as unhandled rejections can crash the process in newer Node.js versions.

48. What is the error-first callback pattern?

The error-first callback pattern is a Node.js convention where asynchronous functions invoke their callback with an error as the first argument and the result as subsequent arguments. If the operation succeeds, the error argument is null or undefined; if it fails, it contains an Error object describing what went wrong. This consistent pattern allows callers to check if (err) first and handle failures uniformly across the Node.js ecosystem.

49. How do you handle uncaught exceptions and unhandled promise rejections?

Node.js emits an uncaughtException event on the process object when an error is thrown but not caught, and an unhandledRejection event when a promise rejects without a handler. While you can listen to these events to log diagnostics, the recommended practice is to let the process exit and restart rather than continuing in an unknown state. Tools like process managers (for example pm2) or container orchestrators can automatically restart the application to maintain availability.

50. What are some common techniques to improve Node.js performance?

Performance can be improved by writing non-blocking asynchronous code, avoiding synchronous operations on the event loop, and using streams for large data instead of loading everything into memory. Scaling across CPU cores with the cluster module or worker_threads, adding caching layers like Redis, and using a reverse proxy or load balancer also help under heavy load. Additionally, profiling with built-in tools, optimizing database queries, and enabling gzip compression are effective ways to reduce latency and increase throughput.