Writing Middleware

Middleware sits between the server and your route handlers, processing every request and response that flows through your application. It’s the right tool for cross-cutting concerns — things that apply to all requests, not just specific routes.

Common middleware use cases:

  • Request logging and timing

  • Authentication and authorization

  • Adding security headers

  • Request ID generation

  • Rate limiting

  • Response compression (built-in)

Hooks vs. Middleware

Responder gives you two levels of request processing:

Hooks (before_request / after_request) run inside Responder’s routing layer. They receive Responder’s req and resp objects and are the simplest way to add behavior:

@api.route(before_request=True)
def add_header(req, resp):
    resp.headers["X-Powered-By"] = "Responder"

@api.after_request()
def log_request(req, resp):
    print(f"{req.method} {req.url.path} -> {resp.status_code}")

Middleware runs at the ASGI level, wrapping the entire application. It’s more powerful but more complex — you work with raw ASGI scopes instead of Responder objects. Use middleware when you need to process requests before they reach Responder’s routing, or when you need to integrate with Starlette middleware.

Using Starlette Middleware

Responder is built on Starlette, so any Starlette middleware works out of the box:

from starlette.middleware.base import BaseHTTPMiddleware

class TimingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        import time
        start = time.time()
        response = await call_next(request)
        duration = time.time() - start
        response.headers["X-Response-Time"] = f"{duration:.3f}s"
        return response

api.add_middleware(TimingMiddleware)

The dispatch method receives a Starlette Request and a call_next function. Call call_next(request) to pass the request to the next middleware (or to your route handler). The return value is a Starlette Response that you can modify before it’s sent.

Built-in Middleware

Responder configures several middleware components automatically:

  • GZipMiddleware — compresses responses larger than 500 bytes

  • TrustedHostMiddleware — validates the Host header

  • ServerErrorMiddleware — catches unhandled exceptions

  • ExceptionMiddleware — routes exceptions to your handlers

  • SessionMiddleware — manages signed cookie sessions

Optional middleware you can enable:

  • CORSMiddlewareapi = responder.API(cors=True)

  • HTTPSRedirectMiddlewareapi = responder.API(enable_hsts=True)

Adding Third-Party Middleware

Any ASGI middleware can be added with api.add_middleware():

from some_package import SomeMiddleware

api.add_middleware(SomeMiddleware, option1="value", option2=True)

Keyword arguments are passed to the middleware’s constructor.

Middleware Order

Middleware wraps your application like layers of an onion. The last middleware added is the outermost layer — it sees the request first and the response last.

Responder’s built-in middleware stack (from outermost to innermost):

  1. SessionMiddleware

  2. ServerErrorMiddleware

  3. CORSMiddleware (if enabled)

  4. TrustedHostMiddleware

  5. HTTPSRedirectMiddleware (if enabled)

  6. GZipMiddleware

  7. ExceptionMiddleware

  8. Your routes

When you call api.add_middleware(), your middleware is added outside the existing stack. Keep this in mind for ordering dependencies — if middleware A depends on middleware B having run first, add B before A.

When to Use What

  • Simple header additions, logging, auth checks → use hooks

  • Response transformation, timing, third-party integrations → use middleware

  • Rate limiting → use the built-in RateLimiter (it uses hooks internally)

  • Request ID → use api = responder.API(request_id=True)

Start with hooks. They’re simpler and cover most cases. Graduate to middleware when hooks aren’t enough.