Building a Django middleware (injecting data into a view's context)

In this post you'll learn how to build your own Django middleware and how to inject data into a view's context directly from the middleware.

Building a Django middleware

What is a Django middleware and what is used for?

I had an interesting use case recently where I needed to inject dynamic data into a Django view's context.

The data didn't come from the database. I needed to serve different objects depending on the request META HTTP_ACCEPT_LANGUAGE, and to make that data accessible from a JavaScript frontend.

Building a Django middleware has been the natural solution. A Django middleware is like a plug-in that you can hook into the Django's request/response cycle.

In this post you'll learn how to build your own Django middleware and how to inject data into a view's context directly from the middleware.

Setting up the project

Create a new folder for the Django project and move into it:

mkdir make-me-middleware && cd $_

Once inside create a Python virtual environment and activate it:

python3 -m venv venv
source venv/bin/activate

Next up install Django:

pip install django

and create the new Django project:

django-admin startproject make_me_middleware .

Finally create a new Django app, I'll call mine middleware_demo:

django-admin startapp middleware_demo

And now let's get to work!

Building the Django middleware

A Django middleware can live inside a Python class implementing at least two dunder methods: init and call.

In a Django middleware init is called once the server starts, while call is called for every new request to the Django application.

With this knowledge in hand create a new file called middleware.py in middleware_demo and create a new middleware named JSONTranslationMiddleware:

# file: middleware_demo/middleware.py

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

    def __call__(self, request):
        response = self.get_response(request)
        return response

You can also see that init takes get_response, while call returns the same object after taking request as a parameter.

This step is important to make the Django app work. get_response in fact will be the actual view or just another middleware in the chain.

The init method can have one-time configurations and instance variables as well, in my case I declared a Python dictionary with a couple other nested dictionaries:

# file: middleware_demo/middleware.py

class JSONTranslationMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.translations = {
            "en": {"greeting": "Hello", "header": "Welcome Django!"},
            "nl": {"greeting": "Hallo", "header": "Welkom Django!"},
        }

    def __call__(self, request):
        response = self.get_response(request)
        return response

In the next section you'll see where the magic happens ...

Template responses and middleware hooks

A middleware can have hooks, that is, class methods which intercept Django responses or views during their lifecycle.

My requirements were clear: I needed to inject self.translations into the view's context. (The real app is more complex and loads translations from multiple files).

For those new to Django, the context is any data that the view should render to the user.

Luckily the middleware offers an hook made for context manipulation: process_template_response. It takes request and response, and has access to the context through response.context_data.

Here's the implementation:

# file: middleware_demo/middleware.py

class JSONTranslationMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.translations = {
            "en": {"greeting": "Hello", "header": "Welcome Django!"},
            "nl": {"greeting": "Hallo", "header": "Welkom Django!"},
        }

    def __call__(self, request):
        response = self.get_response(request)
        return response

    def process_template_response(self, request, response):
        response.context_data["translations"] = self.translations
        return response

Since process_template_response has access to the request you can query any key on request.META.

Imagine I want to serve self.translations["nl"] only if the user has the dutch language in the Django HTTP_ACCEPT_LANGUAGE header. Here's how it would look like:

# file: middleware_demo/middleware.py

class JSONTranslationMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.translations = {
            "en": {"greeting": "Hello", "header": "Welcome Django!"},
            "nl": {"greeting": "Hallo", "header": "Welkom Django!"},
        }

    def __call__(self, request):
        response = self.get_response(request)
        return response

    def process_template_response(self, request, response):
        if "nl" in request.META["HTTP_ACCEPT_LANGUAGE"]:
            response.context_data["translations"] = self.translations
            return response
        return response

Only the sky is your limit with a middleware.

Hold tight, in the next section we'll wire up all the pieces together.

But wait ...

If you're an intermediate Django developer you might argue that a middleware is just too much, I could have checked request.META directly in the view.

But the point of having a middleware is a clear separation of concerns, plus the ability to plug the middleware in as needed.

Setting up the view and the url

Open up make_me_middleware/urls.py and include the urls from middleware_demo:

# file: make_me_middleware/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("middleware_demo.urls")),
]

Then create a new file called urls.py in middleware_demo:

# file: middleware_demo/urls.py

from django.urls import path
from .views import index

urlpatterns = [
    path("demo/", index),
]

Finally let's create a Django view with TemplateResponse:

# file: middleware_demo/views.py

from django.template.response import TemplateResponse


def index(request):
    context = {}
    return TemplateResponse(request, "middleware_demo/index.html", context=context)

This view is a bit different from a stock Django view like those you can see in the introductory tutorial.

It uses TemplateResponse, a special helper which is hooked by process_template_response from the middleware. TemplateResponse does not return any data back to the user until it reaches the middleware.

To touch the result by hand let's finally create a Django template.

Building a Django middleware: setting up the template

My Django template is an humble HTML page, yet with an interesting addition: the json_script Django filter.

Starting from any key from the context json_script creates a new script tag inside the page, with the desired id.

Create a new folder named middleware_demo/templates/middleware_demo and inside it create index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Make me a Django middleware</title>
</head>
<body>
<div id="root"></div>
</body>
{{ translations|json_script:"translations" }}

As the last step we're going to activate the middleware in a minute.

Activating the middleware

First things first open up make_me_middleware/settings.py and enable the app:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # ENABLE THE NEW APP
    'middleware_demo.apps.MiddlewareDemoConfig'
]

Then enable the middleware:

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",
    # ENABLE THE NEW MIDDLEWARE
    "middleware_demo.middleware.JSONTranslationMiddleware",
]

Keep in mind that middlewares are run from top to bottom when there is a request to the Django application and from bottom to top when the response leaves the app.

Now run the server:

python manage.py runserver

and visit the page http://127.0.0.1:8000/demo/. Open up a browser's console and you should have access to the dictionary, now a JavaScript object which has been injected by the middleware:

The JavaScript object

Now you can parse the text with JSON.parse as well as accessing any key on the object:

JSON.parse(document.getElementById('translations').textContent).en.greeting

Great job!

Conclusion

In this tutorial you learned how to create a Django middleware, how to inject data into the context, and how to use json_script in your templates.

I really hope you learned something new! Django middlewares are the right place for abstracting custom functionality that needs to alter the request/response cycle of a Django application.

Keep your Django apps clean.

Thanks for reading and stay tuned!

Resources

Django documentation for middlewares

TemplateResponse in Django

Django json_script documentation

Valentino Gagliardi

Hi! I'm Valentino! I'm a freelance consultant with a wealth of experience in the IT industry. I spent the last years as a frontend consultant, providing advice and help, coaching and training on JavaScript, testing, and software development. Let's get in touch!

More from the blog: