Django + Vue + Vite: REST Not Required

Mike Hoolehan
11 min readOct 3, 2023

--

Frequently I read posts from Django devs struggling to choose the best way to bring JavaScript to their Django project. There is often a reluctance to use heavyweight build-based JavaScript frameworks, as it (seemingly) requires a REST backend and, with it, the abandonment of Django templates. For many devs though, Django templates, with their ease and tight pairing to Django mechanisms (ORM, middleware, caching, etc.), are just too useful to abandon. And so, for many, the price of using Django templates is the use of a lightweight script-based JavaScript framework as a substitute for a full-weight JavaScript framework.

While there are absolutely situations in which these lightweight frameworks excel, they simply cannot match the optimization, modularity, testability, rich package ecosystem, and developer experience offered by modern build-based JavaScript frameworks.

But are we forced to choose between the convenience of Django templates and the power of a full JavaScript framework? Or is there an alternative; one where adding a simple field to a page doesn’t mean asking our back-end devs to build a new REST endpoint, but where we also offer our front-end developers the power to build components using the modern JavaScript ecosystem to which they are accustomed?

Is it possible to have the best of both front-ends?

You bet.

Note: You can quickly view these techniques in action with the Django + Vue + Vite Cookiecutter. If you’re attending DjangoCon US 2023, don’t miss my presentation Vue + Django: Combining Django Templates and Vue Single File Components without compromise.

Templates + Vue: The Best of Both Front-Ends

I’ll describe a unique approach to combining Vue and Django that dispels the myth that a build-based “heavyweight” JavaScript cannot coexist with Django templates. I have used this approach in production for over 4 years. In those years, Vue has evolved a great deal, dropping Webpack in lieu of Vite, exchanging Vuex for Pinia, and adopting a new streamlined composition-based API. These changes have, in my opinion, made Vue even easier to integrate with Django templates and strengthened the case that Vue and Django are a superb pair.

Quick Start

Start with a simple Django project or your own existing project, if you have one. Begin integrating a Vue front-end by using the create-vue scaffold to create the starter Vue project. I run this from my Django project directory, such that my Vue code will exist in a subdirectory of my Django project, but you’re welcome to place it anywhere.

npm create vue@latest -- --pinia # scaffold project with pinia support

Provide your desired project name, e.g. vue-frontend. The scaffolded project will be created. As instructed, run the following:

cd vue-frontend
npm install
npm run dev

The Vite dev server will launch and you can view Vue’s welcome app in your browser. Of course, at this point it’s not integrated into Django. Let’s remedy that.

A First Vue Component in a Django Template

You can integrate your first Vue component with only two lines of code.

Choose a template in your Django app, say home.html, into which you wish to inject the welcome app. In that template, create a div with id app that will serve as container for the welcome app:

<div id="app"></div>

Next, on the same template, load the welcome app’s module JavaScript:

<script type="module" crossorigin src="http://localhost:5173/src/main.js"></script>

If you have a block in your Django template for late-loading scripts, that would be an ideal place to insert this. However, it will work nearly anywhere on the page.

Start the Django dev server (e.g. python manage.py runserver), then navigate in your browser to your view.

It’s that easy; your first Vue component is embedded in a Django view.

Notice embedding in Django doesn’t lose us any benefits of Vue’s dev tooling. Try making a change to your Vue code, for example, in vue_frontend/src/App.vue, altering the msg in <HelloWorld msg="You did it!" /> to your message of choice. Save the file and look again in your browser. Vue’s Hot Module Replacement (HMR) near instantly reflects your changes without any need for page reload.

Also, if you have the very useful Vue devtools extension, open your browser’s developer console, navigate to the “Vue” tab, and explore your Vue components’ metadata and state within your browser.

Development vs Production Builds

You’ll notice the script tag we added above is referencing the locally running Vite devserver, which of course will only be running during development. How then do we handle production builds?

When Vite builds for production, it will compile the project into optimized production-ready JavaScript files. We simply need to instruct Vite to output those files into a Django static directory, after which we can treat them as we would any other static file.

To do this, add the following rollupOptions to the build section in vite.config.js, adjusting the output directory as needed to point to your Django static directory:

