Build an HTTP Server from Scratch

Build a working HTTP server using only Python's socket module.

Introduction

30 Second Summary

Every time you visit a website, your browser sends a text message to a server and waits for a reply. That invisible exchange follows a strict format called HTTP.

In this project, you will build your own HTTP server from scratch using only Python's built-in socket module. Your server will accept real browser connections, parse raw requests by hand, and return proper HTML pages and JSON responses.

What You'll Build

A fully functional HTTP/1.1 server built from raw TCP sockets that serves web pages and JSON API responses to your browser.

By the end of this project, you'll have:

  • A TCP socket server that accepts live browser connections on your machine.
  • A request parser and response builder that handle raw HTTP messages by hand.
  • A routing system that maps URL paths to Python handler functions, the same pattern real frameworks use internally.
  • Secret Mission: Add POST request handling so your server can receive and process data from clients.

Do I need any prerequisites?

This project assumes basic Python knowledge (variables, functions, dictionaries, and strings). No web development experience is required.

Set Up Your Project

Before you can build an HTTP server, you need a project folder and a Python file to work in.

In this step, you will confirm that Python works in your terminal, create your project folder, and verify that Python can run your file.

In this step, get ready to:
  • Confirm Python is working in your terminal.
  • Create a project folder and a server.py file.
  • Verify Python can run your file without errors.
Verify Python is installed and working

You have Python installed, but you need to confirm which command your system uses to run it.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • Open a terminal inside VS Code by clicking Terminal in the top menu bar, then New Terminal.
  • Check your Python version by running this command:
python --version

You should see something like Python 3.12.x printed in the terminal.

Command not recognized?

On some systems (especially macOS), Python is available as python3 instead of python. Try running:

python3 --version

If this works, use python3 in place of python for the rest of this project.

Help me figure out which Python command works on my machine.

Create your project folder and file

Your server code needs a dedicated folder on your Desktop so you can find it easily later.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir http-server-from-scratch
cd http-server-from-scratch

What do these commands do?

  • mkdir http-server-from-scratch creates a new folder with that name.
  • cd http-server-from-scratch moves your terminal into the new folder so future commands run from there.
  • Open this folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the http-server-from-scratch folder, and click Open.

VS Code will reload with your project folder in the sidebar.

  • Create a new file by clicking File in the top menu bar.
  • Click New File.
  • Type server.py as the file name and press Enter to save it.
  • Confirm the file was created by running this in the terminal:
ls

You should see server.py in the output.

Don't see server.py in the terminal?

Your terminal might still be pointed at the Desktop instead of the project folder. Open a fresh terminal by clicking Terminal then New Terminal. VS Code automatically opens the new terminal inside the folder shown in the sidebar.

Help me get my terminal pointed at the right folder.

Test that Python runs your file

A quick test confirms everything is wired up correctly before you start writing server code.

  • In server.py, type the following line:
print("Server starting...")
  • Save the file by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Run the file by entering this command in the terminal:
python server.py

You should see Server starting... printed in the terminal.

Seeing a "No such file or directory" error?

This means your terminal is not in the same folder as server.py. Make sure the terminal prompt shows http-server-from-scratch as the current directory. If not, open a new terminal from Terminal > New Terminal to reset it.

Help me run my server.py file.

  • Now delete the print("Server starting...") line from server.py so the file is empty.
  • Save the file again with Cmd+S (macOS) or Ctrl+S (Windows).

Why delete the test line?

You only needed that line to confirm Python can find and run the file. Now that you know it works, you have a clean slate ready for the real server code.

Your environment is ready. Next up, you will open a raw TCP socket and hear the first connection knock on your server's door.

Open a TCP Socket and Listen for Connections

Your project folder and file are set up. Now it's time to write the code that makes Python listen for incoming network connections from a browser.

Every web server starts the same way: it opens a TCP socket, binds to an address and port, and waits. When a browser connects, the server reads the raw bytes of the HTTP request. In this step, you'll build that foundation from scratch.

In this step, get ready to:
  • Create a TCP socket and configure it to listen on port 8080.
  • Write a loop that accepts browser connections and prints their raw HTTP requests.
  • Test your server by visiting it in a browser.
Create and configure the socket

