Seamless Vite integration for Django. Get lightning-fast HMR during development and optimized builds for production.
pip install django-vite-plugin
npm install django-vite-pluginAdd the app to INSTALLED_APPS in settings.py:
INSTALLED_APPS = [
# ...
'django_vite_plugin',
]Create vite.config.js:
import { defineConfig } from 'vite'
import { djangoVitePlugin } from 'django-vite-plugin'
export default defineConfig({
plugins: [
djangoVitePlugin([
'myapp/js/main.js',
'myapp/css/styles.css',
])
],
}){% load vite %}
<!DOCTYPE html>
<html>
<head>
{% vite %}
{% vite 'myapp/js/main.js' 'myapp/css/styles.css' %}
</head>
<body>
<!-- Your content -->
</body>
</html># Terminal 1
python manage.py runserver
# Terminal 2
npm run devFor production, run npm run build.
Django recommends placing static files in app_name/static/app_name/. This plugin simplifies paths automatically:
<!-- Instead of this -->
{% vite 'myapp/static/myapp/js/main.js' %}
<!-- Write this -->
{% vite 'myapp/js/main.js' %}The plugin resolves myapp/js/main.js to myapp/static/myapp/js/main.js using Django's static file finders.
| Input Path | Resolved Path |
|---|---|
myapp/script.js |
myapp/static/myapp/script.js |
myapp/static/script.js |
myapp/static/script.js |
static/script.js |
static/script.js |
The plugin provides convenient import aliases for JavaScript:
| Alias | Resolves To |
|---|---|
@ |
Project root |
@s:myapp |
myapp/static/myapp/ |
@t:myapp |
myapp/templates/myapp/ |
// Import from your app's static folder
import utils from '@s:myapp/js/utils.js'
// Import from templates (useful for React/Vue components)
import App from '@t:myapp/App.jsx'Aliases are named after the app's label, so a nested app such as apps.blog
is @s:blog. Only apps inside BASE_DIR get one — installed third-party apps
are not aliased.
The browser automatically reloads when .html or .py files change. No configuration required.
<head>
{% vite 'react' 'myapp/js/main.jsx' %}
</head>Remember to add @vitejs/plugin-react to your Vite config.
Standard Vite plugins work as expected. Add them to your vite.config.js alongside djangoVitePlugin.
All settings are optional. Add to settings.py:
DJANGO_VITE_PLUGIN = {
# Use Vite dev server (default: DEBUG)
'DEV_MODE': True,
# Build output directory (default: STATIC_ROOT or 'static').
# A relative path is resolved against BASE_DIR, never the working
# directory, and Vite is given the resolved path.
'BUILD_DIR': 'static',
# Where Vite writes its manifest (default: BUILD_DIR/.vite/manifest.json).
# A relative path is resolved against BASE_DIR. Only needed if
# 'build.manifest' in vite.config.js names a file of its own; the plugin
# warns during a build when the two part company.
'MANIFEST': 'static/.vite/manifest.json',
# URL prefix for built assets (default: STATIC_URL).
# Set this or STATIC_URL. Required to build; dev mode never serves a
# built asset and does not ask for one.
'BUILD_URL_PREFIX': '/static/',
# Enable static file path resolution (default: True)
'STATIC_LOOKUP': True,
# Default attributes for script tags
'JS_ATTRS': {
'type': 'module',
},
# Script attributes for production builds only
'JS_ATTRS_BUILD': {
'type': 'module',
'defer': True,
},
# Default attributes for stylesheet links
'CSS_ATTRS': {
'rel': 'stylesheet',
'type': 'text/css',
},
}djangoVitePlugin({
// Entry points (required)
input: ['myapp/js/main.js'],
// Django project root (where manage.py is), relative to the directory
// vite runs from - usually the one holding vite.config.js
root: '..',
// Write aliases to jsconfig.json/tsconfig.json (default: true if file exists)
addAliases: true,
// Python executable path
pyPath: 'python',
// Additional args for manage.py commands
pyArgs: [],
// Auto-reload on file changes (default: true)
reloader: true,
// Or provide a custom filter
reloader: (file) => file.endsWith('.html'),
// Additional files to watch
watch: ['templates/**/*.html'],
// Reload delay in ms (default: 3000)
delay: 3000,
}){% load vite %}
<!-- Load Vite client (required for HMR in development) -->
{% vite %}
<!-- Load assets -->
{% vite 'myapp/js/main.js' %}
{% vite 'myapp/css/styles.css' %}
<!-- Load multiple assets -->
{% vite 'myapp/js/main.js' 'myapp/css/styles.css' %}{% vite 'myapp/js/main.js' crossorigin='anonymous' data-turbo-track='reload' %}Output:
<script src="..." type="module" crossorigin="anonymous" data-turbo-track="reload"></script>True writes an attribute on its own, and False (or None) leaves it
out — which is also how you drop one of the JS_ATTRS/CSS_ATTRS
defaults, in a tag or in the settings:
{% vite 'myapp/js/main.js' defer=True type=False %}<script defer src="..."></script>HTML has no falsy attribute value — defer="false" still defers — so
leaving the attribute out is the only way to say no. Quote it if you
want the word itself: data-x='false'.
The attributes apply to the assets named in the tag. In a production
build, a JS entry may pull in additional <link> tags for the
stylesheets it imports — those always use the CSS_ATTRS defaults, not
the tag's attributes. To customize a stylesheet's attributes, name the
stylesheet in a {% vite %} tag itself.
{% vite app_name|add:'/js/main.js' %}- Run
npm run build - Run
python manage.py collectstatic - Set
DEBUG = False(or explicitly setDEV_MODE: False)
BUILD_DIR is a directory Django owns — it defaults to STATIC_ROOT, and it
holds hand-written assets and whatever collectstatic put there. So the plugin
leaves it alone rather than emptying it before a build, which is what Vite does
by default with an output directory inside its root. Old hashed files therefore
accumulate; if the directory is Vite's alone, ask for the sweep explicitly:
export default defineConfig({
build: { emptyOutDir: true },
plugins: [djangoVitePlugin({ input: [...] })],
})DJANGO_VITE_PLUGIN = {
'DEV_MODE': False,
'BUILD_URL_PREFIX': 'https://cdn.example.com/static/',
}The manifest file must remain accessible locally at BUILD_DIR/.vite/manifest.json.
BUILD_URL_PREFIX is where Vite's base comes from — the plugin sets it, and a
base of your own in vite.config.js is overridden and warned about. Django
renders asset URLs from that same prefix, so the two cannot disagree.
-
Add the URL pattern to
urls.py:urlpatterns = [ # ... path('', include('django_vite_plugin.urls')), ]
-
Configure settings:
DEBUG = True STATICFILES_DIRS = [BASE_DIR / 'build'] DJANGO_VITE_PLUGIN = { 'DEV_MODE': False, 'BUILD_DIR': 'build', }
DEBUGmust stayTrue. Django never serves static files withDEBUG = False, so these URL patterns are inactive in that case and the built assets will 404. OnlyDEV_MODEneeds to be turned off to load the build instead of the dev server.Listing
BUILD_DIRinSTATICFILES_DIRSmatters:runserveranswers every URL underSTATIC_URLitself and 404s anything the static finders do not know, before these patterns get a chance. If the build lives somewhere the finders do not look -STATIC_ROOT, typically - start the server withrunserver --nostaticinstead. -
Run
npm run buildand start Django.
myproject/
├── myapp/
│ ├── static/
│ │ └── myapp/
│ │ ├── css/
│ │ │ └── styles.css
│ │ └── js/
│ │ └── main.js
│ └── templates/
│ └── myapp/
│ └── index.html
├── manage.py
├── package.json
└── vite.config.js
myproject/
├── myapp/
│ └── static/myapp/...
├── frontend/
│ ├── package.json
│ └── vite.config.js # Set root: '..'
└── manage.py
// frontend/vite.config.js
djangoVitePlugin({
input: ['myapp/js/main.js'],
root: '..',
})Vite's own root follows root here, so it stays the Django project and the
entry paths above are the usual app-relative ones.
If you set Vite's root as well, the two part company: STATIC_LOOKUP names
assets relative to BASE_DIR, while Vite serves them — and keys the manifest —
relative to its own root. Turn the lookup off and name entries the way Vite sees
them:
// frontend/vite.config.js — Vite's root stays in frontend/
export default defineConfig({
root: './',
plugins: [djangoVitePlugin({ input: ['src/main.ts'], root: '..' })],
})DJANGO_VITE_PLUGIN = {
'STATIC_LOOKUP': False,
}The plugin warns at startup when it sees the roots disagree with the lookup
still on. See example/svelte-in-different-dir for the whole layout.
Whenever Vite reads its config — npm run dev and npm run build alike — the
plugin writes the app aliases into your JavaScript/TypeScript config, so the IDE
resolves @s:appname and @t:appname imports the way the bundler does.
It looks in three directories: Vite's root, the directory Vite runs from, and the
Django root. Usually those are one and the same, but a frontend in a subdirectory
has a config file at each end and both of them need the aliases. In each
directory the first of tsconfig.app.json, tsconfig.json and jsconfig.json
that exists is the one written to. Comments, trailing commas and paths of your
own are kept, an alias you define yourself in resolve.alias is left to your
definition, and a file that does not parse is reported rather than overwritten.
To also have a jsconfig.json created when the project has none of them — it
lands at the Django root:
djangoVitePlugin({
input: [...],
addAliases: true,
})addAliases: false writes nothing at all.
- Ensure both Django and Vite dev servers are running
- Check that
DEV_MODEisTrue(orDEBUG = True) - Verify the Vite server is accessible at
http://localhost:5173 - Assets from a
STATICFILES_DIRSentry outsideBASE_DIRare loaded through Vite's/@fs/route, since Vite only serves what is under its root. If Vite answers those with "is outside of Vite serving allow list", add the directory toserver.fs.allow
Pointing browsers at a reachable address is only the first of three gates:
export default defineConfig({
server: {
host: true,
// Where other devices reach this machine
hmr: { host: '192.168.1.5' },
// Where Django serves the page; since Vite 6.0.9 anything but
// localhost is refused by default, and the browser blocks the assets
cors: { origin: 'http://192.168.1.5:8000' },
// Only needed when the address above is a name rather than an IP
allowedHosts: ['dev.example.test'],
},
plugins: [djangoVitePlugin({ input: [...] })],
})A missing cors shows up as a CORS error in the browser console while Vite
logs nothing; a missing allowedHosts shows up as Vite's own "Blocked
request. This host is not allowed."
- Run
npm run buildbefore deploying - Ensure
BUILD_DIRmatches your Vite output directory - Check that the manifest exists at
BUILD_DIR/.vite/manifest.json
- Run
npm run devornpm run buildto updatejsconfig.json/tsconfig.json - If the project has no such file, set
addAliases: trueto have one created - Restart your IDE after alias generation
MIT License. See LICENSE for details.
- PyPI Package
- npm Package
- Vite Documentation
- Buy Me a Coffee - Support the maintainer