Skip to main content

Search

Search problems, hooks, and pages

Node.js Notes

30 of 30

Key Points

  • Non-blocking I/O model , handles high concurrency without spawning a thread per request.
  • Event-driven architecture , I/O completions are delivered as callbacks/events rather than blocking the caller.
  • Lightweight and fast to start, which suits containerized, horizontally-scaled deployments.
  • Same language (JS/TS) across frontend and backend , shared types, shared tooling.
// Netflix-style example: Node.js as an edge/BFF service handling
// millions of lightweight I/O-bound requests with minimal latency —
// not a place for heavy synchronous computation.

Warning

Gotcha

Non-blocking I/O doesn't mean multi-threaded computation , CPU-bound work still runs on the single main thread and blocks everything else. Offload it (worker threads, a queue) instead of doing it inline.

Key Points

  • Phases, in order: Timers (setTimeout/setInterval) → Pending/I-O callbacks → Idle/prepare → Poll (I/O execution) → Check (setImmediate) → Close callbacks.
  • Each phase has its own FIFO queue; the loop only moves to the next phase once the current one drains (or hits a limit).
  • Heavy synchronous computation inside any callback blocks the entire loop , no other request, timer, or I/O event is serviced until it returns.
  • Production fix for CPU-heavy work (e.g. synchronous image compression) blocking every request: move it to worker threads or an external queue (BullMQ).
// Blocks the loop , every other request waits
app.post('/compress', (req, res) => {
  const output = compressImageSync(req.body); // CPU-bound, synchronous
  res.send(output);
});

// Fix: offload to a worker thread or a job queue (BullMQ + Redis)

Warning

Gotcha

A single slow, synchronous handler degrades every concurrent request, not just its own , this is the most common Node.js scalability bug in interviews and in production.

Key Points

  • process.nextTick(fn) , runs immediately after the current operation, before the event loop continues (a microtask, not a loop phase).
  • setImmediate(fn) , runs in the Check phase, after I/O events in the current loop iteration.
  • setTimeout(fn, ms) , runs after (at least) the specified delay, in the Timers phase.
  • Real-world use: process.nextTick for critical microtasks like propagating an error before anything else runs.
console.log('start');
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
process.nextTick(() => console.log('nextTick'));
console.log('end');
// start, end, nextTick, timeout/immediate (order of the last two varies by context)

Key Points

  • Stream types: Readable, Writable, Duplex (both), Transform (Duplex that modifies data as it passes through).
  • Production case: uploading/processing a 1GB file via a stream avoids loading the whole file into memory and crashing the process.
  • Buffer: fixed-size chunk of raw memory used for binary data , file handling, TCP/socket data, image processing.
  • Streams and Buffers compose: a Readable stream emits Buffer chunks that a Transform/Writable stream consumes.
const fs = require('fs');
// Streamed — constant memory regardless of file size
fs.createReadStream('large-file.zip')
  .pipe(fs.createWriteStream('copy.zip'));

Key Points

  • cluster module , forks multiple Node.js instances (one per CPU core) that share the same server port, improving horizontal scaling on a single machine.
  • Worker Threads , run CPU-heavy JS (image resizing, PDF generation, ML inference) on separate threads without blocking the main event loop.
  • Child processes (spawn/exec/fork): spawn streams large data through a child command; exec buffers a shell command's output; fork creates another Node.js process (e.g. for parallel background jobs) with a built-in IPC channel.
  • Rule of thumb: cluster for "more request-handling capacity"; worker threads for "CPU-bound JS work"; child_process for "run another program or Node script".
const cluster = require('cluster');
const os = require('os');

if (cluster.isPrimary) {
  os.cpus().forEach(() => cluster.fork()); // one worker per CPU core
} else {
  require('./server'); // each worker runs the actual app
}

Key Points

  • Synchronous errors: try/catch around the call site.
  • Asynchronous errors: callback error-first convention, or .catch()/try-catch around await.
  • Global/uncaught errors: process.on("uncaughtException", ...) , a last-resort safety net, not a substitute for handling errors properly.
  • Best practice in an API: a single centralized error-handling middleware at the end of the middleware chain, so every route reports errors consistently.
