Node.js
Node.js is a JavaScript runtime built on Chrome's V8 engine. It enables developers to run JavaScript outside the browser, on servers, CLIs, embedded devices, and cloud functions. Its non-blocking I/O model and event-driven architecture make it particularly effective for I/O-bound workloads such as APIs, real-time services, and streaming pipelines.
Since its release in 2009, Node.js has become one of the most widely used server-side platforms. The npm ecosystem hosts millions of packages, and modern Node.js 20+ ships with stable ESM support, built-in test runners, structured diagnostics, and a robust set of core APIs for networking, cryptography, file system access, and process management.
This section covers everything from the event loop and module systems to production deployment with Docker and PM2. Each guide includes real APIs, runnable code, edge cases, and common mistakes observed in production codebases.
Traditional server platforms use a thread-per-request model. Each incoming request is assigned an operating system thread, which consumes memory and incurs context-switching overhead. Under high concurrency, this model struggles with memory exhaustion and thread scheduling latency.
Node.js uses a single event loop with a thread pool for asynchronous operations. I/O calls are offloaded to libuv, which notifies the event loop upon completion. As long as your application code does not perform long-running CPU work on the main thread, a single Node.js process can handle tens of thousands of concurrent connections with modest memory usage.
| Characteristic | Thread-per-Request | Node.js Event Loop |
|---|---|---|
| Memory per connection | 1-2 MB thread stack | Small heap object per connection |
| Blocking behavior | Blocks only current thread | Blocks entire process if main thread is busy |
| Best workload | CPU-bound, long-lived requests | I/O-bound, many concurrent connections |
| Scaling model | Vertical + horizontal | Cluster processes horizontally |
note
The http module is the foundation of every Node.js web framework. Understanding it helps you reason about request lifecycle, headers, backpressure, and error handling before adding abstraction layers like Express or Fastify.
| 1 | const http = require('http'); |
| 2 | |
| 3 | const server = http.createServer((req, res) => { |
| 4 | if (req.url === '/health' && req.method === 'GET') { |
| 5 | res.writeHead(200, { 'Content-Type': 'application/json' }); |
| 6 | res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() })); |
| 7 | return; |
| 8 | } |
| 9 | |
| 10 | res.writeHead(404, { 'Content-Type': 'application/json' }); |
| 11 | res.end(JSON.stringify({ error: 'Not found' })); |
| 12 | }); |
| 13 | |
| 14 | server.listen(3000, () => { |
| 15 | console.log('Server listening on http://localhost:3000'); |
| 16 | }); |
| 17 | |
| 18 | process.on('SIGTERM', () => { |
| 19 | server.close(() => { |
| 20 | console.log('Server closed gracefully'); |
| 21 | process.exit(0); |
| 22 | }); |
| 23 | }); |
Notice the explicit return after handling a route. Without it, execution continues and you risk sending a second response, which throws ERR_HTTP_HEADERS_SENT. Also note the SIGTERM handler for graceful shutdown — production servers must finish in-flight requests before exiting.
best practice
Node.js supports two module systems. CommonJS uses require() and module.exports and has been the default since Node.js was created. ECMAScript Modules (ESM) use import and export and are the modern standard, aligning browser and server JavaScript.
| 1 | // math.cjs (CommonJS) |
| 2 | function add(a, b) { |
| 3 | return a + b; |
| 4 | } |
| 5 | module.exports = { add }; |
| 6 | |
| 7 | // main.cjs |
| 8 | const { add } = require('./math.cjs'); |
| 9 | console.log(add(2, 3)); |
| 1 | // math.mjs (ESM) |
| 2 | export function add(a, b) { |
| 3 | return a + b; |
| 4 | } |
| 5 | |
| 6 | // main.mjs |
| 7 | import { add } from './math.mjs'; |
| 8 | console.log(add(2, 3)); |
To use ESM by default, set "type": "module" in package.json. Alternatively, use the .mjs extension for individual files. Mixing the two systems in the same project requires careful attention to interop rules, top-level await, and __dirname availability.
warning
The Node.js section is organized into focused guides that build from runtime fundamentals to production patterns. Start with the event loop and modules, then progress through core APIs and frameworks.
Event Loop & libuv
Timers, I/O phases, microtasks, nextTick, and event loop lag.
CommonJS vs ESM
require vs import, exports, interop, and package.json fields.
Buffers & Binary Data
Buffer, Blob, ArrayBuffer, encodings, and binary protocols.
Streams & Backpressure
Readable, Writable, Transform, pipes, and backpressure.
EventEmitter
Custom events, listeners, once, error handling, and patterns.
File System
fs/promises, streams, watching, permissions, and paths.
path, os & process
Environment variables, argv, signals, and OS information.
HTTP Server
createServer, routing, headers, and request/response lifecycle.
Express.js
Middleware, routing, error handling, and production patterns.
Fastify
Plugins, hooks, schemas, validation, and benchmarks.
Security
Helmet, CORS, input validation, secrets, and CSP.
Testing
Node.js test runner, Vitest, Jest, Supertest, and mocks.
Performance
Cluster, worker threads, profiling, memory, and event loop lag.
Deployment
PM2, Docker, env vars, health checks, and graceful shutdown.
Node.js 20 LTS and Node.js 22 bring meaningful improvements for production systems. The --experimental-strip-types flag supports native TypeScript execution, the built-in test runner is stable, and import.meta.dirname removes the need for path workarounds in ESM.
| Feature | Node.js 20+ | Benefit |
|---|---|---|
| Built-in test runner | node --test | No test framework dependency for unit tests |
| Watch mode | node --watch | Built-in file watcher for development |
| Permission model | --permission | Restrict filesystem and child process access |
| import.meta.dirname | Node 20.11+ | ESM equivalent of __dirname |
| Stable fetch | Global fetch | Standard HTTP client without node-fetch |
info
Even experienced developers make these mistakes in Node.js. Being aware of them early prevents subtle bugs and production outages.
Blocking the event loop
Synchronous file reads, heavy JSON parsing, and CPU-bound loops on the main thread delay all concurrent requests. Use fs/promises, streams, or worker threads for large workloads.
Unhandled promise rejections
An unhandled rejection can crash the process in strict mode. Always attach catch() handlers or centralize error handling in middleware and event listeners.
Memory leaks from event listeners
Adding listeners without removing them causes objects to stay alive. Use once() for one-time events and call removeListener() when components are destroyed.
Hard-coded secrets
Never commit API keys or database URLs. Load configuration from environment variables and validate it at startup with a schema library like Zod.