In this project, you will build a small Python web framework.
A web framework is the layer that turns incoming HTTP requests into structured application behavior.
It lets a developer define routes, receive request objects, return response objects, render templates, serve static files, handle errors, and compose reusable middleware.
This project will build a framework that feels familiar after using Flask, Django, FastAPI, Starlette, Bottle, or Sanic, but is small enough to understand from first principles.
The final project will support:
route registration
path parameters
HTTP methods
request objects
response objects
JSON responses
HTML responses
redirects
middleware
error handlers
static files
simple templates
a development server
tests for routing, requests, responses, middleware, and errors
This project is not a production replacement for Flask, Django, FastAPI, Starlette, or any mature framework.
It is a learning implementation.
The goal is to understand the moving pieces hidden behind a simple decorator like:
@app.get("/users/{user_id}")
def get_user(request, user_id):
return {"id": user_id}By the end, the reader should understand how HTTP requests become Python objects, how routes match paths, how handlers produce responses, how middleware wraps application behavior, and why framework design is mostly about clean boundaries.
Many Python developers first meet web development through a framework.
They write:
from flask import Flask
app = Flask(__name__)
@app.get("/")
def home():
return "Hello"Then they run a server and open a browser.
That is a wonderful experience.
But it can also hide the important parts.
What is a route?
What is a request?
What does a handler return?
Who decides status codes?
Where do headers come from?
How does middleware run before and after a route?
How does the framework turn a dictionary into JSON?
How does it return a 404?
This capstone answers those questions by making the framework visible.
The reader will build the machinery rather than only use it.
That makes real frameworks easier to understand and debug.
A web framework receives an HTTP request.
It finds the matching route.
It calls the route handler.
It converts the result into an HTTP response.
The central flow is:
HTTP request
-> server adapter
-> Request object
-> middleware chain
-> router
-> handler function
-> Response object
-> server adapter
-> HTTP response
The framework itself is not necessarily the web server.
That distinction matters.
A web server speaks sockets and HTTP.
A framework organizes application logic.
In this capstone, you can use Python's standard library HTTP server for the low-level server.
That lets the project focus on the framework layer.
The earlier Mini Redis project already taught raw TCP.
This project should teach HTTP application design.
You will build a package named miniweb.
Suggested structure:
miniweb/
__init__.py
app.py
routing.py
request.py
response.py
middleware.py
templates.py
static.py
server.py
exceptions.py
tests/
test_routing.py
test_request.py
test_response.py
test_app.py
test_middleware.py
test_errors.py
The user-facing API should be pleasant:
from miniweb import App, JSONResponse
app = App()
@app.get("/")
def home(request):
return "Hello from Mini Web"
@app.get("/users/{user_id}")
def get_user(request, user_id):
return {"id": user_id}
@app.post("/users")
def create_user(request):
data = request.json()
return JSONResponse(data, status=201)
app.run(host="127.0.0.1", port=8000)This API looks simple.
The project will show everything required to make it work.
The mini framework must:
register routes with decorators
support GET and POST at minimum
match exact paths
match parameterized paths like /users/{user_id}
extract path parameters
represent incoming requests with a Request object
represent outgoing responses with a Response object
convert strings to HTML/text responses
convert dictionaries and lists to JSON responses
support explicit Response subclasses
return 404 for unmatched routes
return 405 for unsupported methods on matched paths
support middleware
support custom error handlers
serve static files from a configured directory
render simple templates
run using a development server
have tests for routing, response conversion, middleware, and errors
The framework should be synchronous.
Async support belongs later, after the Mini Event Loop capstone.
The framework should avoid hidden magic.
Every major behavior should be readable in the code.
The framework does not need production-grade security.
It does not need HTTP/2.
It does not need WebSockets.
It does not need ASGI concurrency.
It does not need database integration.
It does not need authentication.
It does not need sessions.
It does not need CSRF protection.
It does not need form validation.
It does not need streaming responses.
It does not need a full template language.
It does not need automatic OpenAPI generation.
These features are important in real frameworks.
They are intentionally outside the first implementation.
A small framework is useful because the reader can understand all of it.
HTTP is a request-response protocol.
A client sends a request:
GET /users/42 HTTP/1.1
Host: example.com
Accept: application/json
The server sends a response:
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 42}
A request has:
method
path
query string
headers
body
client address
A response has:
status code
headers
body
The framework's job is to make those pieces convenient in Python.
It should not erase the HTTP model.
Good frameworks make HTTP easier without pretending it is not there.
The App object is the center of the framework.
It should own:
router
middleware list
error handlers
static file configuration
template renderer
development server entry point
The simplest shape is:
class App:
def __init__(self):
self.router = Router()
self.middleware = []
self.error_handlers = {}
def route(self, path, methods):
def decorator(func):
self.router.add_route(path, methods, func)
return func
return decorator
def get(self, path):
return self.route(path, ["GET"])
def post(self, path):
return self.route(path, ["POST"])The route decorator should return the original function.
That preserves normal Python behavior.
The function can still be called directly in tests.
Route registration records a rule.
For example:
@app.get("/users/{user_id}")
def get_user(request, user_id):
...The framework stores:
path pattern: /users/{user_id}
methods: GET
handler: get_user
It does not call the handler during registration.
Registration happens at import time.
Execution happens later when a request arrives.
This distinction is important.
A decorator is often used to record metadata.
Here the metadata is:
which function handles which HTTP method and path
The router finds the right handler for a request.
It receives:
method
path
It returns:
matched handler
path parameters
allowed methods
There are two kinds of route matching.
The first is exact matching:
/about
/health
/login
The second is parameterized matching:
/users/{user_id}
/posts/{post_id}/comments/{comment_id}
Parameterized routes turn path segments into variables.
For example:
/users/42
matches:
/users/{user_id}
with:
user_id = "42"
Keep path parameters as strings at first.
Type conversion can be an extension.
A route pattern can be compiled into a regular expression.
Pattern:
/users/{user_id}
Regex:
^/users/(?P<user_id>[^/]+)$
Python code:
import re
def compile_path(pattern):
parts = pattern.strip("/").split("/")
regex_parts = []
param_names = []
for part in parts:
if part.startswith("{") and part.endswith("}"):
name = part[1:-1]
param_names.append(name)
regex_parts.append(f"(?P<{name}>[^/]+)")
else:
regex_parts.append(re.escape(part))
regex = "^/" + "/".join(regex_parts) + "$"
return re.compile(regex), param_namesThis is not a complete routing engine.
It is enough to teach the principle.
The regex uses named groups.
Named groups turn matched path values into a dictionary.
match.groupdict()That dictionary can be passed to the handler.
The framework should decide what happens when two routes conflict.
For example:
@app.get("/users/{id}")
def by_id(...):
...
@app.get("/users/me")
def me(...):
...If parameterized routes are checked first, /users/me may match {id}.
A simple policy is:
routes are checked in registration order
That is easy to explain.
The developer can register more specific routes first.
A more sophisticated framework may sort routes by specificity.
For this capstone, registration order is fine.
Document it.
The router should distinguish two cases.
Case one:
no route path matches
Return 404 Not Found.
Case two:
path matches, but method does not
Return 405 Method Not Allowed.
This distinction matters.
If the app has:
@app.get("/users")
def list_users(request):
...Then:
POST /users
should not be 404.
The path exists.
The method is not allowed.
The response should include an Allow header:
Allow: GET
This teaches that routing is not only path matching.
HTTP method matters.
The Request object wraps incoming HTTP data.
It should expose:
method
path
query_params
headers
body
client
A simple dataclass works:
from dataclasses import dataclass
@dataclass
class Request:
method: str
path: str
query_params: dict[str, list[str]]
headers: dict[str, str]
body: bytes
client: tuple[str, int] | None = NoneAdd helpers:
def text(self, encoding="utf-8") -> str:
return self.body.decode(encoding)
def json(self):
return json.loads(self.text())Do not parse JSON automatically for every request.
Only parse it when the handler asks.
That keeps the request object predictable.
The URL:
/search?q=python&page=2&tag=web&tag=framework
has:
path: /search
query string: q=python&page=2&tag=web&tag=framework
Use urllib.parse.
from urllib.parse import urlsplit, parse_qs
parts = urlsplit(raw_path)
path = parts.path
query_params = parse_qs(parts.query)parse_qs returns lists because a parameter can appear multiple times.
{"tag": ["web", "framework"]}That is more accurate than keeping only one value.
The framework can later add convenience helpers like:
request.query("page", default="1")But the underlying data should preserve the real HTTP shape.
HTTP headers are case-insensitive.
These are the same header:
Content-Type
content-type
CONTENT-TYPE
The mini framework can normalize header names to lowercase.
headers = {name.lower(): value for name, value in incoming_headers}Then handlers can write:
request.headers.get("content-type")This avoids many small bugs.
The framework should document the normalization behavior.
The Response object represents outgoing HTTP data.
It should contain:
status
headers
body bytes
Example:
class Response:
def __init__(self, body=b"", status=200, headers=None, content_type=None):
self.status = status
self.headers = headers or {}
self.body = body if isinstance(body, bytes) else str(body).encode("utf-8")
if content_type is not None:
self.headers["Content-Type"] = content_typeThe framework should also set Content-Length.
self.headers["Content-Length"] = str(len(self.body))A correct response should tell the client how many bytes are in the body.
The standard library server may handle some details, but the framework should still teach the concept.
Add convenience response types.
class HTMLResponse(Response):
def __init__(self, html, status=200, headers=None):
super().__init__(
html,
status=status,
headers=headers,
content_type="text/html; charset=utf-8",
)class JSONResponse(Response):
def __init__(self, data, status=200, headers=None):
body = json.dumps(data).encode("utf-8")
super().__init__(
body,
status=status,
headers=headers,
content_type="application/json",
)class RedirectResponse(Response):
def __init__(self, location, status=302):
super().__init__(b"", status=status, headers={"Location": location})These classes are small.
They make application code pleasant.
They also show that framework ergonomics often come from a few thoughtful wrappers.
Handlers should be allowed to return a few simple types.
Response object -> use directly
dict or list -> JSONResponse
str -> HTMLResponse or text response
bytes -> Response
None -> empty 204 response
The app can implement:
def make_response(value):
if isinstance(value, Response):
return value
if isinstance(value, (dict, list)):
return JSONResponse(value)
if isinstance(value, str):
return HTMLResponse(value)
if isinstance(value, bytes):
return Response(value)
if value is None:
return Response(b"", status=204)
raise TypeError(f"Unsupported response type: {type(value).__name__}")This is one of the places where frameworks feel magical.
The magic is only a conversion function.
Once the reader sees it, it becomes understandable.
When a route matches, the framework calls the handler.
For a route without path parameters:
handler(request)For a route with path parameters:
handler(request, **params)Example:
@app.get("/users/{user_id}")
def get_user(request, user_id):
return {"id": user_id}The router extracts:
{"user_id": "42"}The app calls:
get_user(request, user_id="42")This is ordinary Python.
The framework only connects the pieces.
Middleware wraps request handling.
It can run before and after route handlers.
Common middleware examples:
logging
timing
authentication
sessions
CORS
compression
error tracking
request ids
The simplest middleware signature is:
def middleware(request, next_handler):
response = next_handler(request)
return responseExample:
def add_powered_by(request, next_handler):
response = next_handler(request)
response.headers["X-Powered-By"] = "MiniWeb"
return responseThe app can register middleware:
app.add_middleware(add_powered_by)Middleware is powerful because it composes cross-cutting behavior without putting it into every route.
Suppose the app has three middleware functions:
logger
auth
timer
And one final handler:
route_handler
The call chain should be:
logger -> auth -> timer -> route_handler -> timer -> auth -> logger
This means middleware wraps inward.
One implementation:
def build_chain(self, endpoint):
handler = endpoint
for middleware in reversed(self.middleware):
next_handler = handler
def wrapped(request, middleware=middleware, next_handler=next_handler):
return middleware(request, next_handler)
handler = wrapped
return handlerThe default arguments are important.
Without them, Python's late binding closure behavior can make every wrapper use the last middleware.
This is a perfect place to connect back to closures from Volume I.
Applications raise exceptions.
Frameworks should turn expected exceptions into responses.
Define HTTP exceptions:
class HTTPException(Exception):
status = 500
def __init__(self, message=None):
self.message = message or self.__class__.__name__class NotFound(HTTPException):
status = 404class MethodNotAllowed(HTTPException):
status = 405The app can catch these:
try:
response = dispatch(request)
except HTTPException as exc:
response = handle_http_exception(exc)
except Exception as exc:
response = handle_unexpected_exception(exc)Do not expose full tracebacks in production responses.
For a development framework, a debug mode can show more details.
But the default should be safe.
Allow users to register handlers by status code or exception type.
@app.error_handler(404)
def not_found(request, exc):
return HTMLResponse("<h1>Not Found</h1>", status=404)Or:
@app.error_handler(ValueError)
def value_error(request, exc):
return JSONResponse({"error": str(exc)}, status=400)The simple version can support status codes first.
Exception-type handlers can be an extension.
The point is that framework users need a way to control error output without wrapping every route in try/except.
Static files are files served directly from disk.
Examples:
CSS
JavaScript
images
fonts
The mini framework can support:
app.static("/static", directory="static")Then:
GET /static/site.css
serves:
static/site.css
Static file serving must prevent path traversal.
This request must not escape the static directory:
GET /static/../../secrets.txt
Use pathlib.Path.resolve.
Check that the resolved requested path is inside the resolved static root.
root = Path(directory).resolve()
target = (root / relative_path).resolve()
if root not in target.parents and target != root:
raise NotFound()This is an important security lesson.
Even a learning framework should not teach unsafe file serving.
Static files need Content-Type.
Use Python's mimetypes module.
import mimetypes
content_type, _ = mimetypes.guess_type(path.name)
content_type = content_type or "application/octet-stream"Examples:
.html -> text/html
.css -> text/css
.js -> text/javascript or application/javascript
.png -> image/png
Content types help browsers interpret files correctly.
Without them, a browser may display content incorrectly or block resources.
A full template engine is a large project.
This capstone should implement a tiny renderer.
For example:
Template file:
<h1>Hello, {{ name }}</h1>Render call:
render_template("hello.html", name="Narendra")Output:
<h1>Hello, Narendra</h1>The first version can replace {{ variable }} tokens using a regular expression.
It should HTML-escape values by default.
import html
escaped = html.escape(str(value))Escaping matters.
Without escaping, templates can create cross-site scripting vulnerabilities.
This chapter should introduce the idea even if it does not become a full security course.
The tiny template engine does not need:
loops
conditionals
inheritance
filters
macros
includes
auto-reload
bytecode caching
It only needs variable substitution.
That is enough to teach:
load template file
replace placeholders
escape user values
return HTML response
The reader can later compare this with Jinja2 and understand why mature template engines are sophisticated.
Python web frameworks usually sit behind a standard server interface.
The older standard is WSGI.
WSGI is synchronous.
It lets servers and frameworks communicate through a callable interface.
The newer common standard is ASGI.
ASGI supports async, WebSockets, lifespan events, and more modern concurrency patterns.
This capstone does not need to fully implement WSGI or ASGI.
But it should explain the idea:
servers should not need to know every framework
frameworks should not need to implement every server
standard interfaces connect them
The development server in this project is enough for learning.
Production deployments would use battle-tested servers and mature framework adapters.
Use http.server from the standard library.
Create a custom handler that adapts BaseHTTPRequestHandler to the framework.
The handler receives HTTP data from the standard library.
It builds a Request.
It calls app.handle_request(request).
It sends the Response back.
The shape:
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class MiniWebHandler(BaseHTTPRequestHandler):
app = None
def do_GET(self):
self.handle_method("GET")
def do_POST(self):
self.handle_method("POST")
def handle_method(self, method):
request = build_request(self, method)
response = self.app.handle_request(request)
send_response(self, response)Then:
def run(app, host, port):
handler_cls = type("AppHandler", (MiniWebHandler,), {"app": app})
server = ThreadingHTTPServer((host, port), handler_cls)
server.serve_forever()This keeps the framework focused.
The standard library handles low-level HTTP parsing.
The framework handles application dispatch.
POST requests may have a body.
The standard library handler exposes headers and a stream.
Read the body using Content-Length.
length = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(length) if length else b""Do not call read() without a length.
That can block while waiting for the client to close the connection.
This is another valuable server lesson.
Network streams need clear boundaries.
HTTP request bodies use Content-Length or transfer encoding to define those boundaries.
The mini framework can support Content-Length only.
The server adapter sends:
self.send_response(response.status)
for name, value in response.headers.items():
self.send_header(name, value)
self.end_headers()
self.wfile.write(response.body)Make sure Content-Length is present.
Make sure header values are strings.
Make sure body is bytes.
This adapter is the boundary between framework objects and HTTP bytes.
The rest of the framework should not need to know about BaseHTTPRequestHandler.
Most tests should not start a server.
Test app.handle_request directly.
Example:
def test_get_route():
app = App()
@app.get("/hello")
def hello(request):
return "Hello"
request = Request(method="GET", path="/hello", query_params={}, headers={}, body=b"")
response = app.handle_request(request)
assert response.status == 200
assert response.body == b"Hello"This is fast and stable.
Server tests should be fewer.
They prove the adapter works.
They should not be the only way to test routing.
Routing tests should cover:
exact route match
missing route returns 404
wrong method returns 405
path parameter extraction
multiple path parameters
registration order behavior
allowed methods header
Examples:
@app.get("/users/{user_id}")
def get_user(request, user_id):
return {"user_id": user_id}Request:
GET /users/42
Expected JSON:
{"user_id": "42"}Keep the path parameter as a string unless type conversion is implemented.
Response tests should cover:
string return conversion
dict return conversion
list return conversion
explicit Response return
bytes return
None return
content type
content length
redirect headers
unsupported return type
These tests protect framework ergonomics.
A small change in response conversion can affect every application route.
Middleware tests should prove order.
Example:
events = []
def first(request, next_handler):
events.append("first before")
response = next_handler(request)
events.append("first after")
return response
def second(request, next_handler):
events.append("second before")
response = next_handler(request)
events.append("second after")
return responseExpected:
first before
second before
handler
second after
first after
This test also catches closure late-binding bugs.
Static file tests should use tmp_path.
Create:
tmp_path/static/site.css
Configure:
app.static("/static", tmp_path / "static")Request:
GET /static/site.css
Assert:
status 200
body matches file
content type looks like CSS
Also test path traversal:
GET /static/../secret.txt
It should return 404 or 403.
Do not skip this test.
It turns a theoretical security warning into concrete behavior.
Template tests should cover:
variable substitution
missing variable behavior
HTML escaping
template not found
Example:
Hello, {{ name }}Context:
{"name": "<Narendra>"}Output should include escaped HTML:
<Narendra>This teaches that HTML generation is not just string formatting.
It is output in a security-sensitive context.
Start with responses.
Implement:
Response
HTMLResponse
JSONResponse
RedirectResponse
make_response
Add tests for:
body bytes
status code
headers
content type
content length
return value conversion
This gives the project a stable output model before routing begins.
Implement Request.
Add:
text()
json()
query parameter handling
case-normalized headers
Test JSON parsing.
Test body decoding.
Test query strings with repeated keys.
The request object should be simple but dependable.
Implement:
Route dataclass
compile_path
Router.add_route
Router.match
The match result should include:
handler
path params
allowed methods when method is wrong
Add tests for exact and parameterized routes.
Do not connect to HTTP yet.
Routing is pure logic.
Pure logic should be tested directly.
Implement App.handle_request.
It should:
ask router for a match
call handler with request and params
convert return value to Response
handle 404
handle 405
handle HTTP exceptions
handle unexpected exceptions safely
At this point, a test can create a fake request and receive a response.
The framework is alive before any server exists.
Add:
app.route
app.get
app.post
app.put
app.delete
The route decorators should register handlers and return the original function.
Test that:
decorated route works
function identity is preserved
multiple methods can be registered
This gives the framework the familiar feel users expect.
Implement:
app.add_middleware
middleware chain building
Middleware should wrap route dispatch.
Test order.
Test that middleware can modify response headers.
Test that middleware can return early without calling next_handler.
Returning early is useful for authentication:
def require_token(request, next_handler):
if request.headers.get("authorization") != "secret":
return JSONResponse({"error": "unauthorized"}, status=401)
return next_handler(request)Add:
app.error_handler(status_or_exception)
Start with status code handlers.
Test:
custom 404 page
custom 500 response
handler return conversion
Error handlers should use the same return conversion as normal route handlers.
This consistency keeps the framework easy to learn.
Implement static file registration.
Use a special route or a static handler inside app dispatch.
Support:
app.static("/static", directory="static")Test:
existing file
missing file
path traversal rejection
content type
This milestone adds the first real filesystem behavior to the framework.
Implement:
TemplateRenderer
render_template
HTML escaping
Configure:
app = App(template_dir="templates")Usage:
@app.get("/")
def home(request):
return app.render_template("home.html", name="Narendra")The return value can be an HTMLResponse.
Test escaping.
Keep the language small.
Implement the standard library server adapter.
Support:
app.run(host="127.0.0.1", port=8000)Manual test:
python example.py
curl http://127.0.0.1:8000/
curl http://127.0.0.1:8000/users/42Add one integration test if practical.
Most behavior should already be covered without the server.
Do not put all logic inside BaseHTTPRequestHandler.
That creates a framework that is hard to test.
The handler should adapt HTTP to framework objects.
The framework should handle routing and responses.
The distinction is:
server adapter: knows about BaseHTTPRequestHandler
framework app: knows about Request and Response
This separation keeps the design clean.
If every part of the framework returns a different shape, the code becomes fragile.
Use Response as the internal standard.
Handlers may return friendly values.
But after make_response, the framework should deal with one response shape.
That makes middleware, error handlers, and server adapters simpler.
Many tiny routers return 404 for everything.
That loses useful HTTP information.
If the path exists but the method is wrong, return 405.
Include the allowed methods.
This teaches the reader that HTTP semantics matter.
Never join user paths to a directory and serve the result blindly.
This is dangerous:
path = static_root / requested_path
return path.read_bytes()The request may contain:
../
Always resolve and check that the final path stays inside the static root.
Security begins with path handling.
When building middleware wrappers in a loop, be careful with closures.
This can be wrong:
for middleware in reversed(self.middleware):
def wrapped(request):
return middleware(request, handler)Every wrapper may use the last value of middleware.
Use default arguments or a helper function.
This is an excellent real-world closure lesson.
HTML templates must escape untrusted values.
This is unsafe:
template.replace("{{ name }}", value)If value contains HTML or JavaScript, it may be executed by the browser.
Use html.escape.
Even in a tiny framework, teach the safe habit.
Manual curl testing is useful.
It is not enough.
Most framework behavior should be tested with direct Request objects.
That makes tests fast.
Fast tests encourage frequent testing.
Frequent testing makes framework design safer.
Add route parameter converters:
/users/{user_id:int}
/files/{path:path}
Add blueprints or routers.
Add before-request and after-request hooks.
Add sessions.
Add signed cookies.
Add form parsing.
Add multipart file uploads.
Add streaming responses.
Add template inheritance.
Add Jinja2 integration.
Add WSGI adapter.
Add ASGI adapter after the async chapters.
Add dependency injection.
Add request validation.
Add OpenAPI generation.
Add CORS middleware.
Add compression middleware.
Add a test client:
client = app.test_client()
response = client.get("/users/42")Add command-line project scaffolding.
Each extension teaches a real framework feature.
Do not add them all at once.
First make the core request-response cycle beautiful and clear.
This project teaches framework design.
It teaches route registration.
It teaches path matching.
It teaches request parsing.
It teaches response construction.
It teaches HTTP status codes.
It teaches middleware composition.
It teaches error handling.
It teaches safe static file serving.
It teaches basic template rendering.
It teaches server adapters.
It teaches testable architecture.
It connects many earlier ideas:
decorators become route registration
regular expressions become path matching
dictionaries become headers and query params
functions become handlers
closures become middleware chains
exceptions become HTTP responses
files become static assets and templates
JSON becomes API output
tests become framework confidence
The reader should leave with this mental model:
A web framework is a disciplined pipeline from HTTP input to HTTP output.
Once that pipeline is clear, mature frameworks feel less mysterious.
The project is complete when:
App can register routes.
GET routes work.
POST routes work.
Exact paths match.
Parameterized paths match.
Path parameters are passed to handlers.
Missing paths return 404.
Wrong methods return 405.
405 responses include allowed methods.
Request exposes method, path, query params, headers, body, text, and json.
Response stores status, headers, and body bytes.
String returns become HTML/text responses.
Dict and list returns become JSON responses.
Explicit Response objects pass through unchanged.
RedirectResponse sets Location.
Middleware wraps route handling in the correct order.
Middleware can modify responses.
Middleware can return early.
Custom error handlers work.
Static files can be served.
Path traversal is rejected.
Templates render variables.
Template values are HTML-escaped.
Development server runs the app.
Tests cover routing, request, response, middleware, errors, static files, and templates.
Documentation explains framework limitations.
When all of these are true, the reader has built a real miniature web framework.
-
Create the
miniwebpackage structure. -
Implement the base
Response. -
Implement
HTMLResponse. -
Implement
JSONResponse. -
Implement
RedirectResponse. -
Implement
make_response. -
Add response conversion tests.
-
Implement the
Requestobject. -
Add
text()andjson()helpers. -
Parse query strings with repeated keys.
-
Normalize headers.
-
Implement route pattern compilation.
-
Implement exact route matching.
-
Implement path parameter matching.
-
Implement 404 handling.
-
Implement 405 handling.
-
Add route decorators.
-
Implement
App.handle_request. -
Test handler return conversion.
-
Implement middleware registration.
-
Build the middleware chain.
-
Test middleware order.
-
Implement custom error handlers.
-
Implement static file serving.
-
Test path traversal rejection.
-
Implement the template renderer.
-
Test HTML escaping.
-
Implement the development server adapter.
-
Add a small example application.
-
Document limitations and extension ideas.
Capstone 08 built a Mini Web Framework.
It introduced routing, request objects, response objects, middleware, error handlers, static files, templates, and HTTP server adapters.
Capstone 09 will build a Mini Event Loop.
The Mini Event Loop project will move below frameworks and into asynchronous execution itself.
It will introduce cooperative multitasking, callbacks, futures, tasks, non-blocking sockets, timers, selectors, and the foundation of async frameworks.
The transition is:
Mini Web Framework organizes synchronous HTTP application flow
Mini Event Loop explains how asynchronous systems schedule work
The next project will show how Python can juggle many waiting operations without one thread per task.