// Centralized Express error handler , must be registered LAST
app.use((err, req, res, next) => {
  res.status(err.status || 500).json({ message: err.message });
});

Warning

Gotcha

Relying on uncaughtException to keep the process alive after a real error is dangerous , the process is in an unknown state; the standard advice is to log and exit, then let your process manager (PM2, Kubernetes) restart it.
Node.js Core Modules Overviewbasicruntime
Built-in modules that ship with Node itself , no install needed , covering system info, files, networking, and binary/text utilities.

Key Points

  • System/diagnostics: os (os.freemem(), os.cpus()), util (util.inspect() for debugging).
  • File system: fs , fs.readFile()/fs.writeFile() (async) vs fs.readFileSync() (sync, blocks the event loop).
  • Networking: http/https (createServer()), net (raw TCP sockets), dgram (UDP datagrams).
  • Binary/data utilities: crypto (hashing, HMAC), zlib (gzip/deflate compression), path (cross-platform path joins), url (parse/format URLs).
  • Interview framing: knowing these exist , and that they need zero npm installs , signals you understand what Node.js gives you 'for free' before reaching for a library.
const os = require('os');
const path = require('path');
const crypto = require('crypto');

console.log(os.freemem(), os.cpus().length);
console.log(path.join(__dirname, 'data', 'file.txt'));
console.log(crypto.createHash('sha256').update('hi').digest('hex'));

Key Points

  • http.createServer(listener) wires a callback that runs for every incoming request; .listen(port) starts accepting connections.
  • req (IncomingMessage) exposes req.url, req.method, req.headers for reading the request.
  • res (ServerResponse) is written to with res.writeHead(status) and res.end(body) to send the response.
  • Without a framework, you manually branch on req.url/req.method for routing , this manual boilerplate is exactly what Express's app.get()/app.post() replace.
const http = require('http');

const server = http.createServer((req, res) => {
  if (req.url === '/profile') {
    res.writeHead(200);
    res.end('Welcome to your profile!');
  } else {
    res.writeHead(200);
    res.end('Hello, World!');
  }
});

server.listen(8080);

Key Points

  • Middleware: a function (req, res, next) that runs during the request lifecycle , logging, auth, parsing , and calls next() to pass control along.
  • Request lifecycle: Incoming Request → Middleware chain → Route Handler → Response, with Error Middleware able to intercept at any point.
  • Routing maps an HTTP method + path to a handler: app.get("/users/:id", getUser); RESTful convention , GET read, POST create, PUT/PATCH update, DELETE remove.
  • Middleware chaining: multiple middleware run in sequence for one route (e.g. auth → role check → controller) , each must call next() or the chain stalls.
  • Route params (/users/10 → req.params.id) identify a specific resource; query params (/users?page=2 → req.query.page) modify how a collection is fetched (filtering, paging).
app.use(authMiddleware);
app.get('/orders', orderController);
app.use(errorHandler); // must be registered after all routes

// Middleware chaining for one route
app.get('/secure', authMiddleware, roleMiddleware, controller);

Warning

Gotcha