A socket is your program's endpoint for sending and receiving data over a network. You need to create one, tell it which address and port to use, and then start listening for incoming connections.

  • Add the socket setup code to server.py by copying this code:
import socket

# Server address and port
HOST = "127.0.0.1"
PORT = 8080

# Create a TCP socket for IPv4
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Allow reusing the port immediately after stopping the server
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Bind to the address and port
server_socket.bind((HOST, PORT))
# Start listening for incoming connections (queue up to 5)
server_socket.listen(5)
print(f"Server running on http://{HOST}:{PORT}")

What does this code do?

  • socket.AF_INET tells Python to use IPv4 networking. socket.SOCK_STREAM means TCP, which guarantees reliable, ordered delivery of data.
  • SO_REUSEADDR lets you restart the server immediately after stopping it. Without this, the port stays locked briefly and you would see an "Address already in use" error.
  • bind() attaches the socket to a specific IP and port. listen(5) tells the OS to start queuing up to 5 incoming connections while the server is busy.
  • Save server.py.
  • You won't see a change yet. The server needs a connection-handling loop before it can stay running and accept requests.
Accept a connection and read the raw HTTP request

When a browser connects to your server, the operating system hands you a new socket for that specific connection. You read the raw bytes from it, decode them into text, and that's the HTTP request.

  • Add the connection-handling loop below the code you just wrote in server.py:
while True:
    # Wait for a browser to connect
    client_socket, client_address = server_socket.accept()
    # Read up to 4096 bytes of the request and decode to text
    request_data = client_socket.recv(4096).decode()
    # Print the raw HTTP request to the terminal
    print(request_data)
    # Close the connection
    client_socket.close()

What does this code do?

  • accept() pauses the program and waits until a browser connects. It returns a new socket just for that one client, plus their address.
  • recv(4096) reads up to 4096 bytes from the client. .decode() converts the raw bytes into a readable string.
  • The while True loop repeats forever, handling one request at a time. Each client connection is closed after reading so the browser knows the server is done.
  • Save server.py.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on to testing.

ⓧ I'd like to double check the full code

import socket

HOST = "127.0.0.1"
PORT = 8080

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((HOST, PORT))
server_socket.listen(5)
print(f"Server running on http://{HOST}:{PORT}")

while True:
    client_socket, client_address = server_socket.accept()
    request_data = client_socket.recv(4096).decode()
    print(request_data)
    client_socket.close()
Test with your browser
  • Start your server by running this command in the terminal:
python server.py

You should see Server running on http://127.0.0.1:8080 printed in the terminal. The server is now waiting for a connection.

  • Open your browser and navigate to http://127.0.0.1:8080.
  • Switch back to your terminal in VS Code.

The browser will show an error page since your server is not sending any response yet. But in the terminal, you'll see the raw HTTP request the browser sent, including the request line and headers.

What should I see?

Your terminal will show something like this:

GET / HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 ...
Accept: text/html,...
...

This is the raw HTTP request your browser sent. The first line (GET / HTTP/1.1) is the request line. Everything below it is a header. Your server just read real network traffic.

  • Stop the server by pressing Ctrl+C in the terminal.

Server not starting?

  • If you see an "Address already in use" error, another process is using port 8080. Make sure you stopped any previous server run with Ctrl+C before starting again.
  • If you see python: command not found, try python3 server.py instead. Use whichever command worked in Step 1.
  • If you see a SyntaxError, double check that your code matches the full code in the tab above. Look for missing colons, parentheses, or indentation issues.

Still stuck? Help me debug my socket server that won't start on port 8080.

Your server is receiving real HTTP traffic from a browser. Next up, you'll parse that raw text into structured data and send a proper response back.

Parse Requests and Send Valid HTTP Responses

In the last step, you saw the raw HTTP request text that your browser sends. It is just a structured string following a specific format.

Now you will write two functions: one to parse that raw text into usable pieces (method, path, headers) and one to construct a valid HTTP response the browser can render. Then you will wire both into your main loop.

In this step, get ready to:
  • Write a function that extracts the method, path, and headers from a raw HTTP request.
  • Write a function that constructs a valid HTTP/1.1 response string.
  • Connect both functions to your main loop so the browser displays an HTML page.
Write the request parser function

