alt text

Running Vue with Django

This guide will teach you how to deploy a django web service with vue as it's frontend.

Why?

If you're deploying on Heroku's Dyno, you might find yourself needing two separate services to host both the frontend and backend. Not only you have to pay twice, it's usually (in a smaller scale) unnecessary.

How does it work?

Essentially, we are using Django's to serve the build dist files on the root path via whitenoise. In a Single Page Application (SPA) setup, the frontend service will request for a few additional worker files which will also be served by whitenoise. Since Django is the web service, we can use it to process API calls before sending the appropriate data downstream.

Important Note

Deploying your site using this method will mean that you won't be able to host user-uploaded media, as we have set MEDIA_URL = None. This might be an important feature to your site, and if so this guide won't work.

Prerequisites

This guide assumes that you have Python 3.10.12 installed. Some commands are for linux only, such as source env/bin/activate.

Guide

Setting up Django

First, we need to initialize a Python virtual environment:

mkdir <your-project>
cd <your-project>
python3 -m venv env

We can then activate the virtual environment with:

source env/bin/activate

Afterwards, we initialize a django-project by first installing django as a dependency (in this case, version 4.2.11):

(env) pip install django==4.2.11

Only then proceeding with:

(env) django-admin startproject djangovue .

Please make sure to include the dot at the end of the previous command. This is to start the project at the current directory.

Setting up an API for Django

To use this setup effectively, we also need an API between Django (in python) and Vue (in javascript), in this instance, we are using django-rest-framework for Django, and axios for Vue (of which we will install later).

(env) pip install django-rest-framework

Setting up WhiteNoise for Django

To be able to serve staticfiles, we need to use a helper library whitenoise.

(env) pip install whitenoise

Setting up Vue

First, we initialize and instance of Vue with:

npm create vue@latest

My recommended setup are as shown below:

✔ Project name: … frontend
✔ Add TypeScript? … Yes
✔ Add JSX Support? … No 
✔ Add Vue Router for Single Page Application development? …  Yes
✔ Add Pinia for state management? … Yes
✔ Add Vitest for Unit Testing? … Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … No

Then, we run the following commands to install all npm packages:

cd frontend
npm install

Setting up API for Vue

To communicate with backend, we will be using axios. Install axios with:

npm install axios

Configuration

File Structure

Right now, this is how our file structure looks. Django's manage.py should be on the same directory, and all vue files will be in the frontend folder.

📦djangovue
 ┣ 📜__init__.py
 ┣ 📜asgi.py
 ┣ 📜settings.py
 ┣ 📜urls.py
 ┗ 📜wsgi.py
📦frontend
 ┣ 📂public
 ┃ ┗ 📜favicon.ico
 ┣ 📂src
 ┃ ┣ 📂assets
 ┃ ┃ ┣ 📜base.css
 ┃ ┃ ┣ 📜logo.svg
 ┃ ┃ ┗ 📜main.css
 ┃ ┣ 📂components
 ┃ ┃ ┣ 📂__tests__
 ┃ ┃ ┃ ┗ 📜HelloWorld.spec.ts
 ┃ ┃ ┣ 📂icons
 ┃ ┃ ┃ ┣ 📜IconCommunity.vue
 ┃ ┃ ┃ ┣ 📜IconDocumentation.vue
 ┃ ┃ ┃ ┣ 📜IconEcosystem.vue
 ┃ ┃ ┃ ┣ 📜IconSupport.vue
 ┃ ┃ ┃ ┗ 📜IconTooling.vue
 ┃ ┃ ┣ 📜HelloWorld.vue
 ┃ ┃ ┣ 📜TheWelcome.vue
 ┃ ┃ ┗ 📜WelcomeItem.vue
 ┃ ┣ 📂router
 ┃ ┃ ┗ 📜index.ts
 ┃ ┣ 📂stores
 ┃ ┃ ┗ 📜counter.ts
 ┃ ┣ 📂views
 ┃ ┃ ┣ 📜AboutView.vue
 ┃ ┃ ┗ 📜HomeView.vue
 ┃ ┣ 📜App.vue
 ┃ ┗ 📜main.ts
 ┣ 📜.gitignore
 ┣ 📜README.md
 ┣ 📜env.d.ts
 ┣ 📜index.html
 ┣ 📜package-lock.json
 ┣ 📜package.json
 ┣ 📜tsconfig.app.json
 ┣ 📜tsconfig.json
 ┣ 📜tsconfig.node.json
 ┣ 📜tsconfig.vitest.json
 ┣ 📜vite.config.ts
 ┗ 📜vitest.config.ts
 📜manage.py

