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

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

Part 5 of 13 · ~2 min

Prototype Three: Django

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

    def __call__(self, request):
        response = self.get_response(request)  # hand off to whatever's next
        if request.path.startswith("/returns/") and request.method == "POST":
            print(f"approval attempt: {request.path} -> {response.status_code}")
        return response
# settings.py
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "myapp.middleware.ApprovalLogMiddleware",
    "myapp.middleware.RequireManagerMiddleware",
]

Of the three frameworks, Django names the wrapping idea the most literally: a middleware object here is a closure over get_response, the whole downstream chain flattened into one reference it holds onto. Call that reference and there's room to run code both ahead of the call and after it returns, inside the same method. __init__ fires a single time, when the process boots — precisely the moment get_response gets tucked away — while __call__ fires once for every incoming request. Recognize that split? It's the identical constructor-versus-per-request divide ApprovalAuditMiddleware drew a few sections back, just written in Python's grammar instead of C#'s.

process_view and process_exception: the Hooks Most Tutorials Skip

There's one question __call__ is structurally unable to answer: what view is this request actually headed toward? URL resolution hasn't happened by the point __call__ starts running — it's still buried somewhere inside the get_response call this method is about to make. Anything that needs to key off the destination view itself, instead of guessing from a URL string, needs a different hook, one Django only invokes once routing has already picked a winner:

class RequireManagerMiddleware:
    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 after Django knows exactly which view will handle this request
        if getattr(view_func, "requires_manager", False) and not request.user.is_shift_lead:
            return HttpResponseForbidden()
# views.py
def requires_manager(view_func):
    view_func.requires_manager = True
    return view_func

@requires_manager
def approve_return(request, return_id):
    # only reached if process_view above let it through
    ...

The reason process_view checks view_func.requires_manager instead of request.path is going to matter a great deal in a few chapters — a check tied to the actual function Django resolved survives a URL refactor that a raw path string never would.

Short-Circuiting, Django's Way

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

    def __call__(self, request):
        if is_nightly_reconciliation_window() and request.method in ("POST", "PATCH"):
            return HttpResponse("Store is closed for nightly reconciliation", status=503)  # get_response never runs
        return self.get_response(request)

Short-circuiting in Django needs no special API at all — it's just not calling self.get_response(request) and handing back a response directly. No four-argument arity trick, no separate terminal-middleware concept. Every night, Counter's reconciliation job needs writes paused for a few minutes while it settles the day's numbers, and this is the entire mechanism: an if, wrapped around whether the wrapped chain gets called at all.