Every HTTP request starts with a request line like GET / HTTP/1.1, followed by headers. Each line is separated by \r\n (a carriage return and newline). Your parser splits on this delimiter to extract the pieces your server needs.

  • In server.py, find the line PORT = 8080.
  • Add the following function below that line, leaving a blank line between the constant and the function:
def parse_request(request_text):
    # Split the raw request into individual lines
    lines = request_text.split("\r\n")
    # First line is always the request line (e.g. "GET / HTTP/1.1")
    request_line = lines[0]
    parts = request_line.split(" ")
    method = parts[0]
    path = parts[1]

    # Parse header lines into a dictionary
    headers = {}
    for line in lines[1:]:
        if line == "":
            break
        key, value = line.split(": ", 1)
        headers[key] = value

    return method, path, headers

What does this code do?

  • request_text.split("\r\n") breaks the raw HTTP text into a list of lines using the standard HTTP line ending.
  • The first line always contains the method (GET, POST), the path (/, /about), and the HTTP version. Splitting on spaces gives you each piece.
  • The remaining lines are headers in Key: Value format. The loop collects them into a dictionary until it hits an empty line (which signals the end of headers).
  • Save server.py.

You will not see a change yet. The next function builds the response your server sends back.

Seeing a syntax error?

  • Make sure the function starts at the top level of your file (no extra indentation before def).
  • Check that the for loop and if statement inside the function are indented consistently with four spaces.

Why is my parse_request function showing an IndentationError?

Write the response builder function

HTTP responses follow the same structured format: a status line, headers, a blank line, then the body. Your builder assembles these pieces into a single string the browser can interpret.

  • In server.py, add the following function directly below parse_request, leaving a blank line between the two functions:
def build_response(status_code, status_text, content_type, body):
    # Status line tells the browser if the request succeeded
    response_line = f"HTTP/1.1 {status_code} {status_text}\r\n"
    # Headers tell the browser the content type, size, and connection behavior
    headers = f"Content-Type: {content_type}\r\nContent-Length: {len(body.encode())}\r\nConnection: close\r\n"
    # Blank line (\r\n) separates headers from body
    return f"{response_line}{headers}\r\n{body}"

What does this code do?

  • The status line (HTTP/1.1 200 OK) tells the browser whether the request succeeded or failed.
  • Content-Type tells the browser how to interpret the body (text/html for web pages, application/json for API data).
  • Content-Length tells the browser exactly how many bytes to expect. Using body.encode() ensures the count is accurate for any characters.
  • Connection: close tells the browser not to keep the connection open after the response. This simplifies your server.
  • Save server.py.

You will not see a change yet. The next substep connects both functions to your main loop.

Red underlines in your editor?

  • Make sure the f prefix is present before each string that uses {curly braces}. Without it, Python treats the braces as literal text.
  • Check that backslashes in \r\n are not doubled up. You need exactly \r\n inside your f-string.

Why is my build_response function not formatting the string correctly?

Send a response back to the browser

Both functions are ready. Now you will update the main loop to parse incoming requests, build an HTML response, and send it back to the browser.

  • In server.py, find the while True loop at the bottom of your file. It currently looks like this:
while True:
    client_socket, client_address = server_socket.accept()
    request_data = client_socket.recv(4096).decode()
    print(request_data)
    client_socket.close()
  • Replace the entire while True loop with this updated version:
while True:
    client_socket, client_address = server_socket.accept()
    request_data = client_socket.recv(4096).decode()
    # Only process if we received data
    if request_data:
        method, path, headers = parse_request(request_data)
        print(f"{method} {path}")
        # Build an HTML response and send it back
        response = build_response(200, "OK", "text/html", "<h1>Hello from my HTTP server!</h1>")
        client_socket.sendall(response.encode())
    client_socket.close()

What changed here?

  • if request_data: guards against empty requests (which can happen if a browser connects but sends nothing).
  • parse_request(request_data) extracts the method, path, and headers so your server knows what the browser asked for.
  • print(f"{method} {path}") logs each request to your terminal in a clean format instead of dumping the raw text.
  • build_response(200, "OK", "text/html", ...) constructs a proper HTTP response with a 200 status, and sendall transmits the full response bytes to the browser.
  • Save server.py.
  • Run your server in the terminal:
