This roadmap is about NodeJs Developer
NodeJs Developer roadmap starts from here
Advanced NodeJs Developer Roadmap Topics
By Oleksandr K.
1 year of experience
My name is Oleksandr K. and I have over 1 years of experience in the tech industry. I specialize in the following technologies: React, node.js, Next.js, Python, MongoDB, etc.. I hold a degree in Bachelor of Computer Applications, . Some of the notable projects I’ve worked on include: Crypto Water Web project, Crypto Water App, GIS data analysis, Angular Polygon drawing component using Konva.js, KML data generation and visualization. I am based in Salhany, Ukraine. I've successfully completed 5 projects while developing at Softaims.
I'm committed to continuous learning, always striving to stay current with the latest industry trends and technical methodologies. My work is driven by a genuine passion for solving complex, real-world challenges through creative and highly effective solutions. Through close collaboration with cross-functional teams, I've consistently helped businesses optimize critical processes, significantly improve user experiences, and build robust, scalable systems designed to last.
My professional philosophy is truly holistic: the goal isn't just to execute a task, but to deeply understand the project's broader business context. I place a high priority on user-centered design, maintaining rigorous quality standards, and directly achieving business goals—ensuring the solutions I build are technically sound and perfectly aligned with the client's vision. This rigorous approach is a hallmark of the development standards at Softaims.
Ultimately, my focus is on delivering measurable impact. I aim to contribute to impactful projects that directly help organizations grow and thrive in today’s highly competitive landscape. I look forward to continuing to drive success for clients as a key professional at Softaims.
key benefits of following our NodeJs Developer Roadmap to accelerate your learning journey.
The NodeJs Developer Roadmap guides you through essential topics, from basics to advanced concepts.
It provides practical knowledge to enhance your NodeJs Developer skills and application-building ability.
The NodeJs Developer Roadmap prepares you to build scalable, maintainable NodeJs Developer applications.

What is Node.js? Node.js is a JavaScript runtime built on Chrome’s V8 engine.
Node.js is a JavaScript runtime built on Chrome’s V8 engine. It lets you run JS on servers, CLIs, and build tools—not only in browsers—so teams can share language and types across full stacks.
Its event-driven, non-blocking I/O model suits networked apps: APIs, proxies, streaming, and real-time gateways. The standard library covers HTTP, TCP, files, crypto, and child processes; npm supplies everything else.
JavaScript executes on a single thread per process; libuv handles the thread pool for disk and DNS where needed. Async APIs return quickly and resume via callbacks, promises, or async/await.
You ship a package.json, lockfile, and environment-specific config. Production setups pair Node with a reverse proxy, process manager, and structured logging—not a naked node app.js on port 80.
// Minimal HTTP server (Node 18+ can use fetch in workers too)
import { createServer } from 'node:http';
const server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from Node');
});
server.listen(3000);
What is V8? V8 is Google’s open-source JavaScript engine. It compiles JS to machine code with optimizing tiers, which is why Node and Chrome feel fast for typical server workloads.
V8 is Google’s open-source JavaScript engine. It compiles JS to machine code with optimizing tiers, which is why Node and Chrome feel fast for typical server workloads.
You rarely tune V8 directly; instead you choose a supported Node LTS, avoid accidental deopts (hidden classes, megamorphic sites), and profile when CPU is hot.
Node exposes --max-old-space-size and diagnostic flags for memory issues. For isolation, run separate processes or workers rather than fighting one giant heap.
What is the event loop? The event loop schedules JavaScript callbacks after I/O, timers, and microtasks complete. It is why one thread can juggle thousands of idle connections.
The event loop schedules JavaScript callbacks after I/O, timers, and microtasks complete. It is why one thread can juggle thousands of idle connections.
Phases handle timers, pending callbacks, poll, check, and close—order matters when debugging “why did this run first?”.
Microtasks (queueMicrotask, promise reactions) run before the next loop phase. Never starve the loop with infinite synchronous work.
Use setImmediate vs setTimeout(0) deliberately on the server; in busy systems prefer explicit queues (Bull, SQS) over hammering timers.
setTimeout(() => console.log('timer'), 0);
Promise.resolve().then(() => console.log('microtask'));
console.log('sync');
// sync, microtask, timer
What is non-blocking I/O? Non-blocking I/O means your thread does not sit idle waiting on disk or network.
Non-blocking I/O means your thread does not sit idle waiting on disk or network. Node delegates work to the kernel or thread pool and continues executing other JavaScript.
Throughput improves because you spend CPU on business logic instead of waiting—but CPU-bound work still blocks unless offloaded.
Prefer async fs.promises, streams for large files, and connection pooling for databases. Avoid *Sync methods on request paths.
Backpressure matters: pipe streams and respect drain events so memory does not balloon.
import { readFile } from 'node:fs/promises';
const data = await readFile('config.json', 'utf8');
console.log(JSON.parse(data));
What is libuv? libuv is the C library Node uses for the event loop, thread pool, timers, TCP/UDP sockets, and cross-platform async I/O abstractions.
libuv is the C library Node uses for the event loop, thread pool, timers, TCP/UDP sockets, and cross-platform async I/O abstractions.
V8 runs your JavaScript; libuv talks to the OS and schedules work so callbacks fire when handles are ready.
The default thread pool size affects fs and DNS work—tune UV_THREADPOOL_SIZE only when profiling shows pool starvation.
File operations often use the pool; many network operations are handled by the kernel and the loop’s poll phase.
// You rarely import libuv directly—it's under the hood of:
import { readFile } from 'node:fs/promises';
import { createServer } from 'node:net';
// Both ultimately coordinate with libuv + the event loop
What are process and Node globals? process exposes the running Node process: argv, env, cwd(), signals, and exit. It is how CLIs and servers read ports, secrets, and feature flags.
process exposes the running Node process: argv, env, cwd(), signals, and exit. It is how CLIs and servers read ports, secrets, and feature flags.
Node also provides globals like Buffer, queueMicrotask, and globalThis—browser-only APIs such as window are absent.
Validate and coerce process.env at startup; fail fast on missing required variables.
Listen for SIGTERM/SIGINT to close servers and drains cleanly in containers.
const port = Number(process.env.PORT) || 3000;
console.log('argv', process.argv.slice(2));
console.log('cwd', process.cwd());
process.on('SIGTERM', () => {
console.log('shutting down');
process.exit(0);
});
What are Node.js modules? Modules split code into files with explicit boundaries: exports define public APIs; importers depend on those surfaces only.
Modules split code into files with explicit boundaries: exports define public APIs; importers depend on those surfaces only.
Node supports CommonJS (require / module.exports) and ECMAScript modules (import / export). New projects increasingly standardize on ESM.
Keep modules focused—one responsibility per file. Barrel files help UX but can hurt tree-shaking if abused.
Use node: protocol imports (node:fs) to disambiguate builtins and satisfy linters.
// math.mjs
export const add = (a, b) => a + b;
// main.mjs
import { add } from './math.mjs';
console.log(add(2, 3));
What is CommonJS? CommonJS is Node’s original module format: synchronous require() loads and evaluates dependencies; module.exports publishes values.
CommonJS is Node’s original module format: synchronous require() loads and evaluates dependencies; module.exports publishes values.
Most legacy npm packages still ship CJS; interoperability layers let ESM import them with care.
exports is an alias of module.exports until you reassign—then break the link. Prefer module.exports = { ... } for clarity.
Dynamic require inside functions can hide dependency graphs from bundlers; document when you truly need it.
const path = require('node:path');
function greet(name) {
return `Hello ${name}`;
}
module.exports = { greet };
What are ES modules in Node? ES modules use static import/export syntax aligned with browsers. Node enables them via "type": "module" in package.json or .mjs extensions.
ES modules use static import/export syntax aligned with browsers. Node enables them via "type": "module" in package.json or .mjs extensions.
They enable better static analysis, top-level await on modern versions, and clearer circular dependency errors.
Use explicit file extensions in relative imports for ESM portability. Configure TypeScript moduleResolution to match runtime.
For dual packages, maintain exports maps in package.json so consumers resolve correctly.
import fs from 'node:fs/promises';
export async function loadJson(p) {
const raw = await fs.readFile(p, 'utf8');
return JSON.parse(raw);
}
require vs import require is a runtime function (CJS); import is a module graph edge parsed before execution (ESM). That changes hoisting, top-level await, and tree-shaking.
require is a runtime function (CJS); import is a module graph edge parsed before execution (ESM). That changes hoisting, top-level await, and tree-shaking.
Mixing both in one file without a strategy causes friction—pick a primary style per package.
ESM cannot use require without createRequire; use dynamic import() for conditional loading in ESM.
Jest and some tools still assume CJS—configure transforms or use NODE_OPTIONS=--experimental-vm-modules when needed.
// ESM dynamic import (returns a promise)
const { readFile } = await import('node:fs/promises');
What is Node core modules? Core modules ship with Node—http, https, fs, path, crypto, stream, child_process, and more—no install step.
Core modules ship with Node—http, https, fs, path, crypto, stream, child_process, and more—no install step.
They are stable, documented APIs for systems programming in JavaScript.
Prefer node: prefixed imports. Read deprecation notices (e.g. url.parse vs WHATWG URL) when upgrading LTS lines.
What are npm packages? The npm registry hosts reusable libraries—frameworks, clients, parsers, CLIs.
The npm registry hosts reusable libraries—frameworks, clients, parsers, CLIs. Installing adds semver-pinned artifacts under node_modules plus a lockfile for reproducible builds.
Private registries and provenance features help enterprises govern supply chains.
Audit with npm audit; verify maintainer reputation and install counts for critical paths.
Dedupe transitive deps; watch for duplicate versions of the same major that inflate bundles.
npm install fastify
npm ls fastify
Global vs local installs Local installs live in ./node_modules and belong on the dependency graph—libraries your app imports.
Local installs live in ./node_modules and belong on the dependency graph—libraries your app imports.
Global installs (npm i -g) expose CLIs on your PATH—useful for typescript, pm2, scaffolding tools—not runtime libraries.
CI and Docker should never rely on undeclared globals; pin tool versions with npx or devDependencies.
Use corepack to manage pnpm/yarn versions consistently across machines.
npm install --save-dev eslint
npm install -g npm@10
module.exports vs exports In CommonJS, module.exports is the real export object. exports starts as a shorthand reference to it.
In CommonJS, module.exports is the real export object. exports starts as a shorthand reference to it.
If you assign exports = {} you break the link—callers still see the original module.exports.
Use module.exports = factory() when exporting a single function or class.
Attach named properties with exports.x = only while exports still aliases module.exports.
exports.add = (a, b) => a + b;
// same as: module.exports.add = ...
module.exports = function create() { return {}; };
// must use module.exports for reassignment
What is package management? Package management tracks dependencies, scripts, and metadata so installs are reproducible across laptops, CI, and production.
Package management tracks dependencies, scripts, and metadata so installs are reproducible across laptops, CI, and production.
npm, pnpm, and Yarn resolve semver ranges, hoist (or isolate) node_modules, and execute lifecycle scripts.
Commit lockfiles; use npm ci in pipelines for clean, fast installs.
Separate dependencies vs devDependencies; scan for unused packages periodically.
{
"name": "api",
"private": true,
"scripts": { "start": "node src/index.js" },
"dependencies": { "fastify": "^4.28.0" }
}
What is npm? npm is the default CLI and registry client for Node. It installs packages, runs scripts, and publishes scoped modules.
npm is the default CLI and registry client for Node. It installs packages, runs scripts, and publishes scoped modules.
Workspace support coordinates monorepos with multiple packages.
Configure .npmrc for registries, auth tokens, and install strategy. Use overrides to patch transitive vulnerabilities when upstream is slow.
What belongs in package.json? package.json declares name, version, entry points, scripts, engines, and dependency ranges. Tools read it for builds, audits, and publishing.
package.json declares name, version, entry points, scripts, engines, and dependency ranges. Tools read it for builds, audits, and publishing.
The exports field defines public entry surfaces for modern resolution.
Pin Node with "engines" and enforce in CI. Document npm run tasks for onboarding.
Use repository and license fields for open-source hygiene.
{
"name": "@acme/api",
"type": "module",
"engines": { "node": ">=20" }
}
How does npm install work? npm install resolves the dependency tree, fetches tarballs, extracts to node_modules, and updates the lockfile when ranges allow.
npm install resolves the dependency tree, fetches tarballs, extracts to node_modules, and updates the lockfile when ranges allow.
Adding a package writes semver ranges—understand ^ vs ~ vs exact pins for production stability.
Use npm install [email protected] --save-exact for critical infra. Prefer Renovate or Dependabot for ongoing updates.
npm ci deletes node_modules first—ideal for reproducible builds.
npm install
npm install [email protected]
npm install -D vitest
What are npm scripts? Scripts automate dev servers, tests, migrations, and builds. They run with the local node_modules/.bin on PATH.
Scripts automate dev servers, tests, migrations, and builds. They run with the local node_modules/.bin on PATH.
Lifecycle hooks (prestart, posttest) chain tasks without extra tooling.
Keep scripts short—delegate to node scripts/foo.mjs for complex logic.
Cross-platform: use rimraf or shx instead of raw rm -rf when teammates use Windows.
{
"scripts": {
"dev": "node --watch src/server.js",
"test": "node --test",
"lint": "eslint ."
}
}
Yarn and pnpm Yarn (Berry or classic) and pnpm offer alternative installers with caching, workspaces, and stricter node_modules layouts.
Yarn (Berry or classic) and pnpm offer alternative installers with caching, workspaces, and stricter node_modules layouts.
pnpm’s content-addressable store dedupes disk usage—popular in large monorepos.
Pick one package manager per repo; commit its lockfile only. Use corepack enable to pin versions.
Migration: reinstall from scratch and run the full test suite—resolution differences surface edge cases.
corepack enable
pnpm install
yarn install
Why async JavaScript in Node? Node APIs are overwhelmingly asynchronous so the event loop stays responsive. Callbacks, promises, and async/await are three layers of the same idea.
Node APIs are overwhelmingly asynchronous so the event loop stays responsive. Callbacks, promises, and async/await are three layers of the same idea.
Mastering them prevents subtle bugs: double callbacks, swallowed rejections, and blocking loops.
Prefer async/await with try/catch for linear control flow; wrap legacy callbacks with util.promisify.
Use Promise.all for independent work; sequential await when order or rate limits matter.
import { readFile } from 'node:fs/promises';
async function main() {
const [a, b] = await Promise.all([
readFile('a.json', 'utf8'),
readFile('b.json', 'utf8'),
]);
return [JSON.parse(a), JSON.parse(b)];
}
What are callbacks? Callbacks are functions passed into APIs to run when work finishes. Node’s early APIs follow the error-first convention: (err, result) => ….
Callbacks are functions passed into APIs to run when work finishes. Node’s early APIs follow the error-first convention: (err, result) => ….
They compose well for small scripts but scale poorly when nested deeply (“callback hell”).
Always handle err first; never throw from async callbacks without a domain handler.
Convert to promises when chains exceed two levels—readability wins.
import fs from 'node:fs';
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
What are promises? Promises represent a future value: pending, fulfilled, or rejected. They standardize async results and enable .then / .catch chains.
Promises represent a future value: pending, fulfilled, or rejected. They standardize async results and enable .then / .catch chains.
Node’s fs/promises, dns/promises, and many drivers expose promise-first APIs.
Return promises from functions so callers can chain. Always end chains with .catch or use async wrappers.
Avoid mixing raw callbacks and promises in one flow without explicit bridging.
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
await delay(100);
console.log('done');
What is async/await? async functions always return a promise. await pauses the function until a promise settles, writing linear code that still runs non-blocking.
async functions always return a promise. await pauses the function until a promise settles, writing linear code that still runs non-blocking.
It is syntactic sugar over .then—errors propagate to the nearest try/catch.
Do not await inside tight loops without batching—use Promise.all with bounded concurrency utilities when calling external APIs.
Top-level await in ESM simplifies CLIs and boot scripts on supported Node versions.
async function fetchUser(id) {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
What are EventEmitters? EventEmitter is Node’s pub/sub primitive: .on registers listeners, .emit dispatches namespaced events with payloads. Many core APIs (http.
EventEmitter is Node’s pub/sub primitive: .on registers listeners, .emit dispatches namespaced events with payloads.
Many core APIs (http.Server, streams) inherit from it.
Remove listeners on shutdown to avoid leaks in long-lived processes.
Prefer once for one-shot handlers; set captureRejections when async listeners should surface errors.
import { EventEmitter } from 'node:events';
const bus = new EventEmitter();
bus.on('order:placed', (id) => console.log('order', id));
bus.emit('order:placed', 'ord_123');
What is promise chaining? Chaining links asynchronous steps: each .then returns a new promise, passing transformed values forward.
Chaining links asynchronous steps: each .then returns a new promise, passing transformed values forward.
It replaces nested callbacks while keeping error propagation via .catch.
Return promises from .then handlers—implicitly flattens nested async work.
Prefer async/await for multi-step business logic; reserve .then chains for simple pipelines.
fetch('/api/user')
.then((r) => r.json())
.then((u) => fetch(`/api/posts/${u.id}`))
.then((r) => r.json())
.catch(console.error);
Promise.all and Promise.race Promise.all fails fast if any input rejects—ideal when every result is required. Promise.allSettled waits for all outcomes; Promise.
Promise.all fails fast if any input rejects—ideal when every result is required.
Promise.allSettled waits for all outcomes; Promise.race resolves with the first settled promise.
For timeouts, race the work promise against a delay promise—remember to cancel underlying I/O when possible.
Map fixed-size arrays; empty Promise.all([]) resolves immediately to [].
const results = await Promise.all([
fetch('/a').then((r) => r.json()),
fetch('/b').then((r) => r.json()),
]);
Handling errors in async code Unhandled promise rejections crash Node in strict modes; always attach catch or try/catch around await.
Unhandled promise rejections crash Node in strict modes; always attach catch or try/catch around await.
Callbacks use error-first signatures; promises reject; async functions throw to callers as rejections.
Centralize HTTP error mapping in middleware. Log correlation IDs, not stacks, to clients.
Use cause when wrapping errors to preserve root failures.
async function safe() {
try {
await mightFail();
} catch (err) {
console.error('failed', err);
throw new Error('wrapped', { cause: err });
}
}
Web servers in Node Node’s http and https modules create low-level servers: you parse URLs, headers, and bodies manually or via frameworks.
Node’s http and https modules create low-level servers: you parse URLs, headers, and bodies manually or via frameworks.
Understanding this layer clarifies what Express, Fastify, and Nest abstract away.
Terminate TLS at a reverse proxy or use https.createServer with certificates you rotate.
Set timeouts, max header sizes, and keep-alive behavior deliberately—defaults are not always production-safe.
import { createServer } from 'node:http';
createServer((req, res) => {
if (req.url === '/' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('home');
return;
}
res.writeHead(404).end();
}).listen(3000);
The http module http.createServer returns a Server that emits request events with IncomingMessage and ServerResponse objects.
http.createServer returns a Server that emits request events with IncomingMessage and ServerResponse objects.
It is streaming-first: read bodies chunk by chunk for large uploads.
Share one server instance behind a cluster or load balancer; avoid port conflicts in containers.
Prefer higher-level frameworks for routing matrices; drop to raw http for proxies or WebSocket upgrades.
import http from 'node:http';
const server = http.createServer();
server.on('request', (req, res) => {
res.end('ok');
});
server.listen(8080);
Requests and responses The request carries method, URL, headers, and a readable stream body. The response lets you set status, headers, and write/end the body.
The request carries method, URL, headers, and a readable stream body. The response lets you set status, headers, and write/end the body.
HTTP/2 APIs exist under node:http2 for multiplexed connections.
Always end responses—hanging sockets exhaust file descriptors.
Validate Content-Type and size limits before buffering bodies into memory.
// Inspect basics
console.log(req.method, req.url);
res.setHeader('X-Request-Id', crypto.randomUUID());
res.writeHead(204).end();
Basic routing Routing maps (method, path) to handlers. In vanilla Node you branch on req.url—often after parsing with URL and a base.
Routing maps (method, path) to handlers. In vanilla Node you branch on req.url—often after parsing with URL and a base.
Frameworks add parameters, middleware chains, and declarative mounts.
Normalize trailing slashes and case for public APIs—document the canonical form.
Separate static asset routes from API namespaces (/api/v1/...).
const { pathname } = new URL(req.url, 'http://localhost');
if (req.method === 'GET' && pathname === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
return;
}
What is Express? Express is a minimal web framework for Node: routing, middleware, and pluggable view engines sit atop http.
Express is a minimal web framework for Node: routing, middleware, and pluggable view engines sit atop http.
Its middleware model influenced Fastify, Koa, and Hapi—learning Express teaches patterns you will see everywhere.
Mount middleware in order: parsers, auth, routers, error handlers last.
Do not block the event loop in handlers—offload CPU work and stream large payloads.
import express from 'express';
const app = express();
app.use(express.json());
app.get('/api/health', (_req, res) => res.json({ ok: true }));
app.listen(3000);
Express routing Routers group related endpoints. Methods mirror HTTP verbs; Router instances keep files modular.
Routers group related endpoints. Methods mirror HTTP verbs; Router instances keep files modular.
Path parameters (:id) and regex routes cover REST shapes; next() passes control or errors.
Extract nested routers for /api/users vs /api/orders. Version APIs under /v1 when breaking changes ship.
Use express-async-errors or wrapper utilities so async route errors reach error middleware.
import { Router } from 'express';
const r = Router();
r.get('/users/:id', (req, res) => {
res.json({ id: req.params.id });
});
export default r;
What is middleware? Middleware functions receive (req, res, next). They can end the response or call next() to continue the chain.
Middleware functions receive (req, res, next). They can end the response or call next() to continue the chain.
Cross-cutting concerns—logging, auth, rate limits—belong here instead of duplicating per route.
Distinguish error middleware (err, req, res, next) with four parameters—register it after routes.
Third-party middleware order matters: cors before auth, body parsers before handlers that read req.body.
app.use((req, res, next) => {
console.log(req.method, req.url);
next();
});
Express req and res Express augments Node’s request/response: req.query, req.params, req.body (with parsers), res.json, res.sendFile.
Express augments Node’s request/response: req.query, req.params, req.body (with parsers), res.json, res.sendFile.
Typed wrappers (TypeScript, Zod) validate inputs before they hit business logic.
Set cache headers explicitly for GET JSON that can be CDN-fronted.
Avoid storing large objects on req without typing—use res.locals for view-specific data.
app.get('/search', (req, res) => {
const q = String(req.query.q ?? '');
res.json({ q });
});
Server-side templates Template engines (EJS, Pug, Handlebars) render HTML on the server with data—classic for multi-page apps and emails.
Template engines (EJS, Pug, Handlebars) render HTML on the server with data—classic for multi-page apps and emails.
API-first teams may skip them, but admin dashboards and transactional mail still benefit.
Escape user content by default; disable raw interpolation unless you sanitize.
Cache compiled templates in production; watch file I/O on every request in dev.
app.set('view engine', 'ejs');
app.get('/hello', (req, res) => {
res.render('hello', { name: req.query.name || 'guest' });
});
Building APIs with Node APIs expose resources over HTTP with predictable URLs, verbs, and representations—usually JSON for SPAs and mobile clients.
APIs expose resources over HTTP with predictable URLs, verbs, and representations—usually JSON for SPAs and mobile clients.
Good APIs are versioned, documented (OpenAPI), and observable with metrics and tracing.
Design DTOs that hide internal models; never leak ORM entities directly if fields are sensitive.
Return problem+json or consistent error envelopes so clients can branch reliably.
app.get('/api/v1/items', async (_req, res, next) => {
try {
const items = await listItems();
res.json({ data: items });
} catch (e) {
next(e);
}
});
What is REST? REST maps resources to nouns (/users, /orders) and uses HTTP methods for intent. HATEOAS is optional; most teams ship pragmatic REST+JSON.
REST maps resources to nouns (/users, /orders) and uses HTTP methods for intent. HATEOAS is optional; most teams ship pragmatic REST+JSON.
Idempotent PUT/DELETE and safe GET semantics help caches and retries behave.
Use status codes honestly: 201 + Location on create, 204 on empty success, 409 for conflicts.
Paginate list endpoints; document default sort and filter behavior.
CRUD and HTTP verbs Create maps to POST, read to GET, update to PUT/PATCH, delete to DELETE. Partial updates favor PATCH with JSON Merge Patch or explicit schemas.
Create maps to POST, read to GET, update to PUT/PATCH, delete to DELETE. Partial updates favor PATCH with JSON Merge Patch or explicit schemas.
Soft deletes keep audit trails; expose deleted_at filters in admin APIs only.
Validate IDs before database calls—return 400 for malformed input, 404 when missing.
Use transactions when create spans multiple tables.
// Typical mapping
// POST /users -> create
// GET /users/:id -> read
// PATCH /users/:id -> update
// DELETE /users/:id -> remove
JSON in APIs JSON is the lingua franca for web APIs: easy for browsers, mobile, and serverless consumers to parse.
JSON is the lingua franca for web APIs: easy for browsers, mobile, and serverless consumers to parse.
Dates, big integers, and binary need conventions—ISO strings, string-encoded decimals, or separate download endpoints.
Set Content-Type: application/json; charset=utf-8. Limit body size at the parser.
Stream JSON lines for huge exports instead of building giant arrays in memory.
res.json({
user: { id: 'usr_1', name: 'Asha' },
meta: { requestId: req.id },
});
Structuring API routes Group routes by resource and middleware needs. Prefix public vs internal paths; mount version routers under /v1. Feature folders (routes/users.
Group routes by resource and middleware needs. Prefix public vs internal paths; mount version routers under /v1.
Feature folders (routes/users.js, services/users.js) scale better than one giant app.js.
OpenAPI-first: generate clients or validate requests against a shared schema.
Use reverse proxies for path-based routing to multiple Node services.
import usersRouter from './routes/users.js';
app.use('/api/users', usersRouter);
HTTP status codes Status codes communicate outcome without parsing bodies: success (2xx), redirection (3xx), client error (4xx), server error (5xx).
Status codes communicate outcome without parsing bodies: success (2xx), redirection (3xx), client error (4xx), server error (5xx).
Consistency across endpoints reduces client bugs and support noise.
Prefer 422 for validation errors with field details; 401 vs 403 for auth vs authorization.
Log 5xx with stack traces internally; return generic messages externally.
if (!user) return res.status(404).json({ error: 'not_found' });
if (!allowed) return res.status(403).json({ error: 'forbidden' });
return res.status(200).json({ data: user });
Reading the request body POST/PUT bodies arrive as streams. express.json() and express.urlencoded() parse common formats into req.body.
POST/PUT bodies arrive as streams. express.json() and express.urlencoded() parse common formats into req.body.
Multipart uploads use multer or busboy for backpressure-aware parsing.
Validate shape with Zod/Yup immediately after parse—reject before DB.
For webhooks, verify signatures before trusting body content.
app.use(express.json({ limit: '256kb' }));
app.post('/login', (req, res) => {
const { email, password } = req.body;
// validate...
});
API best practices Document authentication, rate limits, and pagination. Provide stable error codes and human-readable messages.
Document authentication, rate limits, and pagination. Provide stable error codes and human-readable messages.
Observability: propagate X-Request-Id, emit structured logs, trace slow queries.
Deprecate fields with sunset headers; avoid silent breaking changes.
Load test critical paths; set SLOs on p95 latency and error rate.
// Consistent envelope
function ok(data) {
return { ok: true, data };
}
function fail(code, message) {
return { ok: false, error: { code, message } };
}
Databases with Node Node talks to SQL and NoSQL engines through drivers and higher-level ORMs/ODMs. Choose based on consistency needs, query patterns, and ops maturity.
Node talks to SQL and NoSQL engines through drivers and higher-level ORMs/ODMs. Choose based on consistency needs, query patterns, and ops maturity.
Connection pools are mandatory at scale—open per process, not per request.
Migrations version schema; never hand-edit production without rollback plans.
Monitor pool saturation and slow queries; use timeouts to shed load gracefully.
// Conceptual pool (pg)
import pg from 'pg';
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const { rows } = await pool.query('select now()');
What is PostgreSQL for Node apps? Relational databases enforce schemas, joins, and transactions—ideal for ledgers, inventory, and reporting.
Relational databases enforce schemas, joins, and transactions—ideal for ledgers, inventory, and reporting.
Node pairs well via pg, Prisma, Drizzle, Knex, or Sequelize depending on abstraction taste.
Use parameterized queries or ORM APIs—never concatenate user input into SQL strings.
Index foreign keys and filter columns you sort on; explain analyze before blaming Node.
NoSQL with Node Document, key-value, wide-column, and graph stores trade SQL joins for flexible schemas and horizontal scaling patterns.
Document, key-value, wide-column, and graph stores trade SQL joins for flexible schemas and horizontal scaling patterns.
Redis excels at cache, rate limiting, and pub/sub; MongoDB fits evolving document shapes.
Model access paths, not ER diagrams—duplicate read-optimized fields when necessary.
Understand consistency guarantees (strong vs eventual) per product requirement.
// Redis GET (ioredis-style)
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
await redis.set('session:1', 'token', 'EX', 3600);
What is Mongoose? Mongoose adds schemas, middleware, and validation on top of the MongoDB Node driver.
Mongoose adds schemas, middleware, and validation on top of the MongoDB Node driver.
It helps teams keep document shape consistent while retaining Mongo’s flexibility.
Define indexes in schema; watch unique errors map to 409 responses.
Use lean queries for read-heavy paths when you do not need document methods.
What is Sequelize? Sequelize is a promise-based ORM for SQL databases with migrations, associations, and hooks. It suits teams wanting an ActiveRecord-like layer over raw SQL.
Sequelize is a promise-based ORM for SQL databases with migrations, associations, and hooks.
It suits teams wanting an ActiveRecord-like layer over raw SQL.
Prefer sequelize-cli migrations in CI. Eager loading avoids N+1 but can over-fetch—profile.
For greenfield TypeScript, compare Prisma/Drizzle ergonomics before committing.
Authentication and authorization Authentication proves who someone is; authorization decides what they may do.
Authentication proves who someone is; authorization decides what they may do. Node apps implement both at the edge (middleware) and domain layer (policies).
Combine password hashing, MFA, OAuth, and tokens based on threat model—not every app needs the same stack.
Never log secrets. Rotate keys; use short-lived access tokens plus refresh flows where appropriate.
Centralize session invalidation and audit sensitive actions.
// Pseudocode middleware
function requireUser(req, res, next) {
if (!req.user) return res.status(401).json({ error: 'unauthorized' });
next();
}
Password hashing Store salted slow hashes—bcrypt, scrypt, or Argon2—not reversible encodings. Compare with constant-time APIs.
Store salted slow hashes—bcrypt, scrypt, or Argon2—not reversible encodings. Compare with constant-time APIs.
Enforce minimum length and breach checks (Have I Been Pwned) where UX allows.
Tune bcrypt cost factor as hardware improves. Prefer Argon2id for new systems.
Password reset flows should be rate-limited and single-use tokens.
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash('user-password', 12);
const ok = await bcrypt.compare('user-password', hash);
What is Passport.js? Passport is authentication middleware for Express with pluggable strategies—local, OAuth, OpenID, JWT. It standardizes how credentials become req.user.
Passport is authentication middleware for Express with pluggable strategies—local, OAuth, OpenID, JWT.
It standardizes how credentials become req.user.
Serialize users for sessions carefully—store IDs, not whole profiles, in cookies.
Keep strategies in separate modules and test failure paths.
JSON Web Tokens JWTs encode claims signed (JWS) or encrypted (JWE). Common pattern: short-lived access JWT + refresh stored server-side or in secure cookie.
JWTs encode claims signed (JWS) or encrypted (JWE). Common pattern: short-lived access JWT + refresh stored server-side or in secure cookie.
They are credentials—treat leakage like password compromise.
Validate iss, aud, exp on every request. Rotate signing keys with kid headers.
Prefer opaque server sessions when you need instant revocation everywhere.
import jwt from 'jsonwebtoken';
const token = jwt.sign({ sub: 'user_1' }, process.env.JWT_SECRET, {
expiresIn: '15m',
algorithm: 'HS256',
});
Session-based auth Sessions store server-side state keyed by a cookie. express-session with Redis scales horizontally if sticky sessions are avoided.
Sessions store server-side state keyed by a cookie. express-session with Redis scales horizontally if sticky sessions are avoided.
Good fit when you need instant logout and centralized revocation.
Mark cookies HttpOnly, Secure, SameSite. Regenerate session ID after login.
CSRF protect state-changing browser forms when using cookies.
import session from 'express-session';
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, sameSite: 'lax' },
}));
Security in Node apps Threats include injection, XSS, CSRF, broken auth, and misconfigured CORS. Defense layers: framework defaults, headers, validation, and monitoring.
Threats include injection, XSS, CSRF, broken auth, and misconfigured CORS. Defense layers: framework defaults, headers, validation, and monitoring.
Assume hostile input from every header, query, and body field.
Automate dependency scanning in CI. Run containers as non-root with read-only FS where possible.
Practice least privilege for DB roles and cloud IAM.
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: { directives: { defaultSrc: ["'self'"] } },
}));
What is CORS? Browsers enforce same-origin policy; CORS headers let servers opt into cross-origin reads from JavaScript.
Browsers enforce same-origin policy; CORS headers let servers opt into cross-origin reads from JavaScript.
Preflight OPTIONS requests validate methods and headers before sending credentialed calls.
Whitelist origins—avoid * when cookies or auth headers are involved.
APIs consumed only by server-side clients may skip CORS entirely.
import cors from 'cors';
app.use(cors({
origin: ['https://app.example.com'],
credentials: true,
}));
What is Helmet? Helmet sets security-related HTTP headers: Content-Security-Policy, X-Frame-Options, HSTS, and more. It is a low-effort baseline for Express apps.
Helmet sets security-related HTTP headers: Content-Security-Policy, X-Frame-Options, HSTS, and more.
It is a low-effort baseline for Express apps.
Tune CSP for your frontend asset hosts; report-only mode helps rollouts.
Combine with reverse proxy headers in Kubernetes ingress.
Rate limiting Rate limits cap requests per IP, user, or API key—reducing brute force and scraper damage.
Rate limits cap requests per IP, user, or API key—reducing brute force and scraper damage.
Distributed systems need Redis-backed counters or edge rules (Cloudflare, API Gateway).
Return 429 with Retry-After. Exempt health checks carefully.
Combine with CAPTCHA or WAF for public login endpoints.
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({ windowMs: 60_000, max: 100 });
app.use('/api/', limiter);
Input validation Validate shape, ranges, and enums at trust boundaries—before DB or shell calls. Libraries like Zod/Joi compose readable schemas.
Validate shape, ranges, and enums at trust boundaries—before DB or shell calls. Libraries like Zod/Joi compose readable schemas.
Sanitization differs by context (HTML vs SQL vs shell)—use the right tool per sink.
Reject unknown keys on public APIs to prevent mass-assignment surprises.
Log validation failures at warn level with field paths, not raw payloads.
import { z } from 'zod';
const Body = z.object({
email: z.string().email(),
age: z.number().int().min(0).max(120),
});
SQL injection prevention SQLi tricks the database into executing attacker-controlled SQL. Parameterized queries and ORMs eliminate the classic string-concatenation hole.
SQLi tricks the database into executing attacker-controlled SQL. Parameterized queries and ORMs eliminate the classic string-concatenation hole.
ORMs are not magic—raw fragments and dynamic ORDER BY still need allowlists.
Code review any $queryRaw equivalents. Escape identifiers separately from values when needed.
Least-privilege DB users cannot DROP production even if a bug slips through.
// Good: placeholder
await pool.query('select * from users where id = $1', [userId]);
// Bad: never
// query(`select * from users where id = '${userId}'`)
XSS and CSRF XSS injects scripts into pages users trust. CSRF tricks browsers into submitting actions with ambient credentials.
XSS injects scripts into pages users trust. CSRF tricks browsers into submitting actions with ambient credentials.
Content Security Policy, escaping output, and anti-CSRF tokens address different facets.
Use templating auto-escape; avoid dangerouslySetInnerHTML patterns on servers too.
SameSite cookies plus double-submit tokens protect cookie-based sessions.
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'"
);
Secure cookies HttpOnly hides cookies from JavaScript, reducing token theft via XSS. Secure sends only over HTTPS.
HttpOnly hides cookies from JavaScript, reducing token theft via XSS. Secure sends only over HTTPS.
SameSite mitigates CSRF; Lax is a common default for session cookies.
Rotate session secrets and invalidate on privilege changes.
Prefer __Host- cookie prefixes when spec allows for strongest guarantees.
res.cookie('sid', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 7 * 24 * 3600 * 1000,
});
Error handling in Node Reliable services map failures to responses, logs, and metrics—without leaking internals. Errors cross sync, callback, promise, and stream boundaries.
Reliable services map failures to responses, logs, and metrics—without leaking internals. Errors cross sync, callback, promise, and stream boundaries.
Operational vs programmer errors differ: retry the former, fix the latter.
Use typed errors (name, code, HTTP status). Wrap unknown throws at process edges.
Crash only when state is unrecoverable; otherwise degrade gracefully with circuit breakers.
class HttpError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
throw new HttpError(404, 'User not found');
try / catch try/catch handles synchronous throws and awaited rejections inside async functions. It does not catch errors inside plain promise callbacks unless you await or .catch.
try/catch handles synchronous throws and awaited rejections inside async functions.
It does not catch errors inside plain promise callbacks unless you await or .catch.
Avoid empty catches—log or rethrow with context.
Use finally for cleanup (file handles, timers) paired with AbortController for fetch.
async function run() {
try {
await job();
} catch (err) {
console.error('job failed', err);
throw err;
}
}
Uncaught exceptions uncaughtException and unhandledRejection fire when errors escape all handlers. They are last-resort hooks—not your primary strategy.
uncaughtException and unhandledRejection fire when errors escape all handlers. They are last-resort hooks—not your primary strategy.
Log, flush telemetry, then exit cleanly so orchestrators restart a fresh process.
Long-running ignore handlers leave corrupt state—prefer fail-fast after logging.
In clusters, let the supervisor replace dead workers.
process.on('unhandledRejection', (reason) => {
console.error('unhandled', reason);
process.exit(1);
});
Express error middleware Error middleware has four arguments (err, req, res, next) and runs when next(err) is called or async errors are forwarded.
Error middleware has four arguments (err, req, res, next) and runs when next(err) is called or async errors are forwarded.
It centralizes logging, mapping, and response shaping.
Register after all routes. Distinguish HttpError from unknown 500s.
Never send stack traces to clients in production.
app.use((err, req, res, _next) => {
console.error(err);
const status = err.status || 500;
res.status(status).json({ error: err.message });
});
Custom error classes Subclass Error (or AggregateError) to attach HTTP codes, error codes for clients, and cause chains.
Subclass Error (or AggregateError) to attach HTTP codes, error codes for clients, and cause chains.
Consumers can instanceof branch without fragile string matching.
Set name and capture stack in constructor.
Map domain failures (CardDeclined) to stable API error slugs.
export class ValidationError extends Error {
constructor(message, fields) {
super(message);
this.name = 'ValidationError';
this.fields = fields;
this.status = 422;
}
}
Testing Node applications Tests guard regressions in pure functions, HTTP handlers, and database integrations. Node ships node:test and assert; Jest and Vitest add ergonomics.
Tests guard regressions in pure functions, HTTP handlers, and database integrations. Node ships node:test and assert; Jest and Vitest add ergonomics.
Balance fast unit suites with a thin layer of integration tests hitting real ports or testcontainers.
Inject dependencies instead of hard-wiring globals—easier to mock I/O.
Run tests in CI on every PR; fail builds on coverage drops for critical modules.
import { test } from 'node:test';
import assert from 'node:assert/strict';
test('adds', () => {
assert.equal(1 + 1, 2);
});
What is Jest for Node? Jest provides runners, matchers, snapshots, and mocks—common in Express and Nest codebases. Use with supertest to assert HTTP status and JSON bodies.
Unit testing Unit tests exercise one module in isolation—mock databases, queues, and third-party HTTP.
Unit tests exercise one module in isolation—mock databases, queues, and third-party HTTP.
They should be deterministic: no real network, clocks frozen with fake timers when needed.
Table-driven tests cover edge cases compactly.
Name tests after behavior, not method names.
import { describe, it, expect, vi } from 'vitest';
import { priceWithTax } from './pricing.js';
describe('pricing', () => {
it('adds tax', () => {
expect(priceWithTax(100, 0.2)).toBe(120);
});
});
Integration testing Integration tests spin real app instances, hit HTTP endpoints, and often use test databases or containers.
Integration tests spin real app instances, hit HTTP endpoints, and often use test databases or containers.
They catch wiring bugs mocks hide—middleware order, parser limits, connection strings.
Truncate tables or use transactions per test for isolation.
Parallelize suites with unique ports or dynamic listen(0).
import request from 'supertest';
import { app } from '../src/app.js';
const res = await request(app).get('/api/health');
expect(res.status).toBe(200);
Assertions Assertions state expected outcomes—equality, throws, partial object matches. Clear messages save debugging time. Matchers like expect(x).
Assertions state expected outcomes—equality, throws, partial object matches. Clear messages save debugging time.
Matchers like expect(x).toMatchObject express intent better than deep manual compares.
Assert on public API contracts, not private fields.
Use snapshot tests sparingly for CLI output or serializers.
import assert from 'node:assert/strict';
assert.deepEqual(
{ a: 1 },
{ a: 1 }
);
Mocking and spies Mocks replace collaborators—HTTP clients, time, randomness—so tests stay fast and deterministic.
Mocks replace collaborators—HTTP clients, time, randomness—so tests stay fast and deterministic.
Spies record calls without replacing behavior, useful for verifying side effects.
Mock at module boundaries, not deep internals.
Restore globals after tests to avoid cross-test pollution.
import { vi } from 'vitest';
const send = vi.fn().mockResolvedValue({ ok: true });
await notifyUser(send, 'hi');
expect(send).toHaveBeenCalledOnce();
Project structure Structure communicates architecture: where routes, services, data access, and config live. New hires should navigate without a tour.
Structure communicates architecture: where routes, services, data access, and config live. New hires should navigate without a tour.
There is no single “right” layout—consistency within a repo matters more than dogma.
Colocate tests or use __tests__ mirrors. Keep config environment-specific secrets out of git.
Document boundaries: what may import what. Enforce with ESLint boundaries or Nx tags.
src/
app.js
routes/
services/
lib/
config/
MVC pattern Model-View-Controller splits data, presentation, and request handling. In APIs the “view” is often JSON serializers.
Model-View-Controller splits data, presentation, and request handling. In APIs the “view” is often JSON serializers.
It tames growth when controllers stay thin and models own invariants.
Avoid fat controllers—delegate to services for rules spanning entities.
Views/templates belong separate from JSON controllers to prevent accidental HTML in APIs.
// routes -> controller -> service -> model
// controller: HTTP only
// service: business rules
// model: persistence
Service and repository layers Repositories encapsulate queries; services orchestrate use cases across repositories and brokers.
Repositories encapsulate queries; services orchestrate use cases across repositories and brokers.
Tests mock repositories to validate service logic without a database.
One service method per user story often maps cleanly to endpoints.
Transactions live at service boundaries.
export class UserService {
constructor(repo) {
this.repo = repo;
}
async register(dto) {
// validate, hash password, save via repo
}
}
Folders by feature Feature folders (billing/, auth/) keep routes, schemas, and tests together—popular in modular monoliths.
Feature folders (billing/, auth/) keep routes, schemas, and tests together—popular in modular monoliths.
Shared kernels (shared/, lib/) hold cross-cutting utilities.
Avoid circular imports—extract shared types to a leaf package if needed.
Microservice extraction is easier when features are already bounded.
src/
billing/
routes.js
service.js
billing.test.js
auth/
...
The config/ directory Centralize non-secret defaults and schema-validated environment loading. Secrets come from env vars or vaults—not committed JSON. 12-factor apps read process.
Centralize non-secret defaults and schema-validated environment loading. Secrets come from env vars or vaults—not committed JSON.
12-factor apps read process.env at boot; fail fast if required keys are missing.
Use dotenv only in development; production injects env via platform.
Document every variable in README or .env.example.
import 'dotenv/config';
const port = Number(process.env.PORT || 3000);
if (!process.env.DATABASE_URL) {
throw new Error('DATABASE_URL required');
}
middleware/ directory Dedicated folders for auth, logging, and rate limit middleware keep app.js readable. Each file exports a factory or configured handler with tests.
Dedicated folders for auth, logging, and rate limit middleware keep app.js readable.
Each file exports a factory or configured handler with tests.
Order imports explicitly—document the chain in a single applyMiddleware(app) helper.
Version breaking middleware changes with release notes.
import { auth } from './middleware/auth.js';
import { logger } from './middleware/logger.js';
app.use(logger);
app.use(auth);
utils and helpers Pure helpers—date formatting, ID generation, small parsers—live in lib/ or utils/ without framework imports.
Pure helpers—date formatting, ID generation, small parsers—live in lib/ or utils/ without framework imports.
If a helper knows about Express, it probably belongs closer to HTTP layer.
Unit test utilities heavily—they are reused everywhere.
Avoid utils/misc.js junk drawers; split by domain when files grow.
export function slugify(s) {
return s.toLowerCase().trim().replace(/\s+/g, '-');
}
errors/ directory Custom error classes and mappers (toHttp) centralized reduce duplication across routes. Pair with i18n keys if clients render localized messages.
Custom error classes and mappers (toHttp) centralized reduce duplication across routes.
Pair with i18n keys if clients render localized messages.
Export a stable code string per error type for client branching.
Log internal cause stacks server-side only.
export class NotFoundError extends Error {
constructor(resource) {
super(`${resource} not found`);
this.code = 'NOT_FOUND';
this.status = 404;
}
}
Deploying Node.js Deployment moves artifacts from CI to runnable environments: containers, PaaS, or VMs behind load balancers.
Deployment moves artifacts from CI to runnable environments: containers, PaaS, or VMs behind load balancers.
You own graceful shutdown, health checks, and observability—not just git push.
Build once, promote immutable images across stages. Run migrations as separate jobs with locks.
Expose /healthz for liveness and /ready when dependencies are up.
Dockerfile excerpt:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "src/index.js"]
Environment variables process.env injects configuration per environment. Treat every value as untrusted string input until validated. Platforms like Kubernetes, Fly.
process.env injects configuration per environment. Treat every value as untrusted string input until validated.
Platforms like Kubernetes, Fly.io, and Vercel expose secrets as env vars at runtime.
Never log secrets. Redact known keys in structured log serializers.
Use Zod/envalid to coerce types (PORT string to number).
const schema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']),
PORT: z.coerce.number().default(3000),
});
export const env = schema.parse(process.env);
What is PM2? PM2 keeps Node processes alive, clusters workers, rotates logs, and hooks into startup scripts. Common on VPS deployments before Kubernetes adoption.
PM2 keeps Node processes alive, clusters workers, rotates logs, and hooks into startup scripts.
Common on VPS deployments before Kubernetes adoption.
Use ecosystem files for env and instances. Combine with nginx/caddy for TLS termination.
Test pm2 reload graceful shutdown handlers locally.
Docker for Node Containers package Node, native deps, and app code for reproducible runs across laptops and cloud.
Containers package Node, native deps, and app code for reproducible runs across laptops and cloud.
Multi-stage builds keep final images small—compile in builder, copy node_modules + dist to runtime.
Run as non-root. Scan images in CI (Trivy, Grype).
Set NODE_ENV=production and use npm ci --omit=dev.
# Build
docker build -t myapi:1 .
docker run --rm -p 3000:3000 -e DATABASE_URL myapi:1
Hosting options Fly.io, Render, Railway, Heroku-style PaaS, AWS ECS/Lambda, and GCP Cloud Run each trade control for convenience.
Fly.io, Render, Railway, Heroku-style PaaS, AWS ECS/Lambda, and GCP Cloud Run each trade control for convenience.
Match choice to traffic shape: always-on APIs vs bursty serverless.
Estimate egress costs. Put static assets on CDN.
Automate deploys from main with preview environments per PR when possible.
# Example: run locally same as prod
NODE_ENV=production node src/index.js
Performance optimization Profile before optimizing—CPU flame graphs, event loop lag, GC pauses. Many “slow Node” issues are N+1 queries or blocking sync I/O.
Profile before optimizing—CPU flame graphs, event loop lag, GC pauses. Many “slow Node” issues are N+1 queries or blocking sync I/O.
Caching, pooling, and horizontal scale address different bottlenecks.
Use clinic.js or 0x for local diagnosis. Track p95 latency and saturation, not averages only.
Offload CPU-heavy work to worker threads or separate services.
// Prefer streams for large responses
import { createReadStream } from 'node:fs';
createReadStream('big.json').pipe(res);
Clustering The cluster module forks workers sharing a port—using all cores for HTTP throughput.
The cluster module forks workers sharing a port—using all cores for HTTP throughput.
Process managers and container orchestrators often replace hand-rolled cluster for lifecycle.
Sticky sessions complicate scale—prefer stateless JWT or centralized sessions.
Recycle workers gracefully to deploy without dropping connections.
import cluster from 'node:cluster';
import os from 'node:os';
if (cluster.isPrimary) {
for (let i = 0; i < os.availableParallelism(); i++) cluster.fork();
} else {
await import('./server.js');
}
Caching Caches store expensive read results—HTTP responses, ORM entities, computed aggregates—in memory or Redis. TTLs and invalidation rules prevent stale data bugs.
Caches store expensive read results—HTTP responses, ORM entities, computed aggregates—in memory or Redis.
TTLs and invalidation rules prevent stale data bugs.
Cache keys must include version and auth scope when data is user-specific.
Use ETag / If-None-Match for conditional GETs.
const cache = new Map();
function getCached(key, ttlMs, fetcher) {
const hit = cache.get(key);
if (hit && Date.now() - hit.t < ttlMs) return hit.v;
const v = fetcher();
cache.set(key, { v, t: Date.now() });
return v;
}
Load balancing Load balancers spread traffic across instances for capacity and fault tolerance—L4 TCP vs L7 HTTP routing.
Load balancers spread traffic across instances for capacity and fault tolerance—L4 TCP vs L7 HTTP routing.
Health checks remove bad nodes; connection draining avoids dropped requests on deploy.
Terminate TLS at the edge; speak HTTP/2 to backends when supported.
Watch keep-alive settings between balancer and Node to avoid socket churn.
# nginx upstream sketch
upstream api {
server 10.0.0.1:3000;
server 10.0.0.2:3000;
}
Worker threads worker_threads run JavaScript in parallel isolates with message passing—good for CPU-heavy transforms without blocking the main loop.
worker_threads run JavaScript in parallel isolates with message passing—good for CPU-heavy transforms without blocking the main loop.
Unlike child_process, workers share more overhead but start faster for fine-grained tasks.
Pool workers; creation is not free. Transfer ArrayBuffers to avoid copying large buffers.
Handle uncaughtException inside workers or they exit silently.
import { Worker } from 'node:worker_threads';
const w = new Worker(new URL('./job.js', import.meta.url));
w.postMessage({ n: 40 });
w.once('message', console.log);
Asynchronous I/O (again) Performance wins come from never blocking the event loop on I/O—use async disk and network APIs, streams, and pipelines.
Performance wins come from never blocking the event loop on I/O—use async disk and network APIs, streams, and pipelines.
Profiling will show “long sync” stacks if someone slipped readFileSync into hot paths.
Batch small writes; use pipeline for stream error propagation.
Tune concurrency limits to external services—unbounded Promise.all can DOS your own dependencies.
import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
await pipeline(
createReadStream('in.txt'),
createWriteStream('out.txt')
);
Real-time applications Real-time UIs need push channels—WebSockets, SSE, or long polling. Node’s evented model suits many concurrent idle sockets.
Real-time UIs need push channels—WebSockets, SSE, or long polling. Node’s evented model suits many concurrent idle sockets.
Design auth, backpressure, and reconnection from day one.
Horizontal scale requires shared pub/sub (Redis, NATS) so messages cross instances.
Heartbeat/ping timeouts detect dead peers.
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
ws.send('hello');
});
WebSockets WebSockets upgrade HTTP to a bidirectional binary/text channel—lower overhead than polling for chat, games, and live metrics.
WebSockets upgrade HTTP to a bidirectional binary/text channel—lower overhead than polling for chat, games, and live metrics.
They are stateful—load balancers must support sticky upgrades or shared backends.
Authenticate during upgrade—cookies or tokens in query (mind leakage).
Compress selectively; permessage-deflate trades CPU for bandwidth.
// ws / undici / fastify-websocket patterns similar
// Always handle 'error', 'close', 'pong'
What is Socket.IO? Socket.IO layers features atop WebSocket: namespaces, rooms, acknowledgements, and fallbacks. It speeds product delivery when you need batteries included.
Socket.IO layers features atop WebSocket: namespaces, rooms, acknowledgements, and fallbacks.
It speeds product delivery when you need batteries included.
Version client and server together. Scale with Redis adapter for multi-node.
Mind memory per connected socket on large broadcasts.
Publish / subscribe Pub/Sub decouples publishers from subscribers via topics—Redis, RabbitMQ, Kafka, NATS each offer different durability guarantees.
Pub/Sub decouples publishers from subscribers via topics—Redis, RabbitMQ, Kafka, NATS each offer different durability guarantees.
Great for cache invalidation, fan-out notifications, and cross-service events.
Choose at-least-once vs exactly-once semantics based on business tolerance.
Idempotent consumers handle duplicate deliveries.
// Redis pub/sub sketch
import { createClient } from 'redis';
const pub = createClient({ url: process.env.REDIS_URL });
await pub.connect();
await pub.publish('events', JSON.stringify({ type: 'order.paid' }));
Real-time APIs Beyond sockets, SSE streams server events over plain HTTP—simpler for read-only dashboards through corporate proxies.
Beyond sockets, SSE streams server events over plain HTTP—simpler for read-only dashboards through corporate proxies.
GraphQL subscriptions add another real-time shape—pick based on client capabilities.
Document connection limits and rate limits for public endpoints.
Combine with message queues so API processes stay stateless.
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.flushHeaders();
res.write('data: hello\n\n');
});
Advanced Node topics At scale you adopt microservices, serverless, GraphQL, and service meshes—each solves organizational and technical pressures.
At scale you adopt microservices, serverless, GraphQL, and service meshes—each solves organizational and technical pressures.
None are free: operational complexity rises with distribution.
Start modular monolith; extract services when boundaries are clear and SLOs demand isolation.
Invest in tracing across processes before adding your tenth deployment unit.
// Architecture is code + ops + people
// Measure before splitting
Microservices Microservices split domains into independently deployable services with private data stores and explicit APIs.
Microservices split domains into independently deployable services with private data stores and explicit APIs.
They enable team autonomy and heterogeneous stacks but require robust DevOps.
Define SLOs per service. Use contracts (protobuf, OpenAPI) and consumer-driven tests.
Avoid distributed monoliths—every change still touching five repos means boundaries are wrong.
// Gateway routes /orders -> orders-service:3001
// Internal calls use mTLS or private networks
Serverless Node Functions-as-a-service run Node handlers on managed infrastructure—scale to zero, pay per invoke.
Functions-as-a-service run Node handlers on managed infrastructure—scale to zero, pay per invoke.
Cold starts, execution time limits, and stateless design change how you structure apps.
Keep bundles small; tree-shake dependencies. Reuse HTTP clients across invokes.
Push long jobs to queues; return 202 from HTTP entrypoints.
// Vercel / AWS Lambda style
export default async function handler(req) {
return Response.json({ ok: true });
}
What is GraphQL? GraphQL lets clients select fields and traverse a graph from one endpoint—reducing over-fetch when models are stable.
GraphQL lets clients select fields and traverse a graph from one endpoint—reducing over-fetch when models are stable.
Servers in Node often use Apollo, GraphQL Yoga, or Pothos with type-safe schemas.
Protect against depth and complexity attacks. Use DataLoader to batch N+1.
Caching differs from REST—HTTP caches help less; plan persisted queries or CDN rules carefully.
Containerization Containers bundle Node apps with exact libc and native modules—solving “works on my machine” for ops.
Containers bundle Node apps with exact libc and native modules—solving “works on my machine” for ops.
Kubernetes orchestrates many containers with scheduling, networking, and autoscaling.
Readiness probes should check DB connectivity, not only process up.
Limit CPU/memory requests so schedulers place pods correctly.
kubectl apply -f deployment.yaml
# Liveness: /healthz Ready: /ready
Service mesh A mesh injects sidecar proxies (Envoy, Linkerd) to handle mTLS, retries, traffic splits, and metrics transparently.
A mesh injects sidecar proxies (Envoy, Linkerd) to handle mTLS, retries, traffic splits, and metrics transparently.
It shifts cross-cutting networking out of application code—at the cost of another layer to operate.
Start without a mesh; adopt when many polyglot services need uniform policy.
Watch latency overhead on high-QPS paths.
# Istio / Linkerd annotate namespaces
# App keeps HTTP; mesh adds mutual TLS
