# Django 5.2: Custom Middleware en Signaalverwerking voor Technische Interviews > Django 5.2 middleware en signals beheersen: middleware-pipeline, async middleware, pre_save/post_save signals en veelvoorkomende interviewpatronen. - Published: 2026-04-29 - Updated: 2026-04-29 - Author: SharpSkill - Tags: django, python, middleware, signals, interview - Reading time: 9 min --- Django 5.2 introduceert belangrijke verbeteringen op het gebied van async-middleware en signaalverwerking, waardoor het framework beter geschikt wordt voor moderne, schaalbare webapplicaties. Custom middleware en signals behoren tot de meest gevraagde onderwerpen tijdens technische Django-interviews, omdat ze inzicht geven in hoe een ontwikkelaar de request-/response-cyclus begrijpt en in hoeverre bedrijfslogica ontkoppeld kan worden opgezet. Dit artikel behandelt beide mechanismen grondig: van de basisstructuur van middleware tot asynchrone varianten in Django 5.2, en van ingebouwde signals tot volledig aangepaste domeingebeurtenissen. Elk codevoorbeeld is direct toepasbaar in productieomgevingen en sluit aan bij de vragen die interviewers daadwerkelijk stellen. > **Snelle interviewreferentie** > > Middleware verwerkt elk HTTP-verzoek en -antwoord (cross-cutting concerns). Signals ontkoppelen bedrijfslogica via het publish-subscribe-patroon. Django 5.2 LTS ondersteunt beide volledig asynchroon. Dit zijn de drie kernpunten die elke kandidaat in een Django-interview paraat moet hebben. ## Hoe Django Middleware werkt Middleware in Django vormt een reeks lagen rondom de view-functie. Elk verzoek doorloopt alle middleware-klassen van boven naar beneden voordat het de view bereikt. Het antwoord volgt dezelfde weg terug in omgekeerde volgorde. Dit onion-achtige model maakt het mogelijk om functionaliteit zoals authenticatie, logging, CORS-headers en beveiligingscontroles centraal af te handelen zonder individuele views aan te passen. Elke middleware-klasse ontvangt een `get_response`-callable in de constructor. Deze callable verwijst naar de volgende middleware in de keten, of naar de view zelf wanneer het de laatste middleware betreft. De `__call__`-methode bevat de logica die voor en na de view wordt uitgevoerd. ```python # middleware.py - Basic middleware structure class SimpleMiddleware: def __init__(self, get_response): self.get_response = get_response # One-time configuration happens here at server start def __call__(self, request): # Code executed BEFORE the view (and later middleware) response = self.get_response(request) # Code executed AFTER the view (on the way back) return response ``` Het `__init__`-gedeelte wordt eenmalig uitgevoerd bij het opstarten van de server, niet bij elk verzoek. Dit is de juiste plek voor configuratie die niet per verzoek hoeft te veranderen, zoals het opzetten van loggers of het laden van instellingen. Interviewers toetsen dit onderscheid regelmatig om te bepalen of een kandidaat de levenscyclus van middleware daadwerkelijk begrijpt. ## Een Request-Logging-Middleware bouwen Een veelgevraagd interviewscenario is het bouwen van middleware die elk verzoek logt met relevante metadata: HTTP-methode, pad, statuscode, duur en IP-adres. Dit voorbeeld demonstreert tevens de `process_exception`-hook, die uitsluitend wordt aangeroepen wanneer de view een onafgevangen uitzondering gooit. ```python # apps/core/middleware.py import logging import time logger = logging.getLogger('django.request') class RequestLoggingMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): start_time = time.monotonic() # Attach metadata before view processing request.start_time = start_time response = self.get_response(request) duration = time.monotonic() - start_time logger.info( 'method=%s path=%s status=%d duration=%.3fs ip=%s', request.method, request.get_full_path(), response.status_code, duration, request.META.get('REMOTE_ADDR'), ) return response def process_exception(self, request, exception): # Called only when the view raises an exception duration = time.monotonic() - getattr(request, 'start_time', 0) logger.error( 'method=%s path=%s exception=%s duration=%.3fs', request.method, request.get_full_path(), str(exception), duration, ) return None # Let Django's default exception handling continue ``` Het gebruik van `time.monotonic()` in plaats van `time.time()` is een belangrijk detail. Monotone klokken worden niet beïnvloed door systeemtijdaanpassingen (NTP-synchronisatie, zomertijd), waardoor de gemeten duur altijd betrouwbaar is. Interviewers waarderen het wanneer kandidaten dit onderscheid spontaan benoemen. De `process_exception`-methode retourneert `None`, wat betekent dat Django de standaard uitzonderingsafhandeling voortzet. Wanneer deze methode een `HttpResponse`-object retourneert, wordt dat antwoord direct naar de client gestuurd en worden latere middleware-lagen overgeslagen. ## Middleware-volgorde De volgorde van middleware in `settings.py` is bepalend voor het gedrag van de applicatie. SecurityMiddleware moet altijd bovenaan staan om beveiligingsheaders zo vroeg mogelijk in te stellen. AuthenticationMiddleware moet na SessionMiddleware komen, omdat authenticatie afhankelijk is van sessiegegevens. Custom middleware wordt doorgaans onderaan de lijst geplaatst, na de volledige Django-kernstack. ```python # settings.py - Middleware ordering matters 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', # Custom middleware placed after Django's core stack 'apps.core.middleware.RequestLoggingMiddleware', ] ``` Een veelgemaakte interviewfout is het plaatsen van middleware die `request.user` gebruikt boven `AuthenticationMiddleware`. In dat geval is het `user`-attribuut nog niet beschikbaar op het request-object, wat leidt tot `AttributeError`-uitzonderingen. Het begrijpen van deze afhankelijkheidsketen onderscheidt ervaren Django-ontwikkelaars van beginners. ## Async Middleware in Django 5.2 Django 5.2 biedt volledige ondersteuning voor asynchrone middleware. Door `async_capable = True` en `sync_capable = False` in te stellen, wordt de middleware uitsluitend in een asynchrone context uitgevoerd. De `get_response`-callable wordt in dat geval een awaitable, waardoor de gehele request-/response-cyclus non-blocking verloopt. ```python # apps/core/middleware.py import asyncio import time class AsyncTimingMiddleware: # Mark this middleware as async-capable async_capable = True sync_capable = False def __init__(self, get_response): self.get_response = get_response async def __call__(self, request): start = time.monotonic() # get_response is awaitable in async context response = await self.get_response(request) duration = time.monotonic() - start response['X-Request-Duration'] = f'{duration:.4f}s' return response ``` Het verschil met synchrone middleware is subtiel maar belangrijk: de `__call__`-methode is nu een `async def` en de aanroep van `self.get_response(request)` wordt geawait. Wanneer `sync_capable = False` is ingesteld, weigert Django de middleware te laden in een synchrone WSGI-context. Dit voorkomt runtime-fouten in productie wanneer de server nog niet op ASGI is overgezet. Voor interviews is het essentieel om te weten dat middleware met `async_capable = True` en `sync_capable = True` (de standaard in Django 5.2) in beide contexten kan functioneren. Django bepaalt automatisch welke variant wordt gebruikt op basis van de serverconfiguratie. ## Django Signals: het publish-subscribe-patroon Signals in Django implementeren het observer-patroon (publish-subscribe). Een signal is een notificatie die wordt verstuurd wanneer een specifieke actie plaatsvindt, zoals het opslaan of verwijderen van een model-instantie. Ontvangers (receivers) registreren zich op een signal en worden automatisch aangeroepen wanneer dat signal wordt verstuurd. Dit mechanisme maakt het mogelijk om bedrijfslogica te ontkoppelen: het model hoeft niet te weten welke acties na het opslaan worden uitgevoerd. De ingebouwde signals van Django omvatten `pre_save`, `post_save`, `pre_delete`, `post_delete`, `m2m_changed` en `request_started`/`request_finished`. Daarnaast kunnen ontwikkelaars volledig eigen signals definiëren voor domeinspecifieke gebeurtenissen. ## post_save voor profielaanmaak Het klassieke voorbeeld van `post_save` is het automatisch aanmaken van een gerelateerd profiel wanneer een nieuwe gebruiker wordt geregistreerd. Het `created`-argument onderscheidt een INSERT (nieuw record) van een UPDATE (bestaand record), waardoor het profiel uitsluitend bij de eerste registratie wordt aangemaakt. ```python # apps/accounts/signals.py from django.db.models.signals import post_save from django.dispatch import receiver from django.contrib.auth import get_user_model from apps.accounts.models import UserProfile User = get_user_model() @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): # 'created' is True only on INSERT, False on UPDATE if created: UserProfile.objects.create( user=instance, display_name=instance.get_full_name() or instance.username, ) ``` Het gebruik van `get_user_model()` in plaats van een directe import van het User-model is een best practice die ervoor zorgt dat de code compatibel blijft met custom user models. Interviewers letten specifiek op dit patroon als indicator van productie-ervaring. ## pre_save voor validatie en transformatie Het `pre_save`-signal wordt verstuurd voordat het model daadwerkelijk in de database wordt weggeschreven. Dit maakt het geschikt voor gegevenstransformatie zoals het automatisch genereren van slugs, het normaliseren van invoer of het uitvoeren van validaties die niet in de model-`clean()`-methode passen. ```python # apps/blog/signals.py from django.db.models.signals import pre_save from django.dispatch import receiver from django.utils.text import slugify from apps.blog.models import Article @receiver(pre_save, sender=Article) def auto_generate_slug(sender, instance, **kwargs): if not instance.slug: base_slug = slugify(instance.title) slug = base_slug counter = 1 # Ensure slug uniqueness while Article.objects.filter(slug=slug).exclude(pk=instance.pk).exists(): slug = f'{base_slug}-{counter}' counter += 1 instance.slug = slug ``` De `exclude(pk=instance.pk)`-clausule is cruciaal: zonder deze uitsluiting zou het opslaan van een bestaand artikel met dezelfde slug een oneindige lus veroorzaken. Dit is een veelvoorkomende valkuil in interviews en productieomgevingen. ## Signals registreren in AppConfig.ready() Django vereist dat signal-receivers worden geïmporteerd voordat ze effect hebben. De aanbevolen methode is het importeren van de `signals`-module in de `ready()`-methode van de `AppConfig`-klasse. Deze methode wordt eenmalig aangeroepen wanneer de applicatie volledig is geladen, wat het ideale moment is voor het registreren van receivers. ```python # apps/accounts/apps.py from django.apps import AppConfig class AccountsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'apps.accounts' def ready(self): # Import signals module to register receivers import apps.accounts.signals # noqa: F401 ``` Het `# noqa: F401`-commentaar onderdrukt de linting-waarschuwing voor ongebruikte imports. De import heeft als neveneffect dat de `@receiver`-decorators worden uitgevoerd, waarmee de signal-receivers daadwerkelijk worden geregistreerd. Zonder deze import in `ready()` worden de receivers nooit aangeroepen, ongeacht hoe correct ze zijn gedefinieerd. ## Aangepaste signals voor domeingebeurtenissen Naast de ingebouwde model-signals ondersteunt Django volledig aangepaste signals voor domeinspecifieke gebeurtenissen. Dit patroon is bijzonder waardevol in grotere applicaties waar meerdere modules moeten reageren op dezelfde bedrijfsgebeurtenis, zoals het afronden van een bestelling of het verwerken van een terugbetaling. ```python # apps/orders/signals.py from django.dispatch import Signal # Define custom signals with documentation order_completed = Signal() # Sent after payment confirmation order_refunded = Signal() # Sent after refund processing # apps/orders/services.py from apps.orders.signals import order_completed def complete_order(order): order.status = 'completed' order.save() # Dispatch signal with relevant context order_completed.send( sender=order.__class__, order=order, total=order.total_amount, ) # apps/notifications/receivers.py from django.dispatch import receiver from apps.orders.signals import order_completed @receiver(order_completed) def send_order_confirmation_email(sender, order, **kwargs): # Email logic decoupled from order processing from apps.notifications.services import send_email send_email( to=order.customer.email, template='order_confirmation', context={'order': order}, ) ``` De architectuur is helder: `orders/signals.py` definieert de signal-objecten, `orders/services.py` verstuurt het signal na de bedrijfsactie en `notifications/receivers.py` reageert erop. De bestelmodule hoeft niet te weten dat er een e-mail wordt verstuurd. Nieuwe ontvangers kunnen worden toegevoegd zonder de orderlogica aan te passen. Dit is het publish-subscribe-patroon in de praktijk. ## Veelvoorkomende interviewvalkuilen > **Signals worden niet getriggerd door QuerySet.update()** > > Een van de meest voorkomende fouten in Django-interviews: `QuerySet.update()` en `QuerySet.delete()` triggeren geen `pre_save`/`post_save` of `pre_delete`/`post_delete` signals. Deze methoden werken rechtstreeks op databaseniveau en omzeilen de model-instantie volledig. Gebruik individuele `.save()`- en `.delete()`-aanroepen wanneer signals moeten worden uitgevoerd. - **Circulaire imports**: Signal-modules die rechtstreeks modellen importeren uit andere apps veroorzaken vaak circulaire imports. De oplossing is het gebruik van lazy imports binnen de receiver-functie of het verkrijgen van het model via `apps.get_model()`. - **Zware logica in receivers**: Signal-receivers worden synchroon uitgevoerd binnen dezelfde transactie als de oorspronkelijke actie. Het versturen van e-mails of het aanroepen van externe API's in een receiver blokkeert het opslaan van het model. Gebruik `transaction.on_commit()` om dergelijke acties uit te stellen tot na de transactie. - **Geen foutafhandeling**: Een exception in een receiver breekt de gehele save-transactie af. Gebruik `Signal.send_robust()` in plaats van `Signal.send()` wanneer ontvangers onafhankelijk van elkaar moeten kunnen falen. - **Dubbele registratie**: Wanneer de `signals`-module meerdere keren wordt geïmporteerd, worden receivers dubbel geregistreerd. De `dispatch_uid`-parameter voorkomt dit: `@receiver(post_save, sender=User, dispatch_uid='create_profile')`. - **Onvoorspelbare uitvoeringsvolgorde**: De volgorde waarin receivers worden uitgevoerd is niet gegarandeerd. Ontwerp receivers als onafhankelijke eenheden die geen specifieke uitvoeringsvolgorde veronderstellen. > **Django 5.2 LTS** > > Django 5.2 is een Long-Term Support (LTS) release met beveiligingsupdates tot april 2028. De volledige async-ondersteuning voor middleware en signals (`Signal.asend()`, `Signal.asend_robust()`) maakt het de eerste LTS-versie die productie-async-patronen volledig ondersteunt. Voor interviewvoorbereiding is het essentieel om zowel de synchrone als de asynchrone varianten te beheersen. ## Middleware versus Signals: wanneer wat gebruiken | Aspect | Middleware | Signals | |---|---|---| | Request-/response-modificatie | Ja | Nee | | Authenticatie en autorisatie | Ja | Nee | | Model-lifecycle-hooks | Nee | Ja | | Cross-cutting concerns (logging, timing) | Ja | Mogelijk maar niet ideaal | | Ontkoppelde bedrijfslogica | Nee | Ja | | Async-ondersteuning in Django 5.2 | Volledig | Volledig (asend/asend_robust) | | Uitvoeringsbereik | Elk HTTP-verzoek | Specifieke events (save, delete, custom) | De vuistregel is helder: middleware voor alles wat met het HTTP-verzoek en -antwoord te maken heeft, signals voor alles wat reageert op modelwijzigingen of domeingebeurtenissen. In interviews wordt deze afweging regelmatig getoetst via scenario-vragen. ## Conclusie Django middleware en signals vormen twee complementaire mechanismen die elk een specifiek doel dienen binnen de architectuur van een webapplicatie. Het beheersen van beide is onmisbaar voor technische interviews en voor het bouwen van onderhoudbare productieapplicaties. - Middleware opereert op HTTP-niveau en verwerkt elk verzoek en antwoord. De volgorde in `settings.py` bepaalt de uitvoeringsvolgorde en daarmee het gedrag van de gehele stack. - Django 5.2 biedt volledige async-middleware-ondersteuning via `async_capable` en `sync_capable` klassevariabelen, waardoor non-blocking verwerking mogelijk wordt onder ASGI. - Signals implementeren het publish-subscribe-patroon voor modelgebeurtenissen en aangepaste domeinacties. Registratie vindt plaats via `AppConfig.ready()`. - `QuerySet.update()` en `QuerySet.delete()` triggeren geen model-signals, een valkuil die in vrijwel elk Django-interview aan bod komt. - Custom signals ontkoppelen bedrijfslogica volledig: de verzendende module hoeft niet te weten welke ontvangers reageren. - De keuze tussen middleware en signals hangt af van het bereik: HTTP-niveau (middleware) versus domeingebeurtenissen (signals). - Django 5.2 LTS biedt langdurige ondersteuning tot 2028 en is de eerste LTS-release met volledige async-capaciteiten voor zowel middleware als signals. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/django/django-5-2-custom-middleware-signals