python server.py
  • Open your browser and go to http://127.0.0.1:8080.
  • You should see a large heading that says Hello from my HTTP server! rendered in the browser.
  • Check your terminal. You should see GET / logged for each request.
  • Stop the server with Ctrl+C when you are done testing.

Browser still shows an error?

  • If you see Connection refused, make sure the server is running in your terminal (you should see the "Server running" message).
  • If the page is blank, check that client_socket.sendall(response.encode()) is inside the if request_data: block (indented under it).
  • If you get Address already in use, your previous server process is still running. Press Ctrl+C in that terminal first, then try again.

My browser is not showing my HTML page when I visit 127.0.0.1:8080. Help me debug.

✔️ Awesome, I've got everything!

Great. Double check you have saved server.py and that visiting http://127.0.0.1:8080 renders your HTML greeting.

ⓧ I'd like to double check the full code

Here is the complete server.py at this point. Compare your file against it:

import socket

HOST = "127.0.0.1"
PORT = 8080


def parse_request(request_text):
    lines = request_text.split("\r\n")
    request_line = lines[0]
    parts = request_line.split(" ")
    method = parts[0]
    path = parts[1]

    headers = {}
    for line in lines[1:]:
        if line == "":
            break
        key, value = line.split(": ", 1)
        headers[key] = value

    return method, path, headers


def build_response(status_code, status_text, content_type, body):
    response_line = f"HTTP/1.1 {status_code} {status_text}\r\n"
    headers = f"Content-Type: {content_type}\r\nContent-Length: {len(body.encode())}\r\nConnection: close\r\n"
    return f"{response_line}{headers}\r\n{body}"


server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((HOST, PORT))
server_socket.listen(5)
print(f"Server running on http://{HOST}:{PORT}")

while True:
    client_socket, client_address = server_socket.accept()
    request_data = client_socket.recv(4096).decode()
    if request_data:
        method, path, headers = parse_request(request_data)
        print(f"{method} {path}")
        response = build_response(200, "OK", "text/html", "<h1>Hello from my HTTP server!</h1>")
        client_socket.sendall(response.encode())
    client_socket.close()

Your server now speaks valid HTTP. It parses what the browser asks for and replies with a real web page. Next up, you will build a routing system so different URLs return different pages.

Build a Router with Multiple Pages

Your server can now parse incoming requests and send back a valid HTTP response. The problem is it always returns the same page no matter what URL the browser requests.

Real web servers map different URL paths to different pages. In this step, you will build a routing system that looks up the requested path in a dictionary and calls the matching handler function. This is the exact pattern that frameworks like Django and FastAPI use internally.

In this step, get ready to:
  • Create handler functions that return HTML pages for home, about, and 404.
  • Build a routes dictionary that maps URL paths to handler functions.
  • Test navigation between multiple pages in your browser.
Create handler functions for each page

Each page your server can display needs its own function. These functions take the request details and return a complete HTTP response with HTML content.

  • In server.py, add the handle_home function below the build_response function by copying this code:
def handle_home(method, headers):
    body = """<!DOCTYPE html>
<html>
<head><title>My HTTP Server</title></head>
<body>
<h1>Hello from my HTTP server!</h1>
<p>This server was built from scratch using Python sockets.</p>
<p>Try these endpoints:</p>
<ul>
<li><a href="/about">/about</a></li>
<li><a href="/api/time">/api/time</a></li>
<li><a href="/api/info">/api/info</a></li>
</ul>
</body>
</html>"""
    return build_response(200, "OK", "text/html", body)

What does this code do?

The handle_home function builds a full HTML page stored in the body variable. The page includes links to /about, /api/time, and /api/info so visitors can navigate your server.

It passes the HTML body to build_response with a 200 status code, which wraps it in proper HTTP headers. The function accepts method and headers parameters so the router can pass request details to any handler.

You won't see a change yet. The routing system in the next substep will connect this handler to incoming requests.

  • Below handle_home, add the handle_about function:
def handle_about(method, headers):
    body = """<!DOCTYPE html>
<html>
<head><title>About</title></head>
<body>
<h1>About This Server</h1>
<p>Built with Python's socket module. No frameworks!</p>
<p><a href="/">Back to home</a></p>
</body>
</html>"""
    return build_response(200, "OK", "text/html", body)

