DEV Community

Cover image for Understanding Middleware in a Lightweight Python Web Framework
HexShift
HexShift

Posted on

Understanding Middleware in a Lightweight Python Web Framework

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
Enter fullscreen mode Exit fullscreen mode

If you register multiple middleware, they are composed from outermost to innermost:

for mw in reversed(middleware_stack):
    handler = mw(handler)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

This allows you to add middleware declaratively:

app = Framework()
app.use(logger_middleware)
app.use(auth_middleware)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)