How to get Django and JavaScript to work together

There tend to be two broad problems to solve when working with Django and JavaScript:

  • getting your JavaScript to the browser

  • communicating between your Django and JavaScript code

This guide shows how Django and its ecosystem can help with both. We’ll build on the polls application from the tutorial in the examples that follow.

How to add JavaScript to your pages

The basic building block is the static template tag for referencing your JavaScript files. For example, to add JavaScript to the polls app:

<script src="{% static 'polls/js/vote.js' %}" defer></script>

As your own JavaScript grows, split it across files using native ES modules. Use import/export and load the entry point with type="module":

<script type="module" src="{% static 'polls/js/main.js' %}"></script>

Note

To rewrite the hashed file names referenced in import statements, override ManifestStaticFilesStorage with a subclass that sets support_js_module_import_aggregation to True or map the names yourself with an importmap.

You can load third-party JavaScript straight from a CDN, or you can manually download and copy it to your static files.

To manage more dependencies and keep them updated, use a package manager. npm is the main registry for JavaScript and there are several package managers that work off a list of packages in a package.json file and download them to a folder, typically node_modules. You will need to link this to static files:

  • To deliver library files unchanged, let static files read from node_modules; the ecosystem provides packages that help with this.

  • Or, more commonly, to combine, convert, or tree-shake JavaScript (including your own), use a JavaScript build tool. The output of the build tool should go to a folder that is part of the static files tree.

Note

Many build tools also provide a development server with features like Hot Module Reload (HMR), allowing your front-end to update as you change your development files. To integrate this with Django, search for django <build tool> to find a community package that supports your setup.

How to communicate between Django and JavaScript

How to update part of a page without a full reload

You can use JavaScript to make requests to a Django view and handle its responses. Consider the following update to the polls tutorial.

polls/templates/polls/results.html
<h1>{{ question.question_text }}</h1>

{% partialdef results-list inline %}
<ul id="results">
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>
{% endpartialdef %}
polls/templates/polls/detail.html
{# Form as in the tutorial (with id="vote-form"), plus load static and: #}
<div id="results"></div>
<script src="{% static 'polls/js/vote.js' %}" defer></script>
polls/static/polls/js/vote.js
const form = document.getElementById("vote-form");
form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const response = await fetch(form.action, {
        method: "POST",
        headers: {
            "X-CSRFToken": getCookie("csrftoken"),
            "X-Requested-With": "XMLHttpRequest",
        },
        mode: "same-origin",
        body: new FormData(form),
    });
    document.getElementById("results").outerHTML = await response.text();
});

The getCookie() helper is the one from the CSRF documentation.

polls/views.py
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST["choice"])
    except (KeyError, Choice.DoesNotExist):
        return render(request, "polls/detail.html", {...})  # as in the tutorial
    selected_choice.votes = F("votes") + 1
    selected_choice.save()
    if request.headers.get("X-Requested-With") == "XMLHttpRequest":
        # fetch() submission: return the updated fragment, no redirect needed
        # (no navigation occurs, so there's no back-button resubmission to guard
        # against).
        return render(
            request, "polls/results.html#results-list", {"question": question}
        )
    # Plain form submission: Post/Redirect/Get, as in the tutorial.
    return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))

This technique uses partials from the Django templating engine to send only the body of the results page as the response. JavaScript overrides the form submission and sends it to the same view, but with an additional identifier the view can use to choose a response path.

This pattern is common enough that several JavaScript libraries handle the request and partial swap logic for you. Some even let you declare the behavior with HTML attributes, saving you from writing any JavaScript.

How to mount a JavaScript component

Another common pattern is for the JavaScript to take over rendering a section of the page.

polls/templates/polls/detail.html
{{ results_data|json_script:"results-data" }}
<div id="results-widget"></div>
<script src="{% static 'polls/js/results-widget.js' %}" defer></script>

The json_script filter safely outputs the data for the JavaScript to read.

polls/static/polls/js/results-widget.js
const data = JSON.parse(document.getElementById("results-data").textContent);
const root = document.getElementById("results-widget");

function render({question, choices}) {
    root.innerHTML = `<h1>${question}</h1><ul>` +
        choices.map((c) =>
            `<li><button data-choice="${c.id}">${c.text}</button> -- ${c.votes}</li>`
        ).join("") +
        `</ul>`;
}