What does this code do?

This follows the same pattern as handle_home. It returns a 200 response with a different HTML body. The page includes a link back to / so visitors can navigate back to the home page.

  • Below handle_about, add the handle_not_found function:
def handle_not_found(method, headers):
    body = """<!DOCTYPE html>
<html>
<head><title>404 Not Found</title></head>
<body>
<h1>404 - Page Not Found</h1>
<p>The page you requested does not exist.</p>
<p><a href="/">Back to home</a></p>
</body>
</html>"""
    return build_response(404, "Not Found", "text/html", body)

What does this code do?

This handler returns a 404 status code instead of 200. The 404 code tells the browser that the requested page does not exist. The HTML body gives visitors a friendly message and a link back to home.

This is your fallback handler. Any URL path that does not match a registered route will trigger this function.

Build the routing dictionary and update the main loop

A routing dictionary maps URL path strings to handler functions. When a request arrives, you look up the path in the dictionary and call the matching function. This is the core of how every web framework dispatches requests.

  • In server.py, find the old main loop code that starts with this line:
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  • Select everything from that line down to the end of the file and delete it.
  • Replace it with the following code that defines the routes dictionary and a start_server function:
routes = {
    "/": handle_home,
    "/about": handle_about,
}


def start_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind((HOST, PORT))
    server_socket.listen(5)
    print(f"Server running on http://{HOST}:{PORT}")
    print("Press Ctrl+C to stop the server")

    try:
        while True:
            client_socket, client_address = server_socket.accept()
            print(f"Connection from {client_address}")

            request_data = client_socket.recv(4096).decode()
            if request_data:
                method, path, headers = parse_request(request_data)
                print(f"{method} {path}")

                # Look up the path in the routes dictionary
                handler = routes.get(path, handle_not_found)
                response = handler(method, headers)

                client_socket.sendall(response.encode())

            client_socket.close()
    except KeyboardInterrupt:
        print("\nServer stopped.")
    finally:
        server_socket.close()


if __name__ == "__main__":
    start_server()

What does this code do?

  • The routes dictionary maps path strings to handler functions. Each key is a URL path, each value is the function to call for that path.
  • The line routes.get(path, handle_not_found) looks up the requested path. If the path exists in the dictionary, it returns the matching handler. If not, it returns handle_not_found as the default.
  • The try/except KeyboardInterrupt block lets you stop the server cleanly with Ctrl+C instead of seeing an ugly traceback.
  • The if __name__ == "__main__" guard ensures the server only starts when you run the file directly.

Why use a dictionary for routing?

A dictionary lookup is the simplest way to map paths to functions. You could use a long chain of if/elif statements instead, but that gets messy fast. Frameworks like Django and Flask use this same dictionary-based pattern internally.

Adding a new page is just one line in the routes dictionary. No other code needs to change.

  • Save server.py.

Seeing a syntax error?

Make sure you deleted the entire old main loop (from server_socket = socket.socket(...) all the way to the end of the file). Leftover lines from the old code will conflict with the new start_server function.

Also check that the routes dictionary is placed after all three handler functions. Python reads top to bottom, so handle_home and handle_about must be defined before the dictionary references them.

help me fix a syntax error in my HTTP server routing code

Test navigation between pages
  • Run your server by entering this command in the terminal:
python server.py
  • Open your browser and go to http://127.0.0.1:8080.
  • You should see your home page with the heading "Hello from my HTTP server!" and a list of links.
  • Click the /about link. You should see the About page with a "Back to home" link.
  • Try visiting a path that does not exist, like http://127.0.0.1:8080/xyz. You should see your 404 page.
  • Check your terminal. You should see each request logged with its method and path.
  • Stop the server with Ctrl+C.

Don't see different pages for different paths?

Make sure the routes dictionary keys match the exact paths including the leading slash. For example, "/about" with a forward slash is not the same as "about" without one.

If you see the old "Hello from my HTTP server!" heading with no links, the old main loop code is still present. Delete everything from server_socket = socket.socket(...) to the end of the file and paste the new start_server code.

help me debug my HTTP server routing

✔️ Awesome, I've got everything!

Great. Double check you have saved server.py and that all three pages work in your browser.

