🌙
☀️ Dark
PART 11

HTTP Servers

TCP server, HTTP, routing, middleware, Fastify/Express.

Intermediate 45 min read

PART 11 — HTTP SERVERS

Engineering Challenge

Write a raw Node.js HTTP server that acts as a simple reverse proxy. It should listen on port 8000, take any incoming GET request, forward it to https://jsonplaceholder.typicode.com, and stream the response back to the original client.

View Solution
import http from 'http';
import https from 'https';

http.createServer((clientReq, clientRes) => {
  const options = {
    hostname: 'jsonplaceholder.typicode.com',
    port: 443,
    path: clientReq.url,
    method: clientReq.method,
    headers: clientReq.headers
  };

  const proxyReq = https.request(options, (proxyRes) => {
    clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
    proxyRes.pipe(clientRes);
  });

  clientReq.pipe(proxyReq);
  
  proxyReq.on('error', (e) => {
    clientRes.writeHead(500);
    clientRes.end('Proxy Error');
  });
}).listen(8000);

Revision Sheet

  • TCP: Continuous stream of bytes.
  • HTTP: Formatted text protocol on top of TCP.
  • Request: Method (GET/POST), Path (/users), Headers (metadata), Body (payload).
  • Response: Status (200 OK), Headers, Body.
  • Routing: Directing traffic based on Method and Path.
  • Middleware: Sequential functions operating on the request/response cycle.

Connections

Now that you understand how HTTP servers handle incoming requests, you need a way to store the data those requests bring in. In the next chapter, we will bridge the gap between our HTTP server and persistent storage by exploring Databases and PostgreSQL.

Learning Objectives

By the end of this chapter, you will understand how network applications communicate over the internet. You will learn to build TCP servers, upgrade them to HTTP servers, parse requests, manage responses, and structure large backends using routing and middleware. Finally, you will transition from raw Node.js HTTP servers to production-ready frameworks like Express and Fastify.

▶ View Solution

Solution implementation.

Prerequisites

You should have a strong grasp of JavaScript/TypeScript fundamentals, Node.js basics (event loop, streams, buffers), and fundamental networking concepts (IP addresses, ports).

▶ View Solution

Solution implementation.

Why Does This Exist?

Applications need a way to communicate across the globe. Without HTTP servers, every application would have to invent its own custom protocol for sending and receiving data. HTTP (HyperText Transfer Protocol) provides a universal, standardized language for clients (like browsers) and servers to exchange information.

▶ View Solution

Solution implementation.

The Problem Before the Solution

Before HTTP, moving information over the network meant using raw TCP (Transmission Control Protocol) sockets. A TCP socket is just a continuous stream of bytes. If you connect to a TCP server and send "HELLO", the server just gets bytes. It has no standard way of knowing what you are asking for, who you are, or what format you expect in return.

▶ View Solution

Solution implementation.

Why the Old Approach Breaks

Writing custom raw TCP protocols for every application is tedious. How do you distinguish a request for an image from a request for a text file? How do you send metadata like authentication tokens? Without a standard protocol, the internet could never have scaled, as every client would need to understand thousands of custom server protocols.

▶ View Solution

Solution implementation.

History

In 1989, Tim Berners-Lee invented HTTP at CERN. Initially, it was extremely simple (HTTP/0.9) — a client would open a TCP connection, send GET /file.html, and the server would dump the HTML text and close the connection. Over time, it evolved into HTTP/1.0, HTTP/1.1 (adding persistent connections, headers, caching), HTTP/2 (multiplexing), and HTTP/3 (QUIC/UDP).

▶ View Solution

Solution implementation.

Mental Model (Analogy -> Reality)

Analogy: Imagine a restaurant. Raw TCP is the road leading to the restaurant. Without a menu or a waiter, you just yell words at the building. HTTP is the waiter and the menu system. You write down an order (the Request): "I want a burger (Method: GET, Path: /burger), hold the pickles (Headers), and here is my payment (Body)." The waiter goes to the kitchen (Server), and brings back a tray (the Response) containing the food (Body) and a receipt (Status: 200 OK).

Reality: HTTP is simply a plain-text format sent over a TCP connection. An HTTP server opens a port, waits for TCP connections, reads the incoming bytes, parses them as an HTTP string, processes the logic, formats a string back as HTTP, and sends those bytes back over TCP.

▶ View Solution

Solution implementation.

Internal Working (Memory, stack, process, network, etc.)

When you start a server in Node.js, it asks the OS kernel to bind to a port (e.g., 3000) and listen for incoming TCP connections. When a request arrives, the OS triggers an interrupt, moving the packet from the network interface card (NIC) to the OS buffer. Node.js's event loop (via libuv) detects this, reads the raw bytes, and passes them to Node's internal HTTP parser. The parser identifies headers, method, URL, and body. It then emits a 'request' event, giving you a Request object (a Readable Stream) and a Response object (a Writable Stream).

