There is a unique energy and satisfaction that comes from peeling back the layers of abstraction to understand how things really work. It is not just about building a tool; it is about the journey of discovery it.
I recently subscribed to CodeCrafters as I was quite hooked by their service. This platform offers a different kind of challenge. Instead of the classical gamified algorithmic puzzles, it invites you to build foundational technologies like an HTTP server, Git, Redis, or Kafka, from the ground up. The cherry on the top of the cake? You can tackle these in a wide array of programming languages.
The process is very straightforward yet very engaging. You are given a set of feature requirements (“here’s the input,” “we expect this output”) and set free to implement them. Then, you run your code against a comprehensive test suite. It is a practical and empowering experience that deserves genuine kudos.
it is still very expensive though.
A HTTP server from scratch
My journey began with the HTTP server challenge, tasked with implementing one according to HTTP 1.1 specifications. The starting point was a simple main.py
file. I started with a straightforward approach: a socket
server that parsed incoming request bytes
and used a long if...else
block to determine the response.
1 | # app/main.py |
It worked, but clearly something felt off. The code was not elegant, and the branching logic was a clear sign that this approach would not scale gracefully with additional features. This is where I find myself thinking like battle between the “ok it works” and “Now it works well.” Such tension is usually an invitation to learn, to refactor, and to find a path that honors both functionality and code craft.
A brief overview of the socket
library
But before we dive into the refactor, let’s gently demystify the core technology enabling this: the Python socket
library. Socket programming is the art of handling interprocess communication (IPC), allowing data to flow between processes, whether on the same machine or across the full network.
In Python the API is elegantly simple. Key functions like socket()
, .bind()
, .listen()
, .accept()
, .connect()
, .connect_ex()
, .send()
, .recv()
and .close()
permits you to handle the network. Creating a server that listens for a client connection is very straightforward and not complicated:
1 | socket.create_server(("localhost", <your_port: int>), reuse_port=True) |
The create_sever()
method returns the socket
and _RetAddress
objects. You can next interact with the socket
object in a context manager.
1 | connexion, addr = server_socket.accept() # wait for client |
The client’s request, a stream of bytes, follows the HTTP specification, in our case HTTP1.1 specifications. Parsing the initial status line, containing the method, target endpoint, and version, is our first step. My initial implementation did this directly in the main loop:
1 | if status_line.target == "/": |
The initial if...else
block was the trigger for change. It was functional but fragile to say the least. This is where the elegance of frameworks like Flask shines. They allow you to declare intent simply:
1 |
|
I started thinking how I could implement something a bit similar. The goal was not to replicate Flask, I do not have the skills for that, but to include a bit of its philosophy and bring clarity and maintainability to my code.
Turning raw bytes into a Request
object
The first step was to encapsulate the raw byte parsing logic. Instead of slicing strings in the main loop
, I created a Request
class to handle the intricate details, providing a clean, attribute-based interface.
1 | # server/request.py |
Once instantieted, the Request
object gives me nice encapsulation of the data recevied from the client. I can now ask for Request.status.target
or Request.headers.Host
instead of slicing strings. Here we model the problem domain instead of just manipulating data.
Next, I applied the same thinking to the response. Hand-crafting HTTP payloads in every branch was error-prone. By creating a Response
class with a __str__
dunder method, I centralized the HTTP formatting logic into a single, responsible interface.
1 | # server/response.py |
Returning the stringified response keeps handlers readable and centralizes the knowledge about HTTP formatting in one place. It ensures consistency and turns response generation from a chore into a simple, declarative act.
Now let’s decorate routes a bit like in Flask
The biggest ergonomics gain is the implementation of the routing system. A decorator registers the route and the function in a global table, keeping URL declarations close to the functions that serve them.
1 | # server/routes.py |
With this, defining a new endpoint became a moment of clarity:
1 |
|
A slimmer entry point
With parsing and routing abstracted away the main loop can be transformed from a set of conditionals into a clear dispatcher:
1 | # main.py |
The complex if...else
block vanished, replaced by a simple dictionary lookup. The system became more powerful by becoming simpler.
From static to dynamic routing
Of course, reality quickly presented a new lesson in humility. The challenge required a dynamic endpoint (/echo/<some_string>
), and my simple dictionary could not handle that. My initial design, while cleaner, was still incomplete.
A challenge is not a failure; it is information. It tells us how to adapt.
The solution I chose was the following: replace the dictionary with a list of tuples where I paired a compiled regex pattern with its handler function. I kept an edge case for the "/"
endpoint.
1 | ROUTES = [] |
The dispatch logic evolved into a pattern-matching loop:
1 | # code before is unchanged |
With this change I managed to somehow unlock dynamic routing and made my code a bit more flexible. In term of efficiency we unfortunately move from O(1)
(dictonnary lookup) to O(n)
. But it is fine usually the number of endpoint is not huge. So while the efficiency of looking up in routes will depend of the length of the list, it will stay pretty small.
Wrap up
We moved from manipulating bytes very roughly to some modeling concepts, from procedural branching to declarative routing. But the journey is not over yet. The next steps— include the implentation of concurrency and compression. This promises new lessons. I am genuinely excited to continue and share what comes next. There is a clearly certain satisfaction in building something with your own hands, even small, and understanding the layers behind the tools we use every day.