Build Your Own Python Web Framework From Scratch!

Welcome Back!
If you read my last blog, which was a kind of precursor to this one, it was about building your own HTTP file server from scratch. Now, as part 2, like I mentioned in that blog, as I built on that, I realized it was turning into a mini Python web framework. That got me thinking, why not build it since I'm already halfway there? And that's how this came to be. So, without wasting any more time, let's get into it!
In the previous parts of this blog series, we explored how to build a basic [[HTTP Server (Python)]] and a [[Basic HTTP File Server]]. Now, letβs take things up a notch β we're creating a mini web framework, inspired by how frameworks like Flask or Django work internally (but much simpler).
We split the logic into configurable and modular files:
π File Structure
mini-web-framework/
β
βββ main.py
βββ routes.py
βββ router.py
βββ utils.py
βββ config.py
βββ templates/
βββ home.html
config.py:
import os
HOST = "192.168.0.124"
PORT = 9999
FILE_DIR = os.path.join(os.path.dirname(__file__), "files")
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
HOST
andPORT
define where your server will run.FILE_DIR
points to the folder where user-uploaded or downloadable files live.TEMPLATE_DIR
points to where your HTML templates live.
Keeping this separate makes your code easier to maintain and portable.
utils.py:
import os
from config import TEMPLATE_DIR
def render_template(filename):
filepath = os.path.join(TEMPLATE_DIR, filename)
if not os.path.isfile(filepath):
return "<h1>Template Not Found</h1>"
with open(filepath, "r", encoding="utf-8") as f:
return f.read()
First, we import
TEMPLATE_DIR
fromconfig
.Next, we create a function,
render_template()
, where we build the file path usingos.path.join()
with the filename from the template directory.Then, we check if the file actually exists:
If it's not there, we return "Template Not Found".
Otherwise, we return the contents using
f.read()
.
router.py:
ROUTES = {}
def route(path):
def decorator(func):
ROUTES[path] = func
return func
return decorator
To understand this, we first need to learn about Python Decorators. You can find more information here: What are Python Decorators. Once we understand that, we can see that we create an empty dictionary called ROUTES
, and then a decorator factory. This factory contains the actual decorator, which doesn't wrap or modify behavior like typical decorators. Instead, it registers the function. When we call the outer decorator, it returns the inner function, which can then be used to pass parameters to the inner decorator function. This essentially adds the parameter to ROUTES[path]
.
routes.py:
from router import route
from utils import render_template
import os
from config import FILE_DIR
@route("/")
def homepage(request):
return 200, "text/html", "<h1>Hello World</h1>"
@route("/files/")
def list_files(request):
try:
files = os.listdir(FILE_DIR)
html = "<h1>Files in server</h1><ul>"
for f in files:
html += f"<li><a href='/files/" + f + "'>" + f + "</a></li>"
html += "</ul>"
return 200, "text/html", html
except FileNotFoundError:
return 500, "text/plain", "Directory not found"
@route("/home")
def home_page(request):
html = render_template("home.html")
return 200, "text/html", html
Here we import:
route
fromrouter.py
, which is our custom decorator for mapping paths (URLs) to functions.render_template
fromutils.py
, which is a helper function used to load HTML files from thetemplates/
folder.os
, used to list the contents of the file directory.FILE_DIR
fromconfig.py
, which gives the absolute path to the folder that contains downloadable files.
We use the
@route()
decorator, passing'/'
as the route.This function is then called whenever the root URL (
/
) is accessed.It returns a status code
200
, content typetext/html
, and a basic HTML message.
Route: /files/
@route("/files/")
def list_files(request):
try:
files = os.listdir(FILE_DIR)
html = "<h1>Files in server</h1><ul>"
for f in files:
html += f"<li><a href='/files/" + f + "'>" + f + "</a></li>"
html += "</ul>"
return 200, "text/html", html
except FileNotFoundError:
return 500, "text/plain", "Directory not found"
This route handles listing all files present in the serverβs
files/
directory.os.listdir(FILE_DIR)
grabs all the filenames as a list.We construct an HTML response dynamically using a loop, which creates a list of clickable links for each file.
These links point to URLs like
/files/filename.txt
, which (in your main logic) allow users to download them.If the directory does not exist, it returns a
500 Internal Server Error
with a plain-text message.
Route: /home
@route("/home")
def home_page(request):
html = render_template("home.html")
return 200, "text/html", html
This route returns a static HTML page from the
templates/
directory.The
render_template("home.html")
function reads the contents oftemplates/home.html
and returns it as a string.This is then returned with a status
200
and content typetext/html
, making it work just like a real webpage.You can create multiple such pages and just call them using this function pattern.
main.py:
from http.server import HTTPServer, BaseHTTPRequestHandler
from router import ROUTES
import time
import routes # Register all routes
from config import HOST, PORT
class SidhHTTP(BaseHTTPRequestHandler):
def do_GET(self):
if self.path in ROUTES:
status, content_type, body = ROUTES[self.path](self)
self.send_response(status)
self.send_header("Content-type", content_type)
self.end_headers()
if isinstance(body, str):
self.wfile.write(body.encode())
else:
self.wfile.write(body)
else:
self.send_error(404, "Page Not Found")
def do_POST(self):
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
date = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
self.wfile.write(bytes(f'{{"time": "{date}"}}', "utf-8"))
server = HTTPServer((HOST, PORT), SidhHTTP)
print("server running...")
server.serve_forever()
server.server_close()
print("server closed...")
The beginning is the same as other projects, but the difference appears when we reach the do_GET()
function. Here, we first check if the path is in ROUTES
. You might wonder how ROUTES
is not empty. When we import routes
at the top, the routes.py file is initialized, and the paths are added automatically. If the path is in ROUTES
, we proceed as usual by sending the response, header, and ending the header. Then, we have an if
statement that checks if the instance is binary or just HTML and handles it accordingly.
The POST part and the rest remain unchanged from the previous version(from the last HTTP server blog).
Templates:
A simple HTML page that could look like this:(for home.html)
<!DOCTYPE html>
<html>
<head>
<title>Welcome</title>
</head>
<body>
<h1>Welcome to My Python Mini Web Framework</h1>
<p>This page is rendered from an HTML file in the templates folder.</p>
</body>
</html>
And just like that, we have completed our mini Python framework.
You should now have a good understanding of how to run this on your own. However, if you need the source code or detailed instructions, you can check out my GitHub with a helpful README: https://github.com/sidh-coder/mini-web-framework
Subscribe to my newsletter
Read articles from Sidharth Sunu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
