interviewDeck

Your one-stop platform to prepare, practice and ace your interviews.

Loading your questions…

All Questions

Filters & tools

Node.js Interview Questions and Answers

35 hand-picked Node.js interview questions with detailed answers. Open the interactive version above to search, filter by difficulty, run code, bookmark questions and track your progress.

What is Node.js and how is it different from browser JavaScript?

Node.js is a JavaScript runtime built on Chrome's V8 engine. It runs JS outside the browser — on servers, CLIs, and tooling.

Key differences from browser JS:

  • No DOM — uses Node APIs (fs, http, path) instead of document/window.
  • Module system — CommonJS (require) and ES Modules (import).
  • Event-driven, non-blocking I/O — designed for concurrent connections on a single thread.
  • npm — largest package ecosystem for server-side libraries.
const fs = require('fs');
const http = require('http');

Explain the Node.js event loop.

Node runs JavaScript on a single main thread but handles I/O asynchronously via libuv. The event loop continuously checks phases and runs callbacks when work completes:

  1. TimerssetTimeout / setInterval
  2. Pending callbacks — deferred I/O callbacks
  3. Idle, prepare — internal
  4. Poll — retrieve new I/O events; may block here
  5. ChecksetImmediate callbacks
  6. Close callbacks — e.g. socket.on('close')

Between each phase, Node drains the microtask queue (Promises, process.nextTick).

Blocking vs non-blocking I/O in Node.js.

Blocking — the thread waits until the operation completes (e.g. fs.readFileSync). Nothing else runs.

Non-blocking — initiates the operation and continues; a callback or Promise resolves when data is ready (e.g. fs.readFile, fs.promises.readFile).

Node's strength is handling thousands of concurrent connections with non-blocking I/O on one thread.

// non-blocking (preferred)
fs.readFile('data.txt', (err, data) => { ... });

// blocking (blocks event loop)
const data = fs.readFileSync('data.txt');

Callbacks vs Promises vs async/await.

Callbacksfn(err, result) pattern; leads to callback hell when nested.

Promises — represent a future value; chain with .then() / .catch(); compose with Promise.all.

async/await — syntactic sugar over Promises; write async code that reads synchronously. Always wrap in try/catch for errors.

Modern Node code uses async/await; callbacks remain in older APIs and streams.

async function getUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    return await res.json();
  } catch (err) {
    logger.error(err);
    throw err;
  }
}

Promise.all vs Promise.allSettled vs Promise.race.

  • Promise.all — waits for all; fails fast on first rejection; returns array of results in order.
  • Promise.allSettled — waits for all regardless of success/failure; returns {status, value|reason} per promise.
  • Promise.race — settles when the first promise settles (win or lose).
  • Promise.any — first fulfilled promise; fails only if all reject.
const [user, orders] = await Promise.all([
  fetchUser(id),
  fetchOrders(id)
]);

process.nextTick vs setImmediate vs setTimeout(0).

process.nextTick — runs before the event loop continues; highest priority microtask. Can starve I/O if abused.

setImmediate — runs in the check phase, after I/O poll.

setTimeout(fn, 0) — runs in the timers phase; minimum delay is ~1ms in practice.

Order after sync code: nextTick → Promises → setImmediate → setTimeout.

process.nextTick(() => console.log('nextTick'));
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));

CommonJS vs ES Modules in Node.js.

CommonJS (CJS)require() / module.exports; synchronous load; default in .js files unless "type": "module" in package.json.

ES Modules (ESM)import / export; static analysis; async loading; use .mjs or "type": "module".

Interop: ESM can import CJS; CJS loads ESM via dynamic import().

// CommonJS
const express = require('express');
module.exports = router;

// ESM
import express from 'express';
export default router;

Key fields in package.json.

  • name, version — package identity (semver).
  • main / exports — entry point(s).
  • scripts — npm run commands (start, test, build).
  • dependencies — runtime deps; devDependencies — build/test only.
  • engines — required Node version.
  • type"module" for ESM.
{
  "name": "my-api",
  "version": "1.0.0",
  "type": "module",
  "scripts": { "start": "node src/index.js" },
  "engines": { "node": ">=20" }
}

