CodeOath
← All posts
Node.js36 min total · 14 parts

Node.js Fundamentals: The Runtime, the Event Loop, and Building Real APIs

Part 5 of 14 · ~2 min

Building an HTTP Server

Before reaching for a framework, it's worth seeing what's actually happening underneath one — Node's built-in http module is the whole foundation:

const http = require("http");

const server = http.createServer((req, res) => {
  if (req.method === "GET" && req.url === "/") {
    res.writeHead(200, { "Content-Type": "text/plain" });
    res.end("Hello from raw Node!");
    return;
  }

  if (req.method === "POST" && req.url === "/echo") {
    let body = "";
    req.on("data", (chunk) => { body += chunk; }); // request bodies arrive as a stream of chunks
    req.on("end", () => {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ youSent: body }));
    });
    return;
  }

  res.writeHead(404, { "Content-Type": "text/plain" });
  res.end("Not found");
});

server.listen(3000, () => console.log("Listening on port 3000"));

A few things worth noticing here. req isn't handed to you with a parsed body — it's a readable stream, and you have to manually accumulate 'data' chunks and know when 'end' fires before you have the whole thing (more on streams below). Routing is a hand-rolled chain of if statements checking req.method and req.url. There's no concept of "middleware" — anything you want to run for every request (logging, auth, CORS headers) has to be manually called at the top of every single handler, or you build your own dispatch wrapper to do it, which is effectively reinventing what Express already gives you.

Why frameworks like Express exist

Scale the raw example above to a real application — dozens of routes, path parameters (/orders/:id), JSON body parsing with size limits and content-type checks, cross-cutting concerns like auth and logging, consistent error responses — and the if/else chain and manual stream-buffering become unmanageable fast. Express (and frameworks like it) exist specifically to solve three recurring problems that the raw http module leaves entirely up to you:

  • Routing — declaring app.get("/orders/:id", handler) instead of parsing req.url yourself and extracting path segments by hand.
  • Middleware — a composable pipeline for cross-cutting logic (see Middleware Pipelines Compared for how this same pattern shows up well beyond Node), instead of remembering to call the same three functions at the top of every handler.
  • Body parsingexpress.json() handles the chunk-accumulation, content-type checking, and size limits that the raw example above did by hand, and hands your handler a ready-to-use req.body.

Express is still built directly on top of http.createServer under the hood — it's not a replacement for the runtime layer, just a much more ergonomic API sitting on top of it.