build: {
rollupOptions: {
input: {
main: resolve('./src/main.js'),
},
output: {
dir: '../<your_django_app_dir>/static/vue/',
entryFileNames: '[name].js',
},
},
},

Run npm run build to create a production build and notice files (one each for your entrypoints, currently only main.js) are output to your Django static directory. For deployments then, we ensure this build command is included as part of the app build process, e.g. just before the collectstatic management command.

Then, to reference production files, alter the script tag in your template to

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

Reload and you’ll again see the welcome app, but now with your production JavaScript resources loaded from your static directory.

Of course, it’s not feasible to alter template code every time we switch between development and production environments, so the template should select the proper source based on some setting (e.g. DEBUG) or environment variable. I use a template tag to simplify switching between devserver and static URLs based on a setting.

Mounting Elements

You may be wondering the above example how Vue knew to mount itself into the div with id app in the template. The answer is found in vue-frontend/src/main.js which is an “entrypoint” included in the scaffolded Vue project. You’ll see in this file the lines

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

In the first line, the root component (App in this case) is specified. The second line indicates that component will be utilizing a Pinia store. The final line mounts this root component to the div identified by #app.

We use such entrypoints to instruct Vue how to insert itself into our Django template views.

Note that we’re not restricted to creating and mounting only a single component, nor even to mounting those components to single elements. Entrypoints can include numerous root components and we can mount those to as many elements as we wish, mounting them by class name or attribute.

We are also not constrained to a single entrypoint. Organize and use entrypoints however best suits your app: perhaps one entrypoint per view, one per group of similarly related components, or even one per component. Your Django template can include as many entrypoints as you wish.

If you want to mount elements on every page of your app, then import your entrypoint on a base template. Otherwise, import as needed on individual templates.

A Blueprint for Vue + Django integration

We’ve covered the essential elements of a single integration of a Vue component into a Django Template. We now have a general blueprint for integrating components:

  1. Decide which parts will be Template-based and which Vue-based
    — Create Vue components accordingly
    — Create Django Templates accordingly
  2. Create a Vue entrypoint (or adapt an existing one)
    — Specify root components (and stores if needed)
    — Mount root components to Django Template elements
  3. Create a Django template (or adapt an existing one)
    — Add container elements for Vue components
    — Pass needed Vue props as data attributes
    — Import Vue entrypoint

Note that, generally, the development of Vue and Django Template can occur independently. Only the integration requires coordination:

  • Vue team provides the Django team: entry point name, component names, and any needed component props
  • Django team provides the Vue team: selectors for container elements

A Sample App

I’ve created a Vue3 + Vite + Django Cookiecutter with which you can bootstrap your own starter project. If you’re starting a new project, I highly recommend beginning here as you not only get all the substantial benefits of the base Django Cookiecutter, but also a sample “Rewards” app demonstrating all the techniques in this article. Create the cookiecutter starter project with:

pip install "cookiecutter>=1.7.0"
cookiecutter https://github.com/ilikerobots/cookiecutter-vue-django

For the remaining article, I will reference code examples from that sample project.

Sample app included with Cookiecutter, blending Templates and Vue components seamlessly

Passing information from Django to Vue

As I mentioned in the introduction to this article, there is a common conception that using a framework such as Vue requires the use of a REST API, e.g. DRF. That is not true. Here are three additional ways to pass data from a Django backend to Vue components.

I’ve created a Django package django-vue-utilities and a Vue plugin vue-plugin-django-utils that provides implementations of many techniques below.

Root Props

Vue components accept arguments called props, allowing parent components to parameterize instances of child components. However, this mechanism doesn’t work in the same way for the so-called root components, which are those top-level components defined in Vue entrypoints. Luckily, it is possible to use entrypoints to assign values to these root component props. We can exploit this to create a generalized means to pass data, including potentially Django template variables, to root components.

I use a custom utility function to pass an arbitrary object as props. This helper method will pass data from an object based on keys and correspondingly named component props. It even converts prop values to the expected JavaScript datatype.

Thus, in the entrypoint, we can simply pass this function the mounted element’s dataset and we gain a simple method of passing dataset attributes as props, e.g.:

