Middleware is the invisible scaffolding of a web framework. It intercepts requests before they reach your route handler and responses before they go back to the client. In a lightweight Python framework, middleware enables modular, composable behavior without heavy abstractions.
1. What Middleware Does
Middleware functions operate around your route handlers. They can:
- Log incoming requests
- Authenticate users
- Inject request-scoped data (e.g., database connections)
- Modify or short-circuit responses
- Handle exceptions in a central place
The simplest middleware is just a function that wraps the handler.
2. Designing a Minimal Middleware System
You can define middleware as a callable that takes a handler and returns a new handler:
def logger_middleware(next_handler):
def handler(request):
print(f"{request.method} {request.path}")
return next_handler(request)
return handler
If you register multiple middleware, they are composed from outermost to innermost:
for mw in reversed(middleware_stack):
handler = mw(handler)
This pattern allows each middleware to act like a layer in an onion: outer layers wrap inner ones.
3. Adding Middleware Support to the Framework
Assuming you have a central Framework
class, you can expose .use()
to add middleware:
class Framework:
def __init__(self):
self.middleware = []
def use(self, middleware_func):
self.middleware.append(middleware_func)
def _apply_middleware(self, handler):
for mw in reversed(self.middleware):
handler = mw(handler)
return handler
def handle_request(self, request):
route = self.match(request)
handler = self._apply_middleware(route.handler)
return handler(request)
This allows you to add middleware declaratively:
app = Framework()
app.use(logger_middleware)
app.use(auth_middleware)
4. Middleware with Context
To pass context downstream (like a user object), you can mutate the request:
def auth_middleware(next_handler):
def handler(request):
token = request.headers.get("Authorization")
request.user = verify_token(token) if token else None
return next_handler(request)
return handler
Or, if you want immutability, you could use a RequestContext
object that wraps the request.
5. Handling Exceptions Gracefully
A powerful pattern is wrapping the entire handler chain in an error-handling middleware:
def error_middleware(next_handler):
def handler(request):
try:
return next_handler(request)
except Exception as e:
return Response("Internal Server Error", status=500)
return handler
This keeps your actual route logic clean, delegating error recovery to middleware.
6. Middleware for Response Manipulation
Middleware isn’t limited to the request path. It can inspect or transform the response:
def add_cors_headers(next_handler):
def handler(request):
response = next_handler(request)
response.headers["Access-Control-Allow-Origin"] = "*"
return response
return handler
You can also standardize headers, apply compression, or set cookies—all in one place.
7. Async Compatibility
To support async def
handlers and middleware, use async def
and await
consistently:
async def logger_middleware(next_handler):
async def handler(request):
print(f"{request.method} {request.path}")
return await next_handler(request)
return handler
Be sure your framework can detect sync vs async handlers and handle them appropriately using inspect.iscoroutinefunction
.
8. Real-World Examples
Some practical middleware ideas include:
- Session middleware – Load/store user sessions in a cookie or Redis
- CSRF protection – Validate tokens on POST/PUT requests
- Rate limiting – Check client IP against an in-memory or external quota store
- Timing middleware – Measure execution time and log slow responses
By stacking these in a predictable order, your app gains power without losing readability.
9. Debugging Tips
- Always test middleware independently before stacking.
- Log at key points to trace the flow through layers.
- Ensure middleware does not swallow exceptions silently unless that's intentional.
Wrap-Up
Middleware enables separation of concerns in your framework: security, logging, error handling, and more can live in isolated units. By designing a simple but powerful middleware chain, your micro-framework stays modular and scalable.
Want to dive deeper? Check out my 20-page PDF guide: Building a Lightweight Python Web Framework from Scratch
Top comments (0)