async function vote(choiceId) {
    const response = await fetch(data.voteUrl, {
        method: "POST",
        headers: {
            "X-CSRFToken": getCookie("csrftoken"),
            "Content-Type": "application/json",
        },
        mode: "same-origin",
        body: JSON.stringify({choice: choiceId}),
    });
    render(await response.json());
}

// Delegate on root, which survives each render() replacing its contents.
root.addEventListener("click", (event) => {
    const button = event.target.closest("button[data-choice]");
    if (button) {
        vote(button.dataset.choice);
    }
});

render(data);

The details page continues to be served by a Django view. The view is reworked to pass the results_data object into the context for the JavaScript widget to render instead of the question. The vote view is changed to a JSON endpoint using JsonResponse, which updates the vote and returns the new state.

polls/views.py
import json

from django.http import JsonResponse
from django.urls import reverse
from django.views.decorators.http import require_POST


def results_data(question):
    return {
        "question": question.question_text,
        "voteUrl": reverse("polls:vote_json", args=(question.id,)),
        "choices": [
            {"id": c.id, "text": c.choice_text, "votes": c.votes}
            for c in question.choice_set.all()
        ],
    }


def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(
        request, "polls/detail.html", {"results_data": results_data(question)}
    )


@require_POST
def vote_json(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        choice_id = json.loads(request.body)["choice"]
        selected_choice = question.choice_set.get(pk=choice_id)
    except (json.JSONDecodeError, KeyError, Choice.DoesNotExist):
        return JsonResponse({"error": "Choice does not exist."}, status=400)
    selected_choice.votes = F("votes") + 1
    selected_choice.save()
    return JsonResponse(results_data(question))
polls/urls.py
# Added to the tutorial's urlpatterns.
path("api/<int:question_id>/vote/", views.vote_json, name="vote_json"),

As the data you are passing gets more detailed, you can look to the ecosystem for a third-party library to help build an API for your models.

How to serve a JavaScript front-end against a Django API

If the front-end is rendered entirely in JavaScript (a single-page app), with Django serving the API, you can still serve the entry view via Django. Add a catch-all URL pattern with re_path() pointing at a TemplateView for the client-side routing, and place it last so it doesn’t capture your API or admin:

urls.py
re_path(r"^.*$", TemplateView.as_view(template_name="index.html")),

As before, place the JavaScript build tool’s output somewhere staticfiles can collect, and use the ecosystem to enable HMR in development.

Alternatively, you can use the serving infrastructure of your SPA framework, in which case you will get HMR for free. You’ll have to adjust how you pass the csrftoken though:

  • Set it on the first view the app calls with ensure_csrf_cookie().

  • Read the token from the cookie on each request since it rotates on login.

The bigger issue then is how to deal with two origins, localhost:8000 and localhost:9000, in development. By default, browsers won’t let JavaScript read from a different origin and CSRF won’t work out of the box across two origins either. There are two possible solutions:

  • proxy requests so they come from one origin (your JavaScript framework’s serving infrastructure can typically proxy API requests to your Django development server)

  • configure your system to handle two different origins

It’s a good idea to pick the one closest to your production setup. In production you can still serve everything from the same origin, either by using Django as above or configuring one reverse proxy for both Django and JavaScript.

If you do need to serve the front-end from a different origin, then you will need to configure two things: CORS and CSRF.

For CORS, set the appropriate headers on your Django responses. Typically you’d do this with a middleware and there are packages in the ecosystem that help with this.

Access-Control-Allow-Origin: http://localhost:9000
Access-Control-Allow-Credentials: true

Then configure CSRF for the two origins:

  • Configure CSRF_TRUSTED_ORIGINS = ["http://localhost:9000"].

  • In a production setup, with app.example.com and api.example.com, add CSRF_COOKIE_DOMAIN = ".example.com" too.

  • Each JavaScript fetch must send credentials: "include".

The above configuration also suffices for authentication across the two origins. The session cookies require those CORS headers and the fetch credentials.

Warning

If you are using different domains, not just different subdomains, this all becomes increasingly unworkable. The browser treats the cookies as third-party, and the security restrictions placed on them make it difficult to keep using Django’s session-based auth. The alternative is token auth, which you may already be using for any mobile app that consumes the API. Your API package will support it, but token auth shifts more security responsibility onto you than the default session cookies do, so read its documentation closely.