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:
| Middleware | What it actually does |
|---|---|
SecurityMiddleware | Applies a handful of security-related HTTP headers and settings (HTTPS redirects, HSTS) to every response |
SessionMiddleware | Attaches request.session to every incoming request, backed by the session store |
CsrfViewMiddleware | Rejects unsafe POST requests missing a valid CSRF token — the enforcement layer behind everything in CSRF & Security |
AuthenticationMiddleware | Attaches 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.
MIDDLEWARE list to watch it run.