ⓧ I'd like to double check the full code

Here is the complete server.py file at this point:

import socket

HOST = "127.0.0.1"
PORT = 8080


def parse_request(request_text):
    lines = request_text.split("\r\n")
    request_line = lines[0]
    parts = request_line.split(" ")
    method = parts[0]
    path = parts[1]

    headers = {}
    for line in lines[1:]:
        if line == "":
            break
        key, value = line.split(": ", 1)
        headers[key] = value

    return method, path, headers


def build_response(status_code, status_text, content_type, body):
    response_line = f"HTTP/1.1 {status_code} {status_text}\r\n"
    headers = f"Content-Type: {content_type}\r\nContent-Length: {len(body.encode())}\r\nConnection: close\r\n"
    return f"{response_line}{headers}\r\n{body}"


def handle_home(method, headers):
    body = """<!DOCTYPE html>
<html>
<head><title>My HTTP Server</title></head>
<body>
<h1>Hello from my HTTP server!</h1>
<p>This server was built from scratch using Python sockets.</p>
<p>Try these endpoints:</p>
<ul>
<li><a href="/about">/about</a></li>
<li><a href="/api/time">/api/time</a></li>
<li><a href="/api/info">/api/info</a></li>
</ul>
</body>
</html>"""
    return build_response(200, "OK", "text/html", body)


def handle_about(method, headers):
    body = """<!DOCTYPE html>
<html>
<head><title>About</title></head>
<body>
<h1>About This Server</h1>
<p>Built with Python's socket module. No frameworks!</p>
<p><a href="/">Back to home</a></p>
</body>
</html>"""
    return build_response(200, "OK", "text/html", body)


def handle_not_found(method, headers):
    body = """<!DOCTYPE html>
<html>
<head><title>404 Not Found</title></head>
<body>
<h1>404 - Page Not Found</h1>
<p>The page you requested does not exist.</p>
<p><a href="/">Back to home</a></p>
</body>
</html>"""
    return build_response(404, "Not Found", "text/html", body)


routes = {
    "/": handle_home,
    "/about": handle_about,
}


def start_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind((HOST, PORT))
    server_socket.listen(5)
    print(f"Server running on http://{HOST}:{PORT}")
    print("Press Ctrl+C to stop the server")

    try:
        while True:
            client_socket, client_address = server_socket.accept()
            print(f"Connection from {client_address}")

            request_data = client_socket.recv(4096).decode()
            if request_data:
                method, path, headers = parse_request(request_data)
                print(f"{method} {path}")

                handler = routes.get(path, handle_not_found)
                response = handler(method, headers)

                client_socket.sendall(response.encode())

            client_socket.close()
    except KeyboardInterrupt:
        print("\nServer stopped.")
    finally:
        server_socket.close()


if __name__ == "__main__":
    start_server()

Your server now serves different pages based on the URL path. Next up, you will add JSON API endpoints that return dynamic data instead of static HTML.

Serve Dynamic JSON API Endpoints

Your server can route browsers to different HTML pages. That alone makes it a web server. But modern servers do something else too: they return structured data that other programs can read.

In this step, you will add JSON API endpoints to your server. These endpoints return data instead of HTML pages. Every time a browser or program hits your /api/time path, it gets the current time as machine-readable JSON.

In this step, get ready to:
  • Write two JSON API handler functions that return dynamic data.
  • Register the new API routes in your routing dictionary.
  • Test that your endpoints return different JSON on each request.
Add the time API handler

Your first API endpoint will return the current time. Unlike your HTML pages which always serve the same content, this handler generates a fresh response every time it is called.

  • In server.py, find the import socket line at the very top of the file.
  • Add two new import lines directly below it so your imports look like this:
import socket
import json
from datetime import datetime
  • Now scroll down to below the handle_about function.
  • Add the handle_api_time function by copying this code below handle_about:
def handle_api_time(method, headers):
    # Get the current time as an ISO format string
    now = datetime.now().isoformat()
    # Build a dictionary with the time data
    data = {"current_time": now, "message": "Time served from raw sockets!"}
    # Convert the dictionary to a JSON string
    body = json.dumps(data, indent=2)
    return build_response(200, "OK", "application/json", body)

What does this code do?

