The first HTTP server I wrote wasn't good.
It wasn't fast. It wasn't secure. It certainly wasn't production-ready.
It barely deserved to be called a web server at all.
It accepted a TCP connection, read a request, and sent back a small HTML page.
Yet in those few hundred lines of code, I learned more about how the web actually works than I had after years of using web frameworks.
Most developers experience HTTP through libraries.
app.get("/", (req, res) => {
res.send("Hello, World!");
});
That's perfectly fine.
But behind those three lines is an enormous amount of machinery.
A socket has to be created.
The operating system has to accept a connection.
Bytes arrive over TCP.
Those bytes have to be buffered, parsed, and validated.
Headers need to be interpreted.
The request line has to be split into its method, path, and protocol version.
A response has to be constructed according to the HTTP specification.
The connection needs to be closed—or kept alive.
None of this is particularly complicated.
It's just hidden.
Once you've written even a tiny server yourself, network debugging becomes much easier.
A 400 Bad Request is no longer just an error message.
You understand that your parser rejected malformed input.
A 404 isn't something your framework invented.
It's simply a status code you decided to return.
Headers stop feeling mysterious.
You know they're just text separated by carriage returns.
The protocol becomes understandable because you've implemented part of it yourself.
One of the first surprises is how messy real requests are.
Browsers don't send identical headers.
Clients omit things.
Some send unexpected whitespace, or disconnect halfway through a request, or even intentionally send malformed data.
Writing a parser forces you to assume nothing.
Every byte has to be treated as untrusted. That's a lesson that extends far beyond networking.
Ironically, writing an HTTP server often makes you appreciate frameworks even more.
You understand exactly what they're saving you from.
Keep it small. Listen on a port and accept a connection.
Parse the request line and return a static HTML page.
That is enough.
You don't need routing, templates, middleware.
You certainly don't need thousands of dependencies.
You just need a socket and a curiosity about what happens underneath.
You will NEVER look at app.get("/") the same way again.