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.
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:
app.get("/orders/:id", handler) instead of parsing req.url yourself and extracting path segments by hand.express.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.