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.
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>
The file sits under polls/static/polls/js/ so it’s namespaced to
the polls app (see How to manage static files (e.g. images, JavaScript, CSS)). You could also add
the file to a folder in STATICFILES_DIRS.
The defer attribute tells the browser to run the script after the HTML
is parsed; see MDN.
If your site sends a Content-Security-Policy, add the
csp_nonce_attr template tag so the script is permitted (see
How to use Django’s Content Security Policy).
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.
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.
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.
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.
Jun 21, 2026