Getting VueJS 3 to work nicely with Django

Most of the tutorials for using Django and VueJS assume that you want to completely remove Django views and instead just use VueJS to serve the entire frontend.

My usecase are usually different - I mainly find that I want to use VueJS sparingly to provide some enhancement to a particular page, serving all the pages in Django through the usual way.

I've not found a good tutorial for doing this, so I thought I would write my own. This tutorial assumes:

  • you know your way around Django and VueJS, and just want to connect them.
  • You've already got a working Django site that you want to add to.
  • You've got NPM installed to add VueJS.

There's also a couple of requirements that are specific to how I like to set up Django, but these are optional. In particular:

  • This is based on VueJS 3 (specifically )
  • I prefer to use Jinja2 templates rather than Django's built in templating
  • I use Whitenoise to serve static files
  • I use Dokku for app deployment (the tips here may also work with Heroku)

Edit: 2024-06-19 I've changed some of the details below based on the experience of deploying to dokku

Initial setup

1. Install django-vite

django-vite provides some useful template tags to automatically find the scripts to add to a page.

  1. Run pip install django-vite
  2. Add django_vite to INSTALLED_APPS in settings.py

2. Create the vue app in your directory

npm create vite@latest frontend -- --template vue

This creates a directory called ./frontend/ in the root directory of your django project. If you want to call it something different you can change frontend in the command.

Move the package.json file from ./frontend/package.json to ./package.json (ie into the root directory of your project). Change the three scripts created in package.json so they use the frontend/vite.config.js config file. This will look like:

...
"scripts": {
    "dev": "vite -c frontend/vite.config.js",
    "build": "vite build -c frontend/vite.config.js",
    "preview": "vite preview -c frontend/vite.config.js"
},
...

Delete the ./frontend/index.html file that's created here - we'll be inserting the scripts into our django templates so we don't need this file. You might also want to delete more of the sample files that are created at this stage.

3. Update settings.py again

We need to tell django where to look for the static files. Add a value to your STATICFILES_DIRS setting:

STATICFILES_DIRS = [
    # ...existing dirs (probably BASE_DIR/static)
    os.path.join(BASE_DIR, "frontend", "dist"),
    # or
    BASE_DIR / "frontend" / "dist"
]

Also add a setting called DJANGO_VITE

DJANGO_VITE = {
    "default": {
        "dev_mode": DEBUG,
        "manifest_path": os.path.join(BASE_DIR, "frontend", "dist", "manifest.json"),
    },
}

This will use the existing DEBUG setting to work out whether django vite operates in dev mode or production mode.

It also adds a path telling Django Vite where to find the manifest for production deployment. This will facilitate the building of the static assets in production.

In dev mode you must be running a static server using npm run dev. This will run a server at http://localhost:5173/static/ which serves the files.

In production mode, you must have generated the files using npm run build and then run python manage.py collectstatic to find the files. This will make sure that the files are added to your staticfiles directory.

If you're using Whitenoise to serve your static files, you may also need to add a new test that takes into account the URL format generated by Vite. This is described in the django-vite documentation. I added the snippet to settings.py.

4. Update frontend/vite.config.js

The vite.config.js file tells vite both which files to build and where to put the outputs. This should look like:

import { defineConfig } from "vite";
import { resolve } from "path";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  base: "/static/",
  plugins: [vue()],
  build: {
    manifest: "manifest.json",
    outDir: resolve("./frontend/dist"),
    rollupOptions: {
      input: {
        main: resolve("./frontend/src/main.js"),
      },
    },
  },
});

This sets the following options:

  • base: "/static/" - this makes sure that the vite development server serves files from /static/ url, as this is where they will be served in production.
  • manifest: "manifest.json" - the manifest file shows where all the outputs can be found
  • outDir: resolve("./frontend/dist") - this sets the directory where outputs are stored to /frontend/dist/. This is the same as is defined in settings.py above. I chose this directory because it's already excluded from git by the default .gitignore.
  • main: resolve("./frontend/src/main.js") - this is an example of an entrypoint for an app. This will create an app called "main" using the script found at /frontend/src/main.js. You can create one of these entries for each entrypoint within your app.

4. [optional] Set up Jinja to use the django_vite template tags

I prefer to use Jinja over the default Django templates. This extra step is needed to make the django-vite template tags available to Jinja templates.

