CodeOath
← All posts
Architecture & Patterns55 min total · 13 parts

Middleware Pipelines Compared: ASP.NET Core, Express.js, and Django

Contents — Part 5 of 13: Django
Part 5 of 13 · ~2 min

Django

class LoggingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        print(f"Incoming: {request.path}")
        response = self.get_response(request)  # pass control down the chain
        print(f"Outgoing: {response.status_code}")
        return response
# settings.py
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "myapp.middleware.LoggingMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
]

Django makes the "wrapping" nature the most explicit of the three: each middleware is a callable that wraps get_response (effectively, "everything after me"), calls it, and can act both before and after that call — which is precisely the down-then-up shape all three frameworks share. Exactly like ASP.NET Core's class-based middleware, __init__ runs once at server startup (where get_response — the next layer down — is captured), and __call__ runs on every single request.

process_view, process_exception, and the older hook-based style

Modern Django middleware is written as the single __call__ method above, but a middleware class can additionally define optional hook methods that Django calls at specific points, without needing to restructure the whole __call__:

class AdminOnlyMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_view(self, request, view_func, view_args, view_kwargs):
        # runs right before Django calls the view, after URL routing has resolved it —
        # this is the only point where the middleware knows WHICH view is about to run
        if request.path.startswith("/admin/") and not request.user.is_staff:
            return HttpResponseForbidden()

    def process_exception(self, request, exception):
        # runs if the view raised an unhandled exception
        log_exception(exception)
        return None  # returning None lets Django's normal exception handling continue

process_view exists because, by the time __call__ itself runs, Django hasn't resolved the URL to a specific view yet — a middleware that needs to know which view is about to handle the request (not just the raw path) has no other hook for that information.

Short-circuiting in Django

class MaintenanceModeMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if settings.MAINTENANCE_MODE:
            return HttpResponse("Down for maintenance", status=503)  # get_response is never called
        return self.get_response(request)

Short-circuiting in Django is simply not calling self.get_response(request) and returning a response directly instead — there's no special API for it, unlike Express's four-argument error handlers or ASP.NET Core's terminal app.Run. This is arguably the simplest of the three mechanisms to reason about, precisely because it's just an if statement around a function call.