The Hard Parts of Servers & Node.js

  • Move Introduction to Node.js Internals
    Open Introduction to Node.js Internals

    Introduction to Node.js Internals

    Node.js is not just JavaScript on the server - it's a carefully architected system that combines JavaScript with powerful C++ internals to handle concurrent operations efficiently on a single thread.

    What Makes Node Special?

    Most server platforms create a new thread for each incoming request. With thousands of concurrent users, this means thousands of threads, each consuming memory and CPU resources for context switching.

    Node takes a radically different approach:

    Traditional Server (Thread per Request):
    Request 1 → Thread 1 → [waiting for DB] → Response
    Request 2 → Thread 2 → [waiting for file] → Response
    Request 3 → Thread 3 → [waiting for API] → Response
    
    Node.js (Single Thread + Event Loop):
    Request 1 → Event Loop → [queue DB callback] → Response
    Request 2 → Event Loop → [queue file callback] → Response
    Request 3 → Event Loop → [queue API callback] → Response
    

    The Two-Language Architecture

    Node.js is fundamentally **two langua

    Introduction to Node.js Internals 480 words
  • Move Node.js Architecture
    Open Node.js Architecture

    Node.js Architecture

    Understanding Node's architecture is crucial for writing efficient server-side JavaScript. Let's examine how JavaScript, V8, libuv, and the operating system work together.

    The Component Stack

    ┌─────────────────────────────────────────────────┐
    │              Your JavaScript Code                │
    ├─────────────────────────────────────────────────┤
    │                  Node.js APIs                    │
    │         (fs, http, net, crypto, etc.)           │
    ├─────────────────────────────────────────────────┤
    │               Node.js Bindings                   │
    │            (C++ bridging layer)                 │
    ├─────────────────────────────────────────────────┤
    │     V8 Engine          │        libuv           │
    │  (JS execution)        │   (Async I/O, loop)    │
    ├─────────────────────────────────────────────────┤
    │               Operating System                   │
    │     (Linux, macOS, Windows system calls)         │
    └────────────────────────────────────────────
    
    Node.js Architecture 547 words
  • Move The Event Loop: Overview
    Open The Event Loop: Overview

    The Event Loop: Overview

    The event loop is the heart of Node.js. It's the mechanism that allows Node to perform non-blocking I/O operations despite JavaScript being single-threaded.

    Why Do We Need It?

    JavaScript can only execute one piece of code at a time (single-threaded). But servers need to:

    • Handle thousands of concurrent connections
    • Read/write files without blocking
    • Make network requests without waiting
    • Process timers and intervals

    The event loop solves this by offloading operations to the system kernel (when possible) or a thread pool, then queuing callbacks when those operations complete.

    The Event Loop Diagram

       ┌───────────────────────────┐
    ┌─>│           timers          │   setTimeout, setInterval
    │  └─────────────┬─────────────┘
    │  ┌─────────────┴─────────────┐
    │  │     pending callbacks     │   System operation callbacks
    │  └─────────────┬─────────────┘
    │  ┌─────────────┴─────────────┐
    │  │       idle, prepare       │   Internal use only
    
    The Event Loop: Overview 655 words
  • Move Timers and Poll Phase
    Open Timers and Poll Phase

    Timers and Poll Phase

    Two of the most important event loop phases are timers and poll. Understanding how they work together is key to writing predictable async code.

    The Timers Phase

    How Timers Work

    When you call setTimeout() or setInterval(), Node doesn't create a new timer at the OS level for each one. Instead, it:

    1. Records the callback and threshold time
    2. Maintains a sorted list of pending timers
    3. Calculates when to check for due timers in poll phase
    // Timer threshold = minimum wait time, not exact execution time
    setTimeout(() => {
      console.log('This runs AFTER at least 100ms');
    }, 100);
    

    Timer Execution Order

    Timers with the same delay fire in order of registration:

    setTimeout(() => console.log('A'), 100);
    setTimeout(() => console.log('B'), 100);
    setTimeout(() => console.log('C'), 100);
    
    // Output: A, B, C (always in this order)
    

    Timer Coalescing

    Node coalesces timers for efficiency:

    Timers and Poll Phase 770 words
  • Move Check Phase and Close Callbacks
    Open Check Phase and Close Callbacks

    Check Phase and Close Callbacks

    The check phase and close callbacks phase complete the event loop cycle. While less discussed than timers and poll, they serve important roles.

    The Check Phase

    Purpose

    The check phase exists specifically for setImmediate(). It runs callbacks immediately after the poll phase completes.

    setImmediate(() => {
      console.log('This runs in check phase');
    });
    

    Why setImmediate Exists

    You might wonder: why not just use setTimeout(fn, 0)?

    // These are NOT equivalent:
    setTimeout(() => console.log('timeout'), 0);
    setImmediate(() => console.log('immediate'));
    

    The difference:

    • setTimeout(fn, 0) → Runs in timers phase (next iteration)
    • setImmediate(fn) → Runs in check phase (same iteration, after poll)

    The Key Guarantee

    Inside an I/O callback, setImmediate always fires first:

    const fs = require('fs');
    
    fs.readFile(__filename, () => {
      // Currently in
    
    Check Phase and Close Callbacks 770 words
  • Move process.nextTick and Microtasks
    Open process.nextTick and Microtasks

    process.nextTick and Microtasks

    These two mechanisms have the highest priority in Node's execution order. Understanding them is crucial for predictable async behavior.

    process.nextTick()

    Not Part of the Event Loop

    Despite its name, process.nextTick() is NOT part of the event loop. It runs between phases, before any phase begins.

    setImmediate(() => console.log('1. Check phase'));
    setTimeout(() => console.log('2. Timer phase'), 0);
    process.nextTick(() => console.log('3. nextTick'));
    console.log('4. Sync');
    
    // Output: 4, 3, 2, 1 (or 4, 3, 1, 2)
    // nextTick ALWAYS runs before any event loop phase
    

    The nextTick Queue

    All nextTick callbacks are processed before continuing:

    process.nextTick(() => {
      console.log('tick 1');
      process.nextTick(() => console.log('tick 2'));
    });
    
    setTimeout(() => console.log('timeout'), 0);
    
    // Output: tick 1, tick 2, timeout
    // All nextTicks drain before timer phase
    

    Warning: I/

    process.nextTick and Microtasks 769 words
  • Move Streams: Introduction
    Open Streams: Introduction

    Streams: Introduction

    Streams are one of Node's most powerful features. They let you process data piece by piece, without loading everything into memory at once.

    The Problem Streams Solve

    Without streams:

    const fs = require('fs');
    
    // This loads ENTIRE file into memory
    const data = fs.readFileSync('huge-file.csv'); // 2GB = 2GB RAM
    processData(data);
    

    With streams:

    const fs = require('fs');
    
    // This processes file in small chunks
    const stream = fs.createReadStream('huge-file.csv');
    stream.on('data', (chunk) => {
      processChunk(chunk); // 64KB at a time
    });
    

    Four Types of Streams

    ┌─────────────┐      ┌──────────────┐      ┌─────────────┐
    │  Readable   │─────>│   Transform  │─────>│  Writable   │
    │   Stream    │      │    Stream    │      │   Stream    │
    └─────────────┘      └──────────────┘      └─────────────┘
                                │
                         (can also be)
                                │
    
    
    Streams: Introduction 718 words
  • Move Readable Streams
    Open Readable Streams

    Readable Streams

    Readable streams are sources of data. Understanding their two modes and how to consume them is essential for efficient data processing.

    Creating Readable Streams

    From Files

    const fs = require('fs');
    
    const readable = fs.createReadStream('data.txt', {
      encoding: 'utf8',      // Return strings instead of Buffers
      highWaterMark: 16384,  // Chunk size (16KB)
    });
    

    From HTTP Requests

    const http = require('http');
    
    http.get('http://example.com/data', (response) => {
      // response is a readable stream
      response.on('data', (chunk) => {
        console.log(chunk.toString());
      });
    });
    

    Custom Readable

    const { Readable } = require('stream');
    
    const readable = new Readable({
      read(size) {
        // Push data when read() is called
        this.push('Hello ');
        this.push('World!');
        this.push(null); // Signal end of stream
      }
    });
    

    Two Reading Modes

    1. Flowing Mode

    Data is r

    Readable Streams 737 words
  • Move Writable and Transform Streams
    Open Writable and Transform Streams

    Writable and Transform Streams

    Writable streams consume data, and transform streams modify data as it passes through. Together with readable streams, they form the complete streaming picture.

    Writable Streams

    Creating Writable Streams

    const fs = require('fs');
    
    const writable = fs.createWriteStream('output.txt', {
      encoding: 'utf8',
      highWaterMark: 16384,  // 16KB buffer
      flags: 'w'             // 'w' write, 'a' append
    });
    

    Writing Data

    // write() returns boolean
    const canContinue = writable.write('Hello World\n');
    
    // If false, buffer is full - should pause
    if (!canContinue) {
      console.log('Buffer full, waiting for drain...');
    }
    
    // Signal end of writing
    writable.end('Final data\n');
    

    Important Events

    const writable = fs.createWriteStream('output.txt');
    
    // Buffer drained, ready for more data
    writable.on('drain', () => {
      console.log('Ready for more data');
    });
    
    // All data has been flushe
    
    Writable and Transform Streams 796 words
  • Move HTTP Server Fundamentals
    Open HTTP Server Fundamentals

    HTTP Server Fundamentals

    Node's HTTP module lets you build web servers from scratch. Understanding the underlying mechanics helps you appreciate what frameworks like Express abstract away.

    Creating a Basic Server

    const http = require('http');
    
    const server = http.createServer((request, response) => {
      // This callback runs for EVERY incoming request
      response.writeHead(200, { 'Content-Type': 'text/plain' });
      response.end('Hello World');
    });
    
    server.listen(3000, () => {
      console.log('Server running at http://localhost:3000/');
    });
    

    The Request Object

    The request (http.IncomingMessage) is a Readable Stream:

    http.createServer((req, res) => {
      // Request metadata
      console.log(req.method);       // 'GET', 'POST', etc.
      console.log(req.url);          // '/path?query=string'
      console.log(req.headers);      // { 'content-type': '...', ... }
      console.log(req.httpVersion);  // '1.1'
    
      // For POST/PUT - read body as stream
      l
    
    HTTP Server Fundamentals 859 words
  • Move Request-Response Pattern Under the Hood
    Open Request-Response Pattern Under the Hood

    Request-Response Pattern Under the Hood

    Understanding what happens between a client request and server response reveals how Node.js leverages the event loop for scalability.

    The Complete Flow

    Client                    Node.js Server              Operating System
      │                            │                           │
      │──────TCP Connect──────────>│                           │
      │                            │<────notify connection─────│
      │                            │   (libuv poll phase)      │
      │                            │                           │
      │────HTTP Request───────────>│                           │
      │                            │<────data available────────│
      │                            │   (poll phase callback)   │
      │                            │                           │
      │                            │   [Your code runs]        │
      │                            │                           │
      │<───HTTP Response───────────│                           
    
    Request-Response Pattern Under the Hood 898 words
  • Move Practical Patterns and Best Practices
    Open Practical Patterns and Best Practices

    Practical Patterns and Best Practices

    Let's consolidate everything with real-world patterns for production Node.js applications.

    Error Handling Patterns

    Never Throw in Async Code

    // BAD: Unhandled rejection
    async function fetchData() {
      const response = await fetch('https://api.example.com');
      if (!response.ok) {
        throw new Error('Failed to fetch'); // Becomes unhandled rejection
      }
      return response.json();
    }
    
    // GOOD: Handle at call site
    async function handler(req, res) {
      try {
        const data = await fetchData();
        res.json(data);
      } catch (err) {
        console.error('Fetch error:', err);
        res.status(500).json({ error: 'Internal error' });
      }
    }
    

    Global Error Handlers

    // Catch unhandled promise rejections
    process.on('unhandledRejection', (reason, promise) => {
      console.error('Unhandled Rejection:', reason);
      // Log to monitoring service
      // Optionally: process.exit(1);
    });
    
    // Catch uncaught exceptions
    
    Practical Patterns and Best Practices 983 words