Have you ever wondered what happens behind the scenes when you browse the web? How does your browser communicate with servers to fetch websites and data? Recently, I completed the “Build Your Own HTTP Server” challenge on codecrafters.io, and I want to share what I learned through this hands-on experience.
In this post, I’ll walk you through creating a basic HTTP server using TypeScript. By the end, you’ll understand the fundamentals of HTTP servers and have the knowledge to build your own!
Creating a Socket Connection
The first step in building an HTTP server is establishing a socket connection. In networking, a socket is an endpoint for sending and receiving data across a network.
TCP is the underlying protocol used by HTTP servers. Let’s create a TCP server that listens on port 4221:
|
|
This code:
- Imports the
net
module, which provides an API for creating TCP servers in Node.js - Creates a server using
net.createServer()
that executes a callback when a client connects - Sets up an event listener to properly close connections
- Tells the server to listen on port 4221 on localhost (127.0.0.1)
Now we have a basic TCP server that can accept connections, but it doesn’t do anything useful yet.
Responding with a Basic HTTP 200 OK
HTTP responses have three parts, each separated by a CRLF (\r\n
):
- Status line
- Zero or more headers, each ending with a CRLF
- Optional response body
Let’s modify our server to respond with a simple “200 OK” status:
|
|
This response contains:
- Status line:
HTTP/1.1 200 OK\r\n
— Indicates the HTTP version, status code, and reason phrase - Headers: Empty, but marked by the second
\r\n
- Response body: Empty
Extracting URL Paths and Handling Routes
Now let’s make our server actually do something useful by parsing HTTP requests and responding differently based on the requested path.
An HTTP request consists of:
- Request line (HTTP method, request target, HTTP version)
- Zero or more headers
- Optional request body
Here’s how we can extract the URL path and respond with either a 200 OK or 404 Not Found:
|
|
This code:
- Listens for data coming through the socket
- Converts the binary data to a string
- Extracts the path from the request (the second part of the first line)
- Returns a 200 response for the root path (
/
), and 404 for anything else - Closes the connection
Responding with a Body
Let’s add an /echo/{str}
endpoint that returns whatever string is passed to it:
|
|
When implementing response bodies, we need to include these important headers:
Content-Type
: Tells the client what format the body is in (in this case,text/plain
)Content-Length
: Specifies the size of the body in bytes
Reading Request Headers
HTTP headers contain important metadata. Let’s implement a /user-agent
endpoint that reads the User-Agent
header from the request and returns it:
|
|
Note that header names are case-insensitive, so we need to handle that properly in a production server.
Handling Concurrent Connections
A real HTTP server needs to handle multiple clients simultaneously. Fortunately, Node.js’s event-driven architecture makes this easy - our existing code can handle concurrent connections without modification!
The JavaScript execution model is based on an event loop, which allows us to handle multiple connections without creating threads.
Serving Files
Let’s add the ability to serve files from the file system with a /files/{filename}
endpoint:
|
|
This code:
- Extracts the filename from the path
- Gets the directory path from command-line arguments.
- Tries to read the file
- If successful, returns the file with appropriate headers
- If the file doesn’t exist, returns a 404 response
Handling POST Requests and Request Bodies
Finally, let’s implement the ability to create files via POST requests to the /files/{filename}
endpoint:
|
|
This code:
- Parses the HTTP method from the request line
- Extracts the request body
- For GET requests, serves the file as before
- For POST requests, creates a new file with the request body content and returns a 201 Created response
Implementing HTTP Compression
HTTP compression is a technique that reduces the size of data transmitted between servers and clients, improving load times and reducing bandwidth usage. Let’s implement this important feature in our server.
Supporting Basic Compression Headers
First, we need to check if the client supports compression by reading the Accept-Encoding
header:
Let’s use the echo endpoint for http compression.
|
|
In this enhanced code, we’re:
- Creating types for structured HTTP requests and responses
- Checking if the
Accept-Encoding
header is set togzip
- If it is, adding a
Content-Encoding: gzip
header to our response
Handling Multiple Compression Schemes
Clients can request multiple compression schemes using a comma-separated list. Common compression schemes include gzip, Brotli, deflate, and zstd. Let’s update our code to handle multiple schemes:
|
|
This code:
- Splits the
Accept-Encoding
header by commas - Trims whitespace from each encoding scheme
- Checks if
gzip
is among the supported schemes
Actually Compressing the Content
Finally, let’s add actual gzip
compression to our responses:
|
|
In this code, we:
- Import Node.js’s built-in
zlib
module for compression - Convert our text to a Buffer
- Compress the Buffer using
gzipSync
- Update the
Content-Length
header to reflect the compressed size - Return the compressed data as the response body
Adding compression to our server makes it more efficient and production-ready. For small payloads like our examples, compression might actually increase the size due to overhead, but for larger content, the bandwidth savings can be substantial.
Conclusion
Building a HTTP server from scratch gives you a deep understanding of how web servers work. We’ve covered:
- Setting up a TCP server
- Parsing HTTP requests
- Generating HTTP responses
- Handling different routes
- Reading headers and request bodies.
- Serving and creating files
- Implementing HTTP compression
This is just the beginning - a production-ready HTTP server would need additional features like proper header parsing, robust error handling, connection pooling, and more.
Here’s the full source code: https://github.com/g-savitha/http-server
Have you built your own HTTP server or completed a similar challenge? What did you learn from the experience? Let me know in the comments below!
Until next time, happy coding! 💻 🎉