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.

Django 5.2 custom middleware en signaalverwerking tutorial

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.

Klaar om je Django gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

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.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

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.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

#django
#python
#middleware
#signals
#interview

Delen

Gerelateerde artikelen