Middleware

Every request TaskFlow receives passes through several pieces of code before it ever reaches a view function, and every response passes back through those same pieces before it reaches the browser. That surrounding layer is middleware — and it's one of the parts of Django most beginner material skips over entirely, even though you've been relying on it since your very first page load. Sessions, login state, CSRF protection — none of that happens inside your views. It happens in middleware, on every single request, automatically.

The Onion Model: Layers Around Every Request

The clearest mental model for middleware is an onion. Your view sits at the center, and each piece of middleware is a layer wrapped around it. An incoming request has to pass inward through every layer, in order, before it reaches the view at the core. The response the view returns then has to pass back outward through those same layers, in the reverse order, before it finally leaves as an HTTP response. Nothing skips a layer in either direction:

Request  ─▶ SecurityMiddleware ─▶ SessionMiddleware ─▶ AuthenticationMiddleware ─▶ your view
                                                                                        │
Response ◀─ SecurityMiddleware ◀─ SessionMiddleware ◀─ AuthenticationMiddleware ◀──────┘

Each middleware gets a chance to act BEFORE calling the next one (on the way in)
and AFTER it returns (on the way out) — which is exactly the shape of self.get_response(request)
inside a middleware's __call__ method.

That's the whole model. A middleware can inspect or reject a request before it goes any deeper, and it can inspect or rewrite a response after every layer inside it has already finished. Both directions matter — some middleware only care about the request on the way in (rejecting it outright), some only touch the response on the way out (adding a header), and some, like the timing example below, do a little of both.

The MIDDLEWARE Setting — and Why Order Matters

Which middleware run, and in what order, is controlled by one list in settings.py:

# taskflow_project/settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

Going back to the onion picture: the first entry in this list is the outermost layer, and the last entry is the innermost, sitting right next to your view. This is exactly why order matters — AuthenticationMiddleware has to run before anything that depends on request.user being set, and it has to run after SessionMiddleware, since it reads the logged-in user's ID out of the session that SessionMiddleware just attached to the request. Reordering these two would break login checking across the whole project — not with an error, but silently, which is exactly why this list deserves care rather than guesswork.

Built-in Middleware You're Already Depending On

Several pieces of Django you've already used in this track are, under the hood, just entries in this same list:

MiddlewareWhat it actually does
SecurityMiddlewareApplies a handful of security-related HTTP headers and settings (HTTPS redirects, HSTS) to every response
SessionMiddlewareAttaches request.session to every incoming request, backed by the session store
CsrfViewMiddlewareRejects unsafe POST requests missing a valid CSRF token — the enforcement layer behind everything in CSRF & Security
AuthenticationMiddlewareAttaches request.user to every request by reading the logged-in user's ID from the session — this is literally what makes request.user exist at all, as used throughout User Authentication

None of this is new behavior — it's the same behavior you've already been using, just named and placed for the first time. request.user feels like it's "just there" in every view precisely because AuthenticationMiddleware put it there, on every single request, before your view code ever ran.

Writing a Custom Middleware: RequestTimingMiddleware

A custom middleware is a class with a constructor that accepts get_response (the next layer inward — either the next middleware, or the view itself) and a __call__ method that runs on every request. Here's one that logs how long each request took to handle — useful for spotting a slow TaskFlow view before users start noticing:

# taskflow/middleware.py
import time
import logging

logger = logging.getLogger(__name__)

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

    def __call__(self, request):
        started_at = time.monotonic()

        # everything before this line runs on the way IN
        response = self.get_response(request)
        # everything after this line runs on the way OUT

        duration_ms = (time.monotonic() - started_at) * 1000
        logger.info(f"{request.path} took {duration_ms:.1f}ms")

        return response

Django creates exactly one instance of RequestTimingMiddleware when the server starts, storing the next layer's get_response callable once in __init__. Every subsequent request reuses that same instance, calling __call__ again — which is why timing has to be measured fresh inside __call__ itself, not __init__. The line response = self.get_response(request) is the pivot point of the whole onion model: everything above it in this method is "on the way in", everything below it is "on the way out", and that single call is what actually invokes the next layer, all the way down to the view.

Registering the Middleware

Writing the class does nothing on its own — Django only runs middleware listed in MIDDLEWARE. Adding RequestTimingMiddleware's dotted import path to the list turns it on:

# taskflow_project/settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'taskflow.middleware.RequestTimingMiddleware',
    # ... the rest of the built-in middleware ...
]

Position in the list still matters here too — placed early, RequestTimingMiddleware measures close to the true total request time, since almost every other layer runs inside it. Placed near the end, right before the view, it measures mostly just the view's own execution time instead.

A Realistic TaskFlow Use Case: Enforcing Profile Setup

Middleware earns its keep on rules that need to apply to every request, regardless of which view handles it — exactly the kind of cross-cutting check that would otherwise mean adding the same guard clause to dozens of individual views. Suppose TaskFlow requires every user to fill out a profile (the same Profile model introduced in Custom User Model) before they can use the rest of the app. A middleware is a natural place to enforce that, once, for the whole project:

# taskflow/middleware.py
from django.shortcuts import redirect

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

    def __call__(self, request):
        allowed_paths = ('/accounts/finish-setup/', '/accounts/logout/')

        if (request.user.is_authenticated
                and not request.user.profile.setup_complete
                and request.path not in allowed_paths):
            return redirect('finish-setup')

        return self.get_response(request)

Notice this middleware has to come after AuthenticationMiddleware in the list, since it reads request.user — one more concrete reason order in MIDDLEWARE isn't cosmetic. A logged-in user with an incomplete profile gets redirected to /accounts/finish-setup/ from anywhere in the app, without a single if check added to any individual view.

Note: This is correct, real middleware code — but this website's own project doesn't run a Django server, so you won't see a request actually flow through these layers on this page. Add either class to your own project's MIDDLEWARE list to watch it run.
Next up: Middleware reacts to every request. Signals cover a different kind of "automatic reaction" — one triggered by model events, like a new user being saved, rather than by HTTP traffic.