npm vs yarn vs pnpm — brief comparison.

All manage dependencies from npm registry.

  • npm — default with Node; package-lock.json; improved significantly since v7.
  • yarn — parallel installs, yarn.lock; Yarn Berry (v2+) uses Plug'n'Play optionally.
  • pnpm — content-addressable store; hard links save disk; strict node_modules (no phantom deps).

Pick one per project and stick to it — don't mix lockfiles.

npm install express
pnpm add express
yarn add express

What is Express.js and how do you define a basic API?

Express is a minimal web framework for Node — routing, middleware, request/response helpers. De facto standard for REST APIs.

Define routes with HTTP verbs. Middleware runs in order for every matching request. Use express.json() to parse JSON bodies.

import express from 'express';
const app = express();
app.use(express.json());

app.get('/health', (req, res) => res.json({ ok: true }));
app.get('/users/:id', async (req, res) => {
  const user = await findUser(req.params.id);
  res.json(user);
});

app.listen(3000);

What is middleware in Express?

Middleware is a function (req, res, next) => {} that runs in the request pipeline. It can read/modify req/res, end the response, or call next() to pass control to the next middleware.

Types: application-level (app.use), router-level, error-handling (4 args: err, req, res, next), and built-in (json parser, static files).

function auth(req, res, next) {
  const token = req.headers.authorization;
  if (!token) return res.status(401).json({ error: 'unauthorized' });
  req.user = verify(token);
  next();
}

app.use('/api', auth);
app.get('/api/profile', (req, res) => res.json(req.user));

What is the EventEmitter pattern?

Node's core pattern for pub/sub. Objects inherit from EventEmitter and emit named events; listeners register with .on() / .once().

Built into streams, HTTP servers, and process events. Many Node APIs are event-driven rather than callback-only.

import { EventEmitter } from 'events';
const bus = new EventEmitter();

bus.on('order:created', (order) => notify(order));
bus.emit('order:created', { id: 1 });

What are streams and why use them?

Streams process data chunk by chunk instead of loading everything into memory. Four types: Readable, Writable, Duplex, Transform.

Use for: large file uploads/downloads, log processing, proxying HTTP responses, gzip compression.

Backpressure — when the consumer is slower than the producer, Writable buffers fill; .pipe() handles this automatically.

import { createReadStream } from 'fs';
import { createGzip } from 'zlib';

createReadStream('big.csv')
  .pipe(createGzip())
  .pipe(res);

What is a Buffer in Node.js?

A Buffer is a fixed-length raw binary data allocation — Node's way to handle bytes (files, network packets, crypto). Similar to Uint8Array but with Node-specific helpers.

Create with Buffer.from(string), Buffer.alloc(size). Encode/decode: buffer.toString('utf8'), Buffer.from(b64, 'base64').

const buf = Buffer.from('hello', 'utf8');
console.log(buf.toString('hex'));  // 68656c6c6f

How does the cluster module scale Node across CPU cores?

The master process forks worker processes (one per core typically). The OS load-balances incoming connections across workers. Each worker has its own V8 instance and event loop.

Workers don't share memory — use Redis/DB for shared state. For modern apps, container orchestration (Kubernetes) often replaces manual clustering.

import cluster from 'cluster';
import os from 'os';

if (cluster.isPrimary) {
  for (let i = 0; i < os.cpus().length; i++) cluster.fork();
} else {
  startServer();  // each worker runs the app
}

Worker threads vs cluster — when to use each?

Cluster — multiple Node processes; best for scaling HTTP servers across cores.

Worker threads — threads within one process; share memory via SharedArrayBuffer; best for CPU-intensive tasks (image processing, crypto, heavy computation) without blocking the main event loop.

import { Worker } from 'worker_threads';

const worker = new Worker('./hash-worker.js', { workerData: input });
worker.on('message', (result) => console.log(result));

spawn vs exec vs execFile vs fork.

  • spawn — streams stdout/stderr; best for long-running processes and large output.
  • exec — buffers full output; runs in a shell; maxBuffer limit; good for short shell commands.
  • execFile — like exec but no shell; safer for direct binary execution.
  • fork — special spawn for Node scripts; sets up IPC channel (send/message).