<div class="reward-claim"
data-reward-id="{{ r.id }}"
data-points="{{ r.point_value }}"
></div>

Note that Vue transforms the data attribute’s names from kebab-case to camelCase, so data-reward-id corresponds to prop rewardId.

Provide/Inject

Another way to pass data from Django to Vue is with Vue’s provide and inject methods. This is especially useful for more ‘global’ parameters which can be made available to all components and even directly to a Pinia store. For example, I use this method to provide the Django CSRF token which is then injected into my Pinia store.

We can set up a more general channel for communication by designating an Object as a named attribute of the window object, e.g. window.vueProvided. We can assign to that object in a Django template (again, I use a template tag to simplify this process).

Then, in the entrypoint we provide any entries in this object to the app, after which we may inject them from within components using their key name.

Pseudo-slots

Slots are DOM fragments that can be passed down to components, which can then incorporate those slots as content in their own rendering. You can think of them as a means for a Vue component to wrap arbitrary content.

Unfortunately, Vue doesn’t allow slots for the root components, meaning we can’t use the slot mechanism directly to wrap content generated in Django templates.

But we can get a reasonable facsimile with some custom code. I designate a dataset attribute name, “data-django-slot”, to serve as indicator for slot content. Then when mounting my root components, custom plugin code checks the component’s children for any elements that contain this attribute and, if found, attaches those elements outerHTML to the app’s config.globalProperties.

We can then build slot-like content in Django templates which can be rendered in the root component using v-html binding.

REST

Of course, we can always pull information into Vue components via a REST backend. If your Vue components need extensive and/or complex data from the backend, then pulling that data via asynchronous requests to a REST backend may be most efficient.

Nothing precludes us from using a REST backend when and where we choose.

Sending information from Vue to Django

The mechanisms to send data from Vue to Django are essentially the same mechanism employed by any other front-end framework interfacing with Django:

Other Tricks

Teleporting

Normally, we designate an element in a Django template to entirely contain a Vue component. However, Vue may teleport content anywhere on the page. This mechanism works perfectly well from a Django template, giving us the ability to send Vue content anywhere, regardless of which container we’ve decided to embed the components.

Persistent State

By default, Pinia state is ephemeral and will be lost as soon as the user leaves a page. For a multi-page application, this is usually not desired. Luckily, pinia-plugin-persistedstate allows us to preserve state across page loads using the browser’s storage mechanisms.

It takes only a couple of lines to turn this on for the entire store. If desired, it is possible to limit the persistence to only certain paths.

Vendor Chunking

As we integrate Vue into a Django app, we may for each page require a different set of Vue components, but we don’t want to force the user to download a large JavaScript bundle on each page of the app. Instead, we want to restrict the download to a common minimal runtime plus only those specific components and dependencies used on the current page.

Fortunately, the Vite splitVendorChunkPlugin does exactly this for us.

Stylesheets

Your Vue components will inherit any styling provided by your normal Django templates, by virtue of being included on those pages. However, you can still provide scoped or unscoped styling to individual components, without having to modify your base Django styling.

Normally, however, during a production build, Vue will produce separate CSS files which are expected to be included from HTML. To eliminate the burden of having to import these CSS files in Django templates, we can, by using the cssInjectedByJsPlugin, instruct Vite to automatically include any needed CSS when we import the component module JavaScript.

SASS Compilation

If your Django project uses a SASS compilation pipeline, then your Vue components can directly reference those same source sass files as long as you include the sass dev dependency in your Vue project.

Conclusion

There are great reasons to employ a DRF + JavaScript SPA stack and many equally great reasons for a Django Template + lightweight JavaScript stack. But for those who have wanted both Templates and a full-weight build-based JavaScript framework, the solutions have appeared limited and compromised.

The techniques described in this article offer a way to take advantage of the great strengths of both Django Templates and Vue without sacrificing the power of either.

Vue Plus Django Logos

Resources

--

--

Mike Hoolehan
Mike Hoolehan

Written by Mike Hoolehan

Software dev, camper, baseball player, tabletop gamer. A long way from Terre Haute.