This handler works differently from your HTML handlers. Instead of returning a static HTML string, it generates fresh data on every call.

  • datetime.now().isoformat() captures the current time as a standardized string.
  • json.dumps(data, indent=2) converts a Python dictionary into a JSON string, with nice formatting.
  • The content type is application/json instead of text/html. This tells the browser the response is data, not a web page.
  • Save the file. You will not test yet because the route is not registered.
Add the server info API handler

Your second API endpoint returns metadata about the server itself. This pattern is common in real APIs where a /info or /health endpoint tells clients what the server can do.

  • Add the handle_api_info function directly below handle_api_time:
def handle_api_info(method, headers):
    # Server metadata as a Python dictionary
    data = {
        "server": "MyHTTPServer/1.0",
        "python_sockets": True,
        "endpoints": ["/", "/about", "/api/time", "/api/info"],
    }
    # Serialize to JSON and return with the correct content type
    body = json.dumps(data, indent=2)
    return build_response(200, "OK", "application/json", body)

What does this code do?

This handler returns a fixed dictionary describing your server. The endpoints list tells any client which paths are available.

Notice how Python's True becomes JSON's true automatically. The json.dumps() function handles the translation between Python types and JSON types.

  • Save the file. The handlers exist but your server still does not know when to call them.
Register the new routes and test

Your routing dictionary is the single place that connects URL paths to handler functions. Adding a new endpoint is as simple as adding one line to this dictionary.

  • In server.py, scroll down to your routes dictionary.
  • Find this code:
routes = {
    "/": handle_home,
    "/about": handle_about,
}
  • Replace it with the updated routes dictionary that includes your two new API handlers:
routes = {
    "/": handle_home,
    "/about": handle_about,
    "/api/time": handle_api_time,
    "/api/info": handle_api_info,
}

Why is this so simple?

This is the power of the routing pattern you built in the previous step. Adding a new endpoint takes one line. Your main loop already handles the lookup and dispatch. This is the exact same pattern that frameworks like Django and FastAPI use internally.

  • Save server.py.
  • Run your server by entering this in the terminal:
python server.py
  • Open your browser and visit http://127.0.0.1:8080/api/time.
  • You should see a JSON response with the current time displayed in your browser.
  • Refresh the page. The time value changes on each refresh because the handler generates a new response every time.
  • Now visit http://127.0.0.1:8080/api/info. You should see the server metadata JSON with the server name and list of endpoints.
  • Try visiting http://127.0.0.1:8080 and clicking the API links from your home page. Everything connects through your routing dictionary.
  • Stop the server with Ctrl+C when you are done testing.

Seeing a KeyError or server crash?

  • Make sure your handle_api_time and handle_api_info functions are defined ABOVE the routes dictionary. Python reads top to bottom, so the functions must exist before you reference them.
  • Check that you added import json and from datetime import datetime at the top of the file.
  • If you see Address already in use, your previous server run may still be holding the port. Wait a few seconds and try again.

Why isn't my JSON API endpoint working?

✔️ Awesome, I've got everything!

Great. Double check you have saved server.py.

ⓧ I'd like to double check the full code

import socket
import json
from datetime import datetime


HOST = "127.0.0.1"
PORT = 8080


def parse_request(request_text):
    lines = request_text.split("\r\n")
    request_line = lines[0]
    parts = request_line.split(" ")
    method = parts[0]
    path = parts[1]

    headers = {}
    for line in lines[1:]:
        if line == "":
            break
        key, value = line.split(": ", 1)
        headers[key] = value

    return method, path, headers


def build_response(status_code, status_text, content_type, body):
    response_line = f"HTTP/1.1 {status_code} {status_text}\r\n"
    headers = f"Content-Type: {content_type}\r\nContent-Length: {len(body.encode())}\r\nConnection: close\r\n"
    return f"{response_line}{headers}\r\n{body}"


def handle_home(method, headers):
    body = """<!DOCTYPE html>
<html>
<head><title>My HTTP Server</title></head>
<body>
<h1>Hello from my HTTP server!</h1>
<p>This server was built from scratch using Python sockets.</p>
<p>Try these endpoints:</p>
<ul>
<li><a href="/about">/about</a></li>
<li><a href="/api/time">/api/time</a></li>
<li><a href="/api/info">/api/info</a></li>
</ul>
</body>
</html>"""
    return build_response(200, "OK", "text/html", body)


