Django ESM
NextGen JavaScript ESM module support for Django.
Sponsors
Highlights
- 😌 easy transition
- ⚡️ smart cache busting
- 📦 no more bundling
- ☕️ native ESM support
- 📍 local vendoring with npm
Setup
Install the package:
pip install django-esm
First, add django_esm to your INSTALLED_APPS settings:
# settings.py
INSTALLED_APPS = [
# …
"django_esm", # add django_esm before staticfiles
"django.contrib.staticfiles",
]
Wrap your WSGI or ASGI application to serve the built output at /esm/ and
pass every other request to the wrapped application:
import os
from django.core.asgi import get_asgi_application
from django.core.wsgi import get_wsgi_application
from django_esm import asgi, wsgi
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
# runserver serves wsgi_app via WSGI_APPLICATION; ASGI servers serve asgi_app.
wsgi_app = wsgi.ESM(get_wsgi_application())
asgi_app = asgi.ESM(get_asgi_application())
Finally, add the import map to your base template:
<!-- base.html -->
<!DOCTYPE html>
{% load esm %}
<html lang="en">
<head>
<script type="importmap">{% importmap %}</script>
<title>Django ESM is awesome!</title>
</head>
</html>
That's it!
Development
Rebuild STATIC_DIR on every change:
python manage.py esm --watch
Run the development server in another terminal:
python manage.py runserver
Treeshaking
By default, every package entry point
ends up in the import map.
Pass --treeshake to drop entry points that are not reachable from your
project's own entry points, keeping only the outputs actually imported:
python manage.py esm --watch --treeshake
Usage
You can now import JavaScript modules in your Django templates:
<!-- index.html -->
{% block content %}
<script type="module">
import "lit"
</script>
{% endblock %}
Script tags
Use the esm tag to load a module from the import map as a script tag, with its
resolved URL and integrity hash:
{% load esm %}
{% esm "htmx.org" %}
<script type="module" src="/esm/node_modules/htmx.org/dist/htmx.min-<hash>.js" integrity="sha256-… sha384-… sha512-…"></script>
The name must be a key of the import map, that is, a package name or a private module.
Form.media
To use your import map in Django forms, you can use the Form.media attribute:
# forms.py
from django import forms
from django_esm.forms import ESM
class MyForm(forms.Form):
name = forms.CharField()
class Media:
js = [ESM("@sentry/browser")]
Now {{ form.media.js }} will render like this:
<script type="module" src="/esm/node_modules/@sentry/browser/build/bundle-<hash>.js" integrity="sha256-… sha384-… sha512-…"></script>
Private modules
You can also import private modules from your Django app:
<!-- index.html -->
{% block content %}
<script type="module">
import "#myapp/js/my-module.js"
</script>
{% endblock %}
To import a private module, prefix the module name with #.
You need to define your private modules in your package.json file:
{
"imports": {
"#myapp/script": "./myapp/static/js/script.js",
// You may use trailing stars to import all files in a directory.
"#myapp/*": "./myapp/static/js/*"
}
}
How it works
Django ESM works via native JavaScript module support in modern browsers. It uses the import map to map module names to their location on the server.
Here is an example import map. Entries resolve under /esm/ and use
content-hashed names:
{
"imports": {
"htmx.org": "/esm/node_modules/htmx.org/dist/htmx.min-<hash>.js"
}
}
Metadata
Release files for django-esm 0.12.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_esm-0.12.0.tar.gz | 9.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_esm-0.12.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 21.4 kB
Release files / django_esm-0.12.0.tar.gz
| Download URL | django_esm-0.12.0.tar.gz |
|---|---|
| Size | 9.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9b881a5e79492b5ee3f81d785429cf27000a1bf88a91571277e3af79f0dd6ecf
|
|
BLAKE2b-256 checksum How to use checksums |
eccc45e3d46ada873a38967785fe7891cb2c696d055fbfcc7698bd54acd3ea9b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.
Transparency logRelease files / django_esm-0.12.0-py3-none-any.whl
| Download URL | django_esm-0.12.0-py3-none-any.whl |
|---|---|
| Size | 11.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
03a692f56d857ba21dc42385a01db23b36516c5f3dbd1b907a04ec9936cb9557
|
|
BLAKE2b-256 checksum How to use checksums |
08224a82994f60eb4946c8e743e24e86d25baaa75548860f1d67a96d6b914250
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.
Transparency log