A focused Node.js interview set for backend developers. The answers are structured for spoken practice: explain the architecture, give an example and mention production trade-offs.
Node.js is a JavaScript runtime outside the browser, built on V8 and extended with APIs for networking, files, processes, streams and operating-system access. It is commonly used for REST APIs, GraphQL services, realtime WebSocket servers, microservices, CLI tools, proxies and API gateways. Its main strength is non-blocking I/O: one process can handle many network connections efficiently. The trade-off is that long CPU-heavy calculations can block the event loop unless moved to workers, processes or another service.
2How does Node.js work internally?
JavaScript runs in V8, while Node.js APIs delegate asynchronous work to the operating system or to libuv's thread pool. The main JavaScript thread does not wait for an I/O operation to finish; it continues executing other code. When the operation completes, its callback or Promise continuation becomes ready for the event loop. This is how Node.js achieves high concurrency even though user JavaScript usually runs on one main thread.
3What are V8, libuv and the event loop?
V8 compiles and executes JavaScript and manages JavaScript objects and memory. libuv provides the cross-platform event loop, async I/O integration and a worker thread pool for operations such as some filesystem work, DNS and crypto. The event loop decides when completed operation callbacks should run. Node.js itself connects JavaScript APIs to native capabilities; V8 alone does not provide an HTTP server, filesystem access or process APIs.
4Why is Node.js often called single-threaded?
A single Node.js process usually executes user JavaScript on one main thread, so two normal request handlers do not run JavaScript at the exact same time in that thread. However, the platform is not literally one thread: libuv has a thread pool, the operating system performs async I/O, V8 has garbage-collection work, and Node.js also supports worker_threads and child processes. The phrase mainly describes the default JavaScript execution model.
5How can Node.js handle many requests concurrently?
Node.js starts I/O operations and then returns to the event loop instead of blocking a thread per request. For example, 1,000 database calls can be in flight while the main thread continues accepting connections and processing ready callbacks. This works well for I/O-bound workloads such as HTTP calls, database queries, files and sockets. It works poorly when request handlers do heavy synchronous CPU work, because that blocks the event loop and delays every other request.
6What are the main phases of the Node.js event loop?
The libuv event loop includes phases such as timers, pending callbacks, idle/prepare, poll, check and close callbacks. Timers run setTimeout and setInterval callbacks; poll handles many I/O events; check runs setImmediate callbacks; close handles close events such as sockets closing. Node.js also has microtask queues, including process.nextTick and Promise microtasks, which run between callbacks. Too many microtasks can starve I/O and make the service appear stuck.
7How do synchronous, asynchronous and non-blocking operations differ?
A synchronous operation returns before the next line runs and blocks the main thread, such as fs.readFileSync in a request handler. An asynchronous operation returns its result later through a callback or Promise. Non-blocking means the thread can continue doing other work while the operation is pending. The terms are related but not identical: async describes how the result is delivered, while non-blocking describes whether the thread waits. Backend request handlers should generally prefer async non-blocking APIs.
Event loop starvation happens when JavaScript work or microtasks keep the event loop busy so it cannot process I/O, timers or new requests promptly. Common causes include heavy synchronous calculations, huge JSON processing, unbounded Promise chains and excessive process.nextTick usage. Fixes include breaking work into chunks, limiting input sizes, profiling CPU, moving CPU-bound work to worker_threads or another service, and avoiding synchronous APIs in hot request paths.
A callback is a function passed to be executed later, often after an asynchronous operation completes. Classic Node.js callbacks commonly follow the error-first style: callback(error, result). Callback hell happens when dependent operations are deeply nested, making error handling and control flow hard to read. The solution is to decompose into named functions, use Promises or async/await, and run independent operations with Promise.all instead of nesting them.
import fs from 'node:fs';
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
10What is a Promise and how does async/await work?
A Promise represents the future result of an asynchronous operation and can be pending, fulfilled or rejected. An async function always returns a Promise. await pauses only the current async function until the Promise settles; it does not block the whole Node.js process. A good answer should mention error handling with try/catch, avoiding floating Promises and using Promise.all for independent work so operations do not run unnecessarily sequentially.
11How do process.nextTick, queueMicrotask, setImmediate and setTimeout differ?
process.nextTick runs after the current operation and before the event loop moves on. queueMicrotask schedules a standard JavaScript microtask, similar to Promise continuations. setImmediate runs in the check phase. setTimeout(fn, 0) runs in the timers phase after a minimum delay. Usually nextTick runs before Promise microtasks, and the order of setTimeout(0) and setImmediate depends on context; after I/O callbacks, setImmediate often runs first.
Synchronous code uses try/catch. Promise-based code should use await inside try/catch or attach .catch. Error-first callbacks should check the error argument before using the result. In server applications, central error middleware, request-context logging, safe client messages and graceful shutdown are important. uncaughtException should not be used to keep a corrupted process running; it is usually a signal to log, clean up and restart.
A module is an isolated unit of code with an explicit exported interface. Node.js supports built-in modules such as node:fs and node:http, local project modules, npm packages, CommonJS modules and ECMAScript modules. Modules make responsibility boundaries clearer and allow reuse. The production trade-off is coupling: if modules import each other too deeply or hide side effects at import time, testing and startup behavior become harder to reason about.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
14How do CommonJS require and ES Modules import differ?
CommonJS uses require and module.exports, while ES Modules use import and export. ES Modules support static analysis and top-level await, and they are selected through package.json type: module or .mjs files. CommonJS is often loaded synchronously and can be marked with .cjs. In modern Node.js projects both systems exist, so a strong answer avoids oversimplifying and explains that file extension, package.json and project context determine the module mode.
15What are npm, package.json and package-lock.json?
npm is the package manager and CLI. package.json describes scripts, metadata, dependencies and devDependencies. package-lock.json records the exact dependency tree so installs are reproducible. In CI, npm ci is usually preferred because it installs strictly from the lock file and fails if package.json and the lock disagree. This protects builds from accidental dependency drift.
16How should configuration and environment variables be managed?
Node.js reads environment variables through process.env, but values are strings and should be validated and converted at startup. Secrets should not be stored in the repository, .env should be ignored, and production secrets should come from the platform or a secret manager. Good services fail fast when required configuration is missing. NODE_ENV is a convention, not a security boundary, so code should not rely on it as a protected mechanism.
const port = Number(process.env.PORT ?? 3000);
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error('DATABASE_URL is required');
}
Events, Buffers and Streams
17What is EventEmitter?
EventEmitter implements a publish-subscribe pattern inside a Node.js process. on registers a listener, once registers a listener for one event, off removes a listener and emit synchronously calls listeners in registration order. The synchronous nature is important: a slow listener blocks the emitter. The error event is special because an unhandled error event can crash the process. Long-lived systems should also remove unused listeners to avoid memory leaks.
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
emitter.on('orderCreated', order => {
console.log(`Order ${order.id} created`);
});
emitter.emit('orderCreated', { id: 42 });
18What is Buffer used for?
Buffer represents a fixed-size sequence of bytes. It is used for files, TCP sockets, HTTP bodies, images, cryptography and binary protocols. Buffer.from creates a buffer from existing data, Buffer.alloc creates zero-filled memory, and Buffer.allocUnsafe is faster but may contain old memory until overwritten. That makes allocUnsafe appropriate only when the code immediately fills the buffer and never exposes stale data.
Streams process data in chunks instead of loading everything into memory. The main types are Readable, Writable, Duplex and Transform. Files, HTTP request and response objects, TCP sockets and gzip streams are common examples. Streams are valuable for large files and network traffic because they reduce memory usage and latency. A production answer should mention that stream errors must be handled correctly across the whole chain.
20What is backpressure and why is pipeline useful?
Backpressure happens when a data source produces chunks faster than the destination can consume them. Without backpressure, memory can grow until the process slows down or crashes. Streams and pipe can coordinate flow, but pipeline from node:stream/promises is safer for multi-step chains because it propagates errors, closes related streams and returns a Promise. It is a good default for file compression, uploads, downloads and stream transformations.
import fs from 'node:fs';
import zlib from 'node:zlib';
import { pipeline } from 'node:stream/promises';
await pipeline(
fs.createReadStream('input.txt'),
zlib.createGzip(),
fs.createWriteStream('input.txt.gz')
);
More backend interview questions
Move between technologies or return to the full backend preparation hub.