Node.js
Node.js is an open-source, cross-platform JavaScript runtime built on Chrome's V8 engine. Released in 2009 by Ryan Dahl, it brought JavaScript to the server side, enabling developers to use a single language across the full stack. Node.js powers a significant portion of the web's backend infrastructure — from Netflix and LinkedIn to NASA and PayPal.
Why Node.js?
Before Node.js, server-side development was dominated by synchronous, multi-threaded frameworks (Java servlets, PHP, Ruby on Rails). Each incoming request was handled by a thread; under high concurrency, this meant thousands of threads consuming memory.
Node.js takes a fundamentally different approach: single-threaded, event-driven, non-blocking I/O.
| Traditional (multi-threaded) | Node.js (event-driven) |
|---|---|
| Each request → new thread | Single thread handles all requests |
| Thread blocks while waiting for I/O | I/O operations run asynchronously; thread continues |
| High memory overhead under concurrency | Low memory footprint even under high concurrency |
| Good for CPU-intensive tasks | Ideal for I/O-intensive tasks (APIs, microservices, streaming) |
PayPal case study: After migrating from Java to Node.js, PayPal's engineers reported 35% faster page load times and 2× fewer developers needed to build the same features, while handling double the requests per second.
The Event Loop — How Node.js Works
Node.js operates on a single thread using an event loop managed by the libuv library. Understanding the event loop is critical:
┌─────────────────────────────────────────┐
│ Event Loop Phases │
│ │
│ timers → pending callbacks → idle → │
│ poll → check → close callbacks │
│ │
│ Between each phase: │
│ process.nextTick() and Promises drain │
└─────────────────────────────────────────┘
Key phases:
- timers: Executes
setTimeoutandsetIntervalcallbacks whose delay has elapsed - poll: Retrieves new I/O events; executes I/O callbacks
- check: Executes
setImmediatecallbacks - close callbacks: e.g.,
socket.on('close', ...)
process.nextTick() and resolved Promises run between every phase — they are microtasks and have the highest priority.
console.log('1');
setTimeout(() => console.log('2'), 0);
process.nextTick(() => console.log('3'));
Promise.resolve().then(() => console.log('4'));
console.log('5');
// Output: 1, 5, 3, 4, 2
Asynchronous Patterns
Callbacks (legacy)
const fs = require('fs');
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
Callback hell: Deeply nested callbacks become unmaintainable — the "pyramid of doom."
Promises
const fs = require('fs/promises');
fs.readFile('data.txt', 'utf8')
.then(data => console.log(data))
.catch(err => console.error(err));
Async/Await (modern standard)
async function readData() {
try {
const data = await fs.readFile('data.txt', 'utf8');
console.log(data);
} catch (err) {
console.error(err);
}
}
Async/await makes asynchronous code look and behave like synchronous code, dramatically improving readability. It is built on Promises — await pauses the function until the Promise resolves, but does not block the event loop.
Module Systems
CommonJS (CJS) — the original
// Export
module.exports = { add: (a, b) => a + b };
// Import
const { add } = require('./math');
ES Modules (ESM) — modern standard
// Export (math.mjs or "type": "module" in package.json)
export const add = (a, b) => a + b;
// Import
import { add } from './math.js';
Node.js supports both. ESM is the future — it enables static analysis, tree shaking, and top-level await.
Core Modules
Node.js ships with built-in modules — no installation needed:
| Module | Purpose |
|---|---|
fs | File system operations (read, write, watch files) |
path | Cross-platform path manipulation |
http / https | Build HTTP servers without a framework |
stream | Handle streaming data (piping large files, transformations) |
events | EventEmitter — the backbone of Node's event-driven architecture |
crypto | Hashing, encryption, random bytes |
os | OS-level info (CPU count, free memory) |
child_process | Spawn shell commands or worker processes |
worker_threads | True parallelism for CPU-intensive tasks |
npm — Node Package Manager
npm is the world's largest software registry with 2M+ packages. Every Node.js project has a package.json:
{
"name": "my-app",
"version": "1.0.0",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest"
},
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"jest": "^29.0.0",
"nodemon": "^3.0.0"
}
}
package-lock.json locks exact versions for reproducible installs. Always commit this file.
Express.js — The De Facto Web Framework
const express = require('express');
const app = express();
app.use(express.json());
app.get('/users/:id', async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
app.listen(3000, () => console.log('Server running on port 3000'));
Express is minimal and unopinionated. NestJS (built on Express/Fastify) provides Angular-like structure (decorators, dependency injection) for large-scale applications — widely used at enterprise US companies.
Streams — Handling Large Data
Streams process data in chunks, avoiding loading entire files into memory:
const fs = require('fs');
// Pipe a 10GB file without loading it into memory
fs.createReadStream('huge-file.csv')
.pipe(transform) // Transform stream (e.g., CSV parsing)
.pipe(fs.createWriteStream('output.json'));
Types: Readable, Writable, Duplex (both), Transform (modify data as it passes through).
Production Patterns
Process Management
- PM2: Process manager for Node.js — runs apps in cluster mode (one process per CPU core), auto-restarts on crash, provides logs and monitoring
- Docker + Kubernetes: Containerised Node.js apps orchestrated by Kubernetes — standard at major US tech companies
Cluster Mode
Node.js is single-threaded, but modern servers have multiple CPU cores. The cluster module (or PM2 cluster mode) forks worker processes:
const cluster = require('cluster');
const os = require('os');
if (cluster.isPrimary) {
os.cpus().forEach(() => cluster.fork());
} else {
require('./server'); // Each worker runs the HTTP server
}
Environment Variables
Never hardcode secrets. Use .env files (loaded via dotenv) and read via process.env:
require('dotenv').config();
const dbUrl = process.env.DATABASE_URL;
Error Handling Best Practices
// Catch unhandled promise rejections globally
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
process.exit(1); // Fail fast; let process manager restart
});
Node.js in the US Tech Ecosystem
Node.js is dominant in:
- API servers and microservices: REST and GraphQL APIs powering mobile apps
- Real-time applications: Chat (Slack clones), collaborative editing (Google Docs-like), live dashboards
- Serverless functions: AWS Lambda, Vercel, Netlify Functions — Node.js is the most common runtime
- Build tooling: Webpack, Vite, ESLint, Prettier, Jest — all run on Node.js
- BFF (Backend for Frontend): Next.js server-side rendering uses Node.js
Where Node.js is NOT ideal: CPU-intensive tasks (video transcoding, ML inference, scientific computation) — Python, Go, or Rust are better choices for CPU-bound work.