I've set up a Jinja2 environment as described in the Django docs so I add the django-vite template tags to the global environment there.

Show how to add django_vite tags to jinja global environment
from django_vite.templatetags.django_vite import (
    vite_asset,
    vite_asset_url,
    vite_hmr_client,
    vite_legacy_asset,
    vite_legacy_polyfills,
    vite_preload_asset,
    vite_react_refresh,
)

from jinja2 import Environment


def environment(**options):
    env = Environment(**options)

    env.globals.update(
        {
            # ... any other globals
            "vite_hmr_client": vite_hmr_client,
            "vite_asset": vite_asset,
            "vite_preload_asset": vite_preload_asset,
            "vite_asset_url": vite_asset_url,
            "vite_legacy_polyfills": vite_legacy_polyfills,
            "vite_legacy_asset": vite_legacy_asset,
            "vite_react_refresh": vite_react_refresh,
        }
    )
    env.filters.update(
        {
            # ... any filters
        }
    )
    return env

Adding a frontend app

This setup allows you to serve different VueJS apps from different pages in your app. To add a VueJS app to a page, you need to do the following:

1. Create a VueJS app in the frontend/src directory

This should look something like the /frontend/src/main.js script, e.g. it should mount a VueJS component to a particular HTML element, usually identified by its ID. A calculator app saved in calculator.js might look something like:

import { createApp } from "vue";
import CalculatorApp from "./CalculatorApp.vue";
import "vite/modulepreload-polyfill";

createApp(CalculatorApp).mount("#calculator");

2. Add the new app to vite.config.js

Within your existing vite.config.js file add another entry to build.rollupOptions.input, alongside any existing entries:

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: resolve("./src/main.js"),
        calculator: resolve("./src/calculator.js"),
      },
    },
  },
});

3. Insert the template tags into your templates

As I mentioned above, I use Jinja2 for compiling templates. My base templates tend to have a headscripts block which is where <script> tags go. So my template has a block like this:

{% block headscripts %}
{{ super() }}
{{ vite_hmr_client() }}
{{ vite_asset('frontend/src/calculator.js') }}
{% endblock %}

The vite_hmr_client() call creates script tags in development mode to load the development script. And the vite_asset('frontend/src/calculator.js') call loads the actual calculator script.

In the django template language it would look more like:

{% load django_vite %}

{% block headscripts %}
{{ block.super }}
{% vite_hmr_client %}
{% vite_asset 'frontend/src/calculator.js' %}
{% endblock %}

You'll also need to at this point add a <div id="calculator"></div> tag to your template to provide somewhere for your app to live.

Serving in development mode

When developing your site locally, you'll need to run two different servers.

I usually use django's built in development server by running python manage.py runserver.

You'll also need to run the vite server by running npm run dev.

Serving the files in production mode

When your site is in production, the built files will need to be available for the static file storage engine to use. You can build the files by running npm run build.

This will create the files in /frontend/dist/. By default this folder won't be included in Git, so if you're using something like Heroku or Dokku to deploy, then these files won't make it into your staticfiles. There are two ways to achieve this:

You can add a nodejs buildpack, which will install the needed modules and build your files to the right place. To do this add a file called .buildpacks in the root directory of your app. This file should contain two lines:

https://github.com/heroku/heroku-buildpack-nodejs.git
https://github.com/heroku/heroku-buildpack-python.git

When you deploy your app to dokku/heroku, it will then install the needed packages from package.json and then automatically run the npm run build command. These files will then be available for django's collectstatic process.

2. Manually build files every time you deploy

Or you can unignore the /frontend/dist folder from git (or use a different folder to output) and make sure you build the files every time you deploy.

Appendix I - example file structure

This shows an indicative file structure of the key files that need to be in the right place.

- config/   # django configuration directory
  - admin.py
  - apps.py
  - asgi.py
  - jinja2.py
  - settings.py   # main django settings file
  - urls.py
  - wsgi.py
- frontend/
  - dist/   # created by vite build - should be git ignored
    - assets/
      - main-1a2b3c4e.js   # compiled main JS
    - manifest.json
  - src/
    - components/
      - Calculator.vue
      - ... any other files
    - calculator.js
  - vite.config.js   # where vite configuration lives
- my-app/   # normal django app
  - migrations/
  - models.py
  - views.py
  - ...etc
- static/
  - ... other static files like CSS, images, etc
- manage.py