Middleware & Signals
🧵 Middleware & Signals
The Middleware Chain
MIDDLEWARE in settings.py (first seen back in Course 1's settings tour) is an ordered list — a request flows through it top-to-bottom, and the response flows back bottom-to-top, the same "onion" model Express's app.use() chain uses:
✍️ Writing Custom Middleware
The simplest form is a function that takes get_response (the next step in the chain) and returns a function that wraps it — request logic before calling it, response logic after:
request.request_id set here is exactly the correlation ID pattern from the TypeScript course's Logging & Observability chapter — attach one ID per request at the very top of the chain, and every view or log call downstream can reference it.
Express Middleware vs Django Middleware
Express
Django
Project-Wide, Not Per-View
Unlike @login_required (applied per-view), middleware in MIDDLEWARE runs on every single request the project handles — the right place for logging, timing, or security headers, not for view-specific access rules.
Class-Based Alternative
Middleware can also be a class with __init__(self, get_response) and __call__(self, request) — functionally identical, useful when a middleware needs to hold configuration set up once.
📡 Signals: Decoupled Event Notifications
A signal lets one piece of code announce "this happened" without knowing (or caring) who's listening — Django fires several built-in signals automatically, most usefully post_save and post_delete:
Auto-Creating a Profile When a User Registers
sender=User
Restricts the receiver to fire only for saves on the User model — without it, this function would run for every model's post_save, project-wide.
created
A boolean passed to every post_save receiver — True only on the initial INSERT, letting this receiver distinguish "a new user just registered" from "an existing user was updated."
💻 Coding Challenges
Challenge 1: Write a Security-Header Middleware
Write a middleware function that adds an X-Content-Type-Options: nosniff header to every response, then add it to MIDDLEWARE in the correct order relative to Django's own SecurityMiddleware.
Goal: Practice the get_response closure pattern for a response-modifying (rather than request-modifying) middleware.
Challenge 2: Write a post_delete Signal Receiver
Write a signal receiver that logs a message (e.g. print(f"Book deleted: {instance.title}")) whenever a Book is deleted, including the required apps.py wiring to make sure it actually runs.
Goal: Practice the full signal setup, including the easy-to-forget ready() import step.
Challenge 3: Middleware vs Signal — Which Fits?
For each of these three requirements, state whether middleware or a signal is the better fit, and why: (a) rejecting all requests from a blocked list of IP addresses, (b) sending a welcome email after a new user registers, (c) adding a Content-Security-Policy header to every response.
Goal: Practice recognizing which of the two tools actually matches a given requirement's shape.
A signal receiver connected in some other app's signals.py can silently run every time you call .save() on a model — with nothing at the call site hinting that anything else happens. This makes signals genuinely hard to trace when debugging: "why did a profile get created here?" sends you hunting across files instead of reading top-to-bottom. Reach for a signal only when the sender genuinely shouldn't know about the receiver (decoupled apps, plugin-style extensibility); for logic within your own app that always needs to happen together, a plain method call or an overridden save() is usually clearer. And remember: a signals.py file that's never imported from apps.py's ready() method never actually connects — the receivers simply don't run, with no error telling you why.
🎯 What's Next
Middleware and signals both run synchronously, inside the request/response cycle. The next chapter covers work that shouldn't block a response at all: Background Tasks — Celery basics and task queues for anything too slow to run while a user waits.