def handle_about(method, headers):
    body = """<!DOCTYPE html>
<html>
<head><title>About</title></head>
<body>
<h1>About This Server</h1>
<p>Built with Python's socket module. No frameworks!</p>
<p><a href="/">Back to home</a></p>
</body>
</html>"""
    return build_response(200, "OK", "text/html", body)


def handle_api_time(method, headers):
    now = datetime.now().isoformat()
    data = {"current_time": now, "message": "Time served from raw sockets!"}
    body = json.dumps(data, indent=2)
    return build_response(200, "OK", "application/json", body)


def handle_api_info(method, headers):
    data = {
        "server": "MyHTTPServer/1.0",
        "python_sockets": True,
        "endpoints": ["/", "/about", "/api/time", "/api/info"],
    }
    body = json.dumps(data, indent=2)
    return build_response(200, "OK", "application/json", body)


def handle_not_found(method, headers):
    body = """<!DOCTYPE html>
<html>
<head><title>404 Not Found</title></head>
<body>
<h1>404 - Page Not Found</h1>
<p>The page you requested does not exist.</p>
<p><a href="/">Back to home</a></p>
</body>
</html>"""
    return build_response(404, "Not Found", "text/html", body)


routes = {
    "/": handle_home,
    "/about": handle_about,
    "/api/time": handle_api_time,
    "/api/info": handle_api_info,
}


def start_server():
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind((HOST, PORT))
    server_socket.listen(5)
    print(f"Server running on http://{HOST}:{PORT}")
    print("Press Ctrl+C to stop the server")

    try:
        while True:
            client_socket, client_address = server_socket.accept()
            print(f"Connection from {client_address}")

            request_data = client_socket.recv(4096).decode()
            if request_data:
                method, path, headers = parse_request(request_data)
                print(f"{method} {path}")

                handler = routes.get(path, handle_not_found)
                response = handler(method, headers)

                client_socket.sendall(response.encode())

            client_socket.close()
    except KeyboardInterrupt:
        print("\nServer stopped.")
    finally:
        server_socket.close()


if __name__ == "__main__":
    start_server()

Secret mission

Handle POST Requests

Your server can respond to GET requests, but real APIs also receive data from clients. In this secret mission, you will add POST request handling so your server can accept JSON data, parse it, and echo it back with a confirmation response.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally using only Python's standard library, so there are no ongoing costs or cloud resources to worry about.

Decide whether to keep your project folder for reference or delete it entirely.

Resources you used:

  • The http-server-from-scratch folder on your Desktop (contains server.py).

Keep everything running

No action needed. Choose this if you want to keep your server code for reference or plan to extend it later.

  • Your project folder stays on your Desktop. You can run the server anytime with python server.py.
  • There are no background processes or services running. The server only runs when you start it manually.

Pause - I'll come back to this later

Since this project uses no cloud services and no background processes, there is nothing to pause. Your project folder just sits on disk until you need it again.

  • If your server is still running in the terminal, press Ctrl+C to stop it.
  • That's it. No other cleanup needed.

Delete - I don't want to use this again

Remove all project files from your machine.

  • If your server is still running in the terminal, press Ctrl+C to stop it.
  • Delete the project folder by running this command in your terminal:

macOS

cd ~/Desktop
rm -rf http-server-from-scratch

Windows

cd ~/Desktop
Remove-Item -Recurse -Force http-server-from-scratch
  • Confirm the folder is gone by running:
ls

You should no longer see http-server-from-scratch in the output.

Nice Work!

Nice Work!

You just built a fully functional HTTP server from nothing but Python's socket module. Every web framework you'll ever use sits on top of this exact foundation.

You've learned how to:

  • Build a TCP server from raw sockets that accepts browser connections and parses HTTP/1.1 requests by hand.
  • Implement a routing system that maps URL paths to Python handler functions, the same pattern Django and FastAPI use internally.
  • Serve dynamic JSON API responses with correct status codes, headers, and bodies using only Python's standard library.
  • Secret Mission: Handled POST requests with JSON body parsing and error handling for malformed input.

Ready to quiz yourself?