Skip to main content

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 threadSingle thread handles all requests
Thread blocks while waiting for I/OI/O operations run asynchronously; thread continues
High memory overhead under concurrencyLow memory footprint even under high concurrency
Good for CPU-intensive tasksIdeal 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:

  1. timers: Executes setTimeout and setInterval callbacks whose delay has elapsed
  2. poll: Retrieves new I/O events; executes I/O callbacks
  3. check: Executes setImmediate callbacks
  4. 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:

ModulePurpose
fsFile system operations (read, write, watch files)
pathCross-platform path manipulation
http / httpsBuild HTTP servers without a framework
streamHandle streaming data (piping large files, transformations)
eventsEventEmitter — the backbone of Node's event-driven architecture
cryptoHashing, encryption, random bytes
osOS-level info (CPU count, free memory)
child_processSpawn shell commands or worker processes
worker_threadsTrue 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.

Pages in This Section