Node.js95 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 Express enters the picture, here's the version of billhook that actually shipped first — Node's built-in http module, nothing else:
const http = require("http");
const server = http.createServer((req, res) => {
if (req.method === "POST" && req.url === "/webhooks/meridian") {
let chunks = [];
req.on("data", (chunk) => chunks.push(chunk)); // the body arrives as a stream of chunks
req.on("end", () => {
const raw = Buffer.concat(chunks);
handleMeridianWebhook(raw, res);
});
return;
}
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not found");
});
server.listen(3000, () => console.log("billhook listening on 3000"));
Worth noticing: req doesn't arrive with a parsed body sitting on it — it's a readable stream, and accumulating 'data' chunks and waiting for 'end' is the only way to get the whole thing (streams get their own chapter shortly). Routing is a hand-checked if on req.method and req.url. And there's no concept of middleware at all — the one route this version has does its own body-accumulation dance inline, and the moment billhook needs a second route (the internal support API, a few chapters from now), that dance either gets copy-pasted or extracted into something that looks suspiciously like what a framework already gives you for free.
Why frameworks like Express exist
The moment billhook needs more than that one route — real path parameters, size-limited JSON parsing, auth that applies to some endpoints and not others, error responses that look the same everywhere — hand-checking req.method/req.url and manually buffering every request body stops scaling almost immediately. Express solves three specific problems the raw http module leaves entirely to you:
- Routing — declaring
app.post("/webhooks/meridian", handler)directly, with no hand-checking ofreq.methodandreq.urlanywhere in sight. - Middleware — one composable pipeline that anything cross-cutting can plug into once (the same shape shows up well past Node — see Middleware Pipelines Compared) rather than every handler separately remembering to invoke the same logging, auth, and CORS logic on its own.
- Body parsing — everything the raw version was doing inline by hand — buffering chunks, checking content type — collapses into a single
req.bodyalready sitting there by the time your handler runs.
None of this replaces the runtime layer from the previous section, either — Express is http.createServer the whole way down, just with a considerably friendlier API bolted on top of it.