import { spawn } from 'child_process';
const ls = spawn('ls', ['-la']);
ls.stdout.on('data', (d) => console.log(d.toString()));

How do you manage environment variables in Node?

process.env holds environment variables. Use .env files locally with dotenv (loaded at startup). In production, inject vars via the platform (Docker, K8s, CI/CD secrets).

Never commit .env to git. Validate required vars at boot — fail fast if missing.

import 'dotenv/config';

const port = process.env.PORT || 3000;
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL required');

How does JWT authentication work in a Node API?

After login, the server issues a signed JWT (header.payload.signature) containing claims (user id, roles, expiry). Client sends it in the Authorization: Bearer <token> header.

Server verifies signature with a secret/public key on each request — stateless (no server-side session store). Refresh tokens handle expiry securely.

import jwt from 'jsonwebtoken';

const token = jwt.sign({ sub: user.id }, process.env.JWT_SECRET, { expiresIn: '15m' });
const payload = jwt.verify(token, process.env.JWT_SECRET);

What are CORS and Helmet in Express?

CORS (Cross-Origin Resource Sharing) — browser security that blocks requests from different origins. The cors middleware sets Access-Control-* headers so your API accepts requests from allowed frontends.

Helmet — sets security HTTP headers (XSS protection, content-type sniffing, HSTS, etc.) with sensible defaults.

import cors from 'cors';
import helmet from 'helmet';

app.use(helmet());
app.use(cors({ origin: 'https://myapp.com', credentials: true }));

How do you implement rate limiting?

Limit requests per IP/user within a time window to prevent abuse and DDoS. Implement with express-rate-limit (in-memory) or Redis-backed limiter for distributed deployments.

Return 429 Too Many Requests with Retry-After header when exceeded.

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 });
app.use('/api/', limiter);

REST API best practices in Node.

  • Use nouns for resources: GET /users, POST /users, GET /users/:id.
  • Correct HTTP status codes: 200, 201, 204, 400, 401, 403, 404, 409, 500.
  • Consistent error format: { error: { code, message } }.
  • Pagination: ?page=1&limit=20 or cursor-based.
  • Version APIs: /api/v1/users.
  • Validate input (Joi, Zod) before business logic.
app.post('/api/v1/users', async (req, res) => {
  const user = await createUser(req.body);
  res.status(201).location(`/api/v1/users/${user.id}`).json(user);
});

How does Mongoose work with MongoDB?

Mongoose is an ODM (Object Document Mapper) for MongoDB. Define schemas with types, validation, defaults, and middleware. Models map to collections.

MongoDB stores BSON documents (flexible schema); Mongoose adds structure on top. Use .find(), .findById(), .save(), aggregation pipelines for complex queries.

const userSchema = new Schema({
  email: { type: String, required: true, unique: true },
  name: String
});
const User = model('User', userSchema);

const users = await User.find({ active: true }).limit(10);

How do you use Redis for caching in Node?

Store frequently accessed data in Redis (in-memory) with a TTL to reduce DB load. Pattern: check cache → on miss, query DB → store in cache → return.

Also used for: session storage, rate limiting, pub/sub, job queues (Bull/BullMQ).

async function getUser(id) {
  const cached = await redis.get(`user:${id}`);
  if (cached) return JSON.parse(cached);
  const user = await db.findUser(id);
  await redis.setex(`user:${id}`, 300, JSON.stringify(user));
  return user;
}

What is database connection pooling?

Opening a DB connection is expensive. A pool maintains a set of reusable connections. Requests borrow a connection, run queries, and return it to the pool.

Libraries: pg Pool for PostgreSQL, Mongoose manages MongoDB connections. Configure max pool size based on DB limits and concurrency needs.

import pg from 'pg';
const pool = new pg.Pool({ max: 20, connectionString: process.env.DATABASE_URL });
const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [id]);

How do you test Node.js code with Jest?

Jest provides test runner, assertions, mocking, and coverage. Tests live in *.test.js or __tests__/.

Mock modules with jest.mock(); spy with jest.spyOn(). For HTTP APIs, use supertest to call Express without starting a real server.

import request from 'supertest';
import { app } from './app.js';