▶ View Solution

Solution implementation.

Visual Explanation (ASCII diagrams)

[Client Browser]                       [Node.js Process]
       |                                       |
       |--- 1. TCP Handshake (SYN, ACK) ------>| (OS Kernel TCP Stack)
       |                                       |
       |--- 2. HTTP Request String ----------->| -> [TCP Socket]
       |    "GET /users HTTP/1.1\r\n           | -> [HTTP Parser]
       |     Host: api.com\r\n\r\n"            | -> Event Loop triggers 'request'
       |                                       |
       |                                       | -> Your app logic runs
       |                                       |
       |<-- 3. HTTP Response String -----------| <- [Response Stream]
            "HTTP/1.1 200 OK\r\n               |
             Content-Type: application/json\r\n|
             \r\n                              |
             [{\"id\":1}]"                       |
▶ View Solution

Solution implementation.

Syntax

In raw Node.js:

import http from 'http';

const server = http.createServer((req, res) => {
  // req is IncomingMessage, res is ServerResponse
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello World');
});

server.listen(3000);
▶ View Solution

Solution implementation.

Tiny Example

Let's build a server that greets you.

import http from 'http';

http.createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/hello') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ message: "Hello, engineer!" }));
  } else {
    res.writeHead(404);
    res.end("Not Found");
  }
}).listen(8080, () => console.log('Server running on 8080'));
▶ View Solution

Solution implementation.

Walkthrough

1. We import the built-in http module.
2. We call createServer which takes a callback. This callback fires every time a client sends an HTTP request.
3. We check the req.method and req.url. This is the foundation of Routing.
4. If it matches, we set the HTTP Status Code to 200 (OK), and set a Header (Content-Type) telling the client to expect JSON.
5. We write the body (our JSON string) and end the response.
6. server.listen(8080) binds our process to port 8080.

▶ View Solution

Solution implementation.

Break It

What happens if you remove res.end()?

http.createServer((req, res) => {
  res.writeHead(200);
  res.write("Data...");
  // Missing res.end()
}).listen(3000);

If you visit this in a browser, it will spin forever. The HTTP protocol allows for streaming data. The browser keeps waiting because the server never closed the stream.

▶ View Solution

Solution implementation.

Debug It

If your server is spinning, you can debug it using `curl -v http://localhost:3000`. You will see the connection remains open. To fix it, ensure every single logical path in your request handler eventually calls res.end() or pipes to the response stream.

▶ View Solution

Solution implementation.

Mini Project (20-30 min)

Goal: Build an HTTP server that parses a POST request body, reads JSON, and echoes it back.

import http from 'http';

http.createServer((req, res) => {
  if (req.method === 'POST' && req.url === '/echo') {
    let body = '';
    req.on('data', chunk => {
      body += chunk.toString(); // req is a readable stream
    });
    
    req.on('end', () => {
      try {
        const parsed = JSON.parse(body);
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ youSent: parsed }));
      } catch (e) {
        res.writeHead(400);
        res.end("Invalid JSON");
      }
    });
  } else {
    res.writeHead(404); res.end();
  }
}).listen(3000);
▶ View Solution

Solution implementation.

Bigger Project (1-2 hours)

Write a simple Express.js application with three routes, a custom logging middleware, and error handling for 404s.

▶ View Solution
typescript
// Implementation for HTTP Servers
console.log("Bigger project solution");

Interview Questions

Easy: What is the difference between a GET and POST request?

GET is used to retrieve data and parameters are placed in the URL query string. POST is used to submit data to the server, and the payload is placed in the HTTP request body.

Medium: How does middleware work in Express/Fastify?

Middleware are functions that have access to the request object, response object, and the `next` function in the application's request-response cycle. They can execute code, make changes to req/res, end the response, or call `next()` to pass control to the next middleware.

Hard: Why does parsing a large JSON body sometimes freeze a Node.js server, and how do you fix it?

JSON.parse is a synchronous, CPU-intensive operation. For large payloads, it blocks the event loop. To fix it, you should stream the parsing using libraries like `stream-json`, limit payload sizes, or offload parsing to a worker thread.

Senior: Explain how Keep-Alive connections work and their impact on performance.

Without Keep-Alive, every HTTP request requires a new TCP handshake, which adds latency. Keep-Alive (standard in HTTP/1.1) keeps the underlying TCP connection open after a request completes, allowing subsequent requests to reuse the same connection, dramatically reducing latency, especially over TLS where handshakes are expensive.

🏠 Curriculum NextVolume 2