Configuring Django's settings.py

We are going to go over changing the configuration settings for Django to enable it to serve files on the root directory.

WhiteNoise

We first need to include whitenoise as a middleware. We can also enable it's compression and caching support along the way as well.

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',

    "whitenoise.middleware.WhiteNoiseMiddleware",

    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

STORAGES = {
    # ...
    "staticfiles": {
        "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
    },
}

Staticfiles

Django's default STATIC_URL is /static. This means that to access a image with the name yippe.png, we would have to hit the filepath www.example.com/static/yippe.png. This will not work with Vue SPA, so we need to set it up in a way that sets the STATIC_URL to be the root path (/). However, MEDIA_URL cannot be the same as STATIC_URL, so we will need to modify that as well. This is also the reason why we can't have user hosted media.

...
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/

# this will serve all files under the root url.
# primarily to resolve issues related to working with PWA.
STATIC_URL = '/' 

# explicitly set the STATIC_ROOT directory
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

# MEDIA_URL must not be the same as STATIC_URL, but
# since STATIC_URL is the root url, we have to omit MEDIA_URL.
# this means that we can't serve user stored media.
# https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-MEDIA_ROOT
MEDIA_URL = None

# likewise, we like to remove MEDIA_ROOT as well
MEDIA_ROOT = None

# add the folder `dist` to the list of staticfiles directories
# this is where the build files from the frontend will be exported to.
STATCFILES_DIRS = [
    os.path.join(BASE_DIR, 'dist')
]

Don't forget to import os!

Adding dist to Template Directory

For us to be able to render the index.html file, we need to serve it. We can let Django know of it's existence by modifying DIRS:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            BASE_DIR / 'dist'
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Configuring Vue's vite.config.ts

In vite.config.ts, we are trying to achieve a few objectives:

  • We need to route all api calls to the django server via proxy.
  • We need to change to build directory.
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  build: {
    // this will output the files to 1 folder up.
    outDir: '../dist',
    // any static files (css/js/img) will be stored in this folder
    // (e.g) {outDir}/static/main.css
    assetsDir: 'static',
  },
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    proxy: {
      // all calls that hit the /api/ filepath will be routed to the target server
      // you will need to ensure that django will be running on this port instead
      '/api': {
        target: 'http://localhost:5000',
        changeOrigin: false,
        secure: false
      }
    }
  }
})

Final Setup

Migrating

(env) python3 manage.py migrate

Running Development Server

Django's own staticfiles server does not allow root-level directory staticfiles, and so we need to run the command with the argument --nostatic. We also need to ensure that runserver is serving at port 5000.

(env) python3 manage.py runserver --nostatic 5000

Deploying as a Production

Please read Django's Deployment Checklist before deploying Django online. You may risk running into security vulnerabilites otherwise.

I also highly recommend separating your settings.py into development and production variants. You can read more here.

First, we need to install gunicorn

(env) pip install gunicorn

We can then build the distribution files using:

cd frontend
npm run build
cd ..

You can then deploy the service using:

(env) gunicorn --chdir djangovue djangovue.wsgi --log-file -

The argument --chdir change the directory to djangovue, and --log-file - outputs all log into stdout, which is the terminal.

Learn more about deploying Django here