test('GET /health', async () => {
  const res = await request(app).get('/health');
  expect(res.status).toBe(200);
  expect(res.body.ok).toBe(true);
});

Best practices for error handling in Node APIs.

  • Centralised error middleware — one place formats all errors.
  • Distinguish operational errors (bad input, 404) from programmer errors (bugs).
  • Never expose stack traces in production responses.
  • Always handle unhandledRejection and uncaughtException — log and graceful shutdown.
  • Use custom error classes with statusCode property.
class AppError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.statusCode = statusCode;
  }
}

app.use((err, req, res, next) => {
  res.status(err.statusCode || 500).json({ error: err.message });
});

Common causes of memory leaks in Node.js.

  • Global variables accumulating data.
  • Closures holding references to large objects.
  • Event listeners not removed — especially on long-lived EventEmitters.
  • Timers (setInterval) never cleared.
  • Caches without eviction/TTL growing unbounded.
  • Detached DOM (less common in pure Node, but in Electron).

Debug with --inspect, Chrome DevTools heap snapshots, and process.memoryUsage().

setInterval(() => {
  console.log(process.memoryUsage().heapUsed / 1024 / 1024, 'MB');
}, 5000);

How should you handle logging in production Node apps?

Use a structured logger (Pino or Winston) — JSON logs with levels (error, warn, info, debug), timestamps, and request IDs.

Don't use console.log in production — no levels, no structure, hurts performance. Correlate logs with a per-request ID passed through middleware.

import pino from 'pino';
const logger = pino();

app.use((req, res, next) => {
  req.log = logger.child({ reqId: crypto.randomUUID() });
  next();
});

What is PM2 and why use it?

PM2 is a production process manager for Node. Features: cluster mode (multi-core), auto-restart on crash, log management, zero-downtime reload, startup scripts, monitoring.

Alternative in containerised deployments: let Kubernetes/Docker handle restarts and scaling; run a single Node process per container.

pm2 start src/index.js --name api -i max
pm2 reload api    # zero-downtime restart
pm2 logs api

What is NestJS and how does it differ from Express?

NestJS is an opinionated framework built on Express (or Fastify). Brings Angular-inspired patterns: modules, controllers, services, dependency injection, decorators, guards, interceptors, pipes.

Better for large teams and enterprise APIs where structure and testability matter. Express is lighter and more flexible for small services.

@Controller('users')
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(id);
  }
}

How do WebSockets work in Node?

WebSockets provide a persistent, bidirectional connection — unlike HTTP request/response. Use for real-time features: chat, live notifications, collaborative editing.

Libraries: native ws, Socket.IO (fallback transports, rooms, namespaces). Socket.IO adds reconnection and broadcasting abstractions.

import { Server } from 'socket.io';
const io = new Server(httpServer);

io.on('connection', (socket) => {
  socket.on('message', (data) => io.emit('message', data));
});

What roles do V8 and libuv play in Node.js?

V8 — Google's JavaScript engine; compiles JS to machine code (JIT), manages the JS heap and garbage collection.

libuv — C library providing the event loop, thread pool, and cross-platform async I/O (file system, network, DNS). Some operations (file I/O, crypto, DNS lookup) use libuv's thread pool because they're blocking at the OS level.

Working with the fs and path modules.

fs — file system operations. Prefer fs.promises (async) over callbacks. Key methods: readFile, writeFile, readdir, stat, mkdir.

path — cross-platform path manipulation: join, resolve, basename, extname. Always use path.join instead of string concatenation.

import { readFile } from 'fs/promises';
import { join, extname } from 'path';

const filePath = join(__dirname, 'data', 'config.json');
const data = await readFile(filePath, 'utf8');

How do you validate request input in Node APIs?

Never trust client input. Validate at the API boundary with a schema library:

  • Zod — TypeScript-first, infers types from schema.
  • Joi — mature, widely used with Express.
  • express-validator — chainable validators on req fields.

Return 400 with clear field-level errors on validation failure.

import { z } from 'zod';

const schema = z.object({
  email: z.string().email(),
  age: z.number().int().min(18)
});

const body = schema.parse(req.body);  // throws on invalid