A middleware that forgets to call next() (and doesn't send a response) hangs the request forever , the client just times out with no error logged.

Key Points

  • controllers/ , parse the request, call a service, shape the response (no business logic).
  • services/ , business logic, orchestrates repositories.
  • repositories/ , the only layer that talks to the database/ORM.
  • middlewares/, routes/, utils/, config/ , cross-cutting and wiring concerns kept separate from the above.
  • Why: separation of concerns, a clean/testable architecture, and the ability to swap a layer (e.g. the DB) without touching controllers.
src/
├── controllers/
├── services/
├── repositories/
├── middlewares/
├── routes/
├── utils/
└── config/

Key Points

  • Validation (is the shape right?): Joi, Yup, Zod, or express-validator , reject malformed payloads before they reach business logic.
  • Sanitization (is the content safe?): strip/escape malicious input to prevent XSS and SQL injection , validator.js, DOMPurify.
  • CORS: app.use(cors({ origin: "https://yourdomain.com" })) , restricts which origins may call the API from a browser.
  • Helmet.js: app.use(helmet()) sets protective HTTP headers in one call, preventing XSS, clickjacking, and MIME-sniffing.
  • REST API security checklist: HTTPS, JWT auth, input validation, rate limiting, Helmet, logging & monitoring.
const schema = Joi.object({ email: Joi.string().email().required() });

app.use(helmet());
app.use(cors({ origin: 'https://yourdomain.com' }));
const rateLimit = require('express-rate-limit');
app.use(rateLimit({ windowMs: 60_000, max: 5 })); // e.g. 5 login attempts/min

Key Points

  • JWT flow: user logs in → server generates a token → client sends it in request headers on subsequent calls (no server-side session needed).
  • Refresh tokens: the (short-lived) access token expires quickly for security; a longer-lived refresh token is used to silently obtain a new access token , improves both security and UX.
  • OAuth: delegate authentication to Google/Facebook/GitHub , the backbone of SSO (single sign-on) systems.
  • Security best practice for REST APIs generally: HTTPS everywhere, JWT for stateless auth, plus rate limiting on auth endpoints specifically (e.g. 5 login attempts/minute).

Key Points

  • Versioning prevents breaking existing clients: URL versioning (/v1/users), header versioning, or query versioning.
  • Pagination fetches large datasets in chunks: GET /products?page=1&limit=20.
  • API-level caching: in-memory (node-cache), Redis, or CDN caching , e.g. cache a product list for 60 seconds.
  • Rate limiting prevents abuse by capping requests per client (express-rate-limit) , e.g. limit a login endpoint to 5 attempts/minute.
  • HTTP status codes: 200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 500 Server Error.

Key Points

  • SQL stacks: PostgreSQL/MySQL via pg, mysql2, Sequelize, or TypeORM.
  • NoSQL stacks: MongoDB via Mongoose.
  • ORM (TypeORM, Sequelize): abstracts the database into objects , higher productivity, less control over the exact query.
  • Query Builder (Knex.js): write SQL-like queries programmatically , more control, less abstraction.
  • Production pattern: an order service on PostgreSQL with TypeORM and connection pooling.

Key Points

  • Connection pooling maintains a set of reusable DB connections , reduces connection overhead, improves performance, and prevents overloading the database.
  • Critical at scale: without pooling, a high-traffic API (1000+ concurrent users) can exhaust the database's max connections.
  • Indexing improves query performance by avoiding a full table scan for lookups on the indexed column.
  • Trade-off: every index speeds up reads on that column but adds overhead to writes (the index must be updated too).
CREATE INDEX idx_user_email ON users(email);

Key Points

  • ACID: Atomicity (all-or-nothing), Consistency (valid state to valid state), Isolation (concurrent transactions don't interfere), Durability (committed data survives a crash) , expected in transactional systems like payments/banking.
  • Eventual consistency: used in distributed systems where data is not immediately consistent everywhere but converges over time.
  • Example: an order is created, and inventory is updated asynchronously afterward rather than in the same transaction.
  • Trade-off framing: ACID for correctness-critical single-system transactions; eventual consistency for scale across distributed services (social feeds, high-throughput systems).

Key Points

  • Use SQL for: transactions, structured/relational data (e.g. banking systems).
  • Use NoSQL for: high scalability, flexible schema (e.g. social media feeds).
  • Sharding: split a database into smaller pieces (shards) , e.g. users split by region (India shard, US shard) , scales writes/storage horizontally.
  • Replication: copy data across multiple servers (master-slave or multi-master) for high availability and fault tolerance.

Key Points

  • Redis use cases: caching, session store, pub/sub messaging, rate limiting.
  • Caching strategies: cache-aside (app checks cache, falls back to DB and populates cache on a miss), write-through (write to cache and DB together), write-back (write to cache first, DB later).
  • Cache-aside pattern in practice: check cache → on miss, fetch from DB → store in cache.
  • Cache invalidation strategies: TTL expiration (e.g. cache a product catalog for 30 seconds), manual invalidation, or event-driven invalidation when the underlying data changes.
const redis = require('redis');
const client = redis.createClient();

// Cache-aside pattern
let data = await client.get(key);
if (!data) {
  data = await fetchFromDb();
  await client.set(key, data, { EX: 60 }); // TTL: 60s
}

Warning

Gotcha

"Cache invalidation" is famously one of the two hard problems in computer science , pick a strategy deliberately (TTL is simplest, event-driven is freshest but more code) rather than caching everything indefinitely.

Key Points

  • Message queue tools: RabbitMQ, Kafka, BullMQ (Redis-based job queue for Node.js).
  • Use message queues for: background jobs, email sending, payment processing, order fulfillment , anything that shouldn't block the request/response cycle.
  • Pub/Sub pattern: a publisher sends a message and multiple subscribers independently receive it , e.g. "order placed" notifies both the inventory service and the notification service.
  • Kafka specifically: event streaming, log aggregation, real-time analytics (e.g. tracking user activity events) , built for high-throughput, ordered event logs, not just simple job queues.
  • BullMQ specifically: a Redis-based job queue used for email jobs, image processing, and report generation in Node.js apps.

Key Points

  • Idempotency: the same request sent multiple times produces the same result , critical for payment APIs, which must never charge a customer twice on a retried request.
  • Distributed locking prevents multiple processes/instances from modifying the same data concurrently.
  • Common tool: Redis-based locks (and coordination services like Zookeeper) to implement a distributed lock across multiple Node.js instances.

Key Points

  • Vertical scaling: increase CPU/RAM on one machine. Horizontal scaling: add more servers , Node.js apps typically scale horizontally.
  • Load balancing distributes traffic across multiple servers (Nginx, AWS ELB).
  • Circuit breaker: stop retrying and return a fallback response when a downstream service is failing, instead of cascading the failure (e.g. payment service down → stop retrying → fallback response).
  • API Gateway: single entry point for microservices, responsible for auth verification, rate limiting, routing, and response aggregation (Kong, NGINX, AWS API Gateway).
  • CQRS (Command Query Responsibility Segregation): separate read and write operations for better scalability in large systems.

Key Points

  • Production request path: Client → CDN → Load Balancer → API Gateway → Node Services → DB/Cache/Queue.
  • Key design principles: stateless services (so any instance can serve any request), horizontal scaling, externalized state (Redis/DB , not in-process memory), an API gateway for routing.
  • Microservices architecture: split the app into independent, deployable services (e.g. Auth, Order, Payment, Notification services), each independently scalable and team-owned.
  • Monolith vs Microservices trade-off: monolith = single codebase, simple to start, tightly coupled; microservices = multiple services, more complex but scalable, loosely coupled.
  • Service discovery (Consul, Eureka, Kubernetes DNS) lets services dynamically find each other as instances scale up/down.

Key Points

  • Benefits: identical environment everywhere, easy/repeatable deployment, process isolation.
  • A minimal Dockerfile copies package files first (for layer caching), installs deps, then copies the rest of the source and sets the start command.
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]

Key Points

  • Auto scaling: adds/removes container instances based on load.
  • Self healing: restarts or replaces failed containers automatically.
  • Rolling deployment: updates instances gradually with no downtime window.
  • Kubernetes DNS also doubles as a service-discovery mechanism, alongside dedicated tools like Consul or Eureka.

Key Points

  • CI/CD pipeline automates: build → test → deploy. Common tools: GitHub Actions, Jenkins, Azure DevOps.
  • Blue-green deployment: run two environments (Blue = current, Green = new); switch traffic to Green after testing it.
  • Canary deployment: release to a small percentage of users first (e.g. 5%), monitor, then roll out fully.
  • Rolling update: gradually replace instances with the new version (a Kubernetes-native rollout technique).
  • Rollback strategy: revert to the previous stable version immediately if a failure is detected post-deploy.
  • Zero-downtime deployment is the umbrella goal achieved by any of: rolling updates, blue-green, or canary.

Key Points

  • Logging tools: Winston, Pino. Best practice: log request id, user id, and the error stack , not just a message string , so logs are traceable per-request.
  • Monitoring tools (system health): Prometheus, Grafana, Datadog.
  • Distributed tracing (tracks one request across many microservices): Jaeger, Zipkin.
  • Health check endpoint: a simple route (GET /health → "OK") that load balancers poll to verify an instance is alive before routing traffic to it.
app.get('/health', (req, res) => res.send('OK'));

Key Points

  • Secrets: use environment variables or a secret manager (Azure Key Vault, AWS Secrets Manager) , never commit credentials to source.
  • Infrastructure-level rate limiting is often implemented outside the app itself: API gateway, NGINX, or Cloudflare.
  • Graceful shutdown ensures: no data loss, DB connections closed cleanly, and in-flight requests finish before the process exits.
  • Typical trigger: listen for SIGTERM (sent by orchestrators like Kubernetes before killing a pod) and close the server before exiting.
process.on('SIGTERM', async () => {
  await server.close();      // stop accepting new connections, finish in-flight ones
  await db.disconnect();     // close DB connections cleanly
  process.exit(0);
});

Key Points

  • SSR (server renders HTML, SEO-friendly) vs CSR (browser renders, faster client interactions after initial load) , a rendering-strategy trade-off, not exclusive to Node but commonly implemented in it.
  • BFF (Backend for Frontend): a separate backend tailored to each frontend's needs , e.g. a Mobile BFF and a Web BFF in front of the same core services.
  • Feature flagging: enable/disable features without a redeployment (LaunchDarkly, Firebase Remote Config) , decouples deploy from release.

Key Points

  • The accurate interview answer to "is Node.js single-threaded?" is: your JavaScript runs on a single thread (the event loop), but Node.js as a whole is not , libuv, the C library underneath Node, maintains a separate worker pool for expensive operations.
  • What actually runs on the thread pool: most fs operations (except the sync versions and FSWatcher), dns.lookup(), and specific CPU-heavy crypto functions (pbkdf2, scrypt, randomBytes, generateKeyPair) and zlib compression.
  • Default pool size is 4 threads, controlled by the UV_THREADPOOL_SIZE environment variable , a common production tuning knob when an app does heavy concurrent file I/O or password hashing and threads become the bottleneck.
  • Network I/O (http, most of net) does NOT use the thread pool at all , the OS kernel itself handles socket readiness asynchronously, which is what makes Node so efficient at high-concurrency networking specifically.
  • This is exactly why CPU-bound JavaScript (a synchronous loop, JSON.parse on a huge payload) still blocks everything , that work runs on the main JS thread, not the libuv pool, and has no way to be silently offloaded like fs/crypto calls are.
const fs = require('fs');
const crypto = require('crypto');

console.log('start');
fs.readFile('big-file.txt', () => console.log('file read done'));      // thread pool
crypto.pbkdf2('pw', 'salt', 100000, 32, 'sha256', () => console.log('hash done')); // thread pool
console.log('end');
// start, end, then file/hash callbacks , main thread never blocked

// Tune pool size for I/O or crypto-heavy workloads:
// UV_THREADPOOL_SIZE=8 node app.js

Warning

Gotcha

Saying "Node.js is single-threaded" without qualification is the classic incomplete interview answer , it's true for JS execution, but several built-in async APIs are backed by a real, tunable thread pool underneath.

Key Points

  • writable.write(chunk) returns false once the stream’s internal buffer has grown past its highWaterMark , that return value is the signal to stop writing more.
  • Ignoring a false return and continuing to call write() in a tight loop doesn’t error immediately , Node just keeps buffering chunks in memory until it hits a limit, then the process aborts.
  • When write() returns false, listen for the "drain" event , it fires once the buffer has emptied enough that writing can safely resume.
  • .pipe(destination) implements exactly this write/drain dance internally , that’s why piping a Readable into a Writable is memory-safe by default, and a naive manual copy loop usually isn’t.
  • This is the mechanism that lets Node.js stream a multi-gigabyte file without the process’s memory usage growing unbounded , the source is throttled to match how fast the destination can actually consume data.
// ❌ ignores backpressure — can exhaust memory on large input
for (let i = 0; i < 1_000_000; i++) writable.write(chunk);

// ✅ respects backpressure
function writeAll(writable, chunks, i = 0) {
  while (i < chunks.length) {
    const ok = writable.write(chunks[i++]);
    if (!ok) {
      writable.once('drain', () => writeAll(writable, chunks, i));
      return; // pause until drained
    }
  }
}

// ✅ simplest option — pipe() handles backpressure automatically
readable.pipe(writable);

Warning

Gotcha

A manual write() loop that never checks the return value is a classic memory-leak-under-load bug , it works fine in local testing with small files, then OOMs in production with large ones because nothing was throttling the writes.