Use Vue with Django (including Auth) in 15 mins 🍒

Photo of Tom Dekan
by Tom Dekan

I'll show you how to setup a Vue.js frontend that Django REST API - including user authentication.

But, we'll do even more than connecting Vue to Django with auth. I'll also show you 5 things that you'll want to do when adding auth:

  1. How to add page routing (using Vue Router) to your Vue.js frontend
  2. How to add centralised state management in Vue.js using Pinia
  3. How to add persistent state (e.g., refreshing the page and still being logged in) using simple local storage
  4. How to register new users, with Vue and Django
  5. Secure and simple auth using Django's in-built session authentication

Side note from me on the best frontend stack with Django:

If you are determined to use a frontend (rather than full-stack Django), Vue 3 + Django is my favourite stack (I'll check out Nuxt 3 soon). React is great, but I think it's easier to write simpler code with Vue.

Here's a video guide of me building this in real-time (featuring me 🙂):


P.S Check out the FAQ at the end of this article for questions that people have asked me about this guide.

Let's go! 🚀

Set up the Django project:

pip install django django-cors-headers
django-admin startproject core .
python manage.py startapp sim

Update core/settings.py:

Add the following to your INSTALLED_APPS:

INSTALLED_APPS = [
    ## ...
    'sim',
    'corsheaders',
]

Update MIDDLEWARE:

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware", # Add this line at the top. Position matters.
    ...
]

Add your CORS settings for the backend to allow requests from the frontend:

CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = ["http://localhost:5173"]  # We add your frontend URL here.
CSRF_TRUSTED_ORIGINS = ['http://localhost:5173']  # We add your frontend URL here.

Update core/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('sim.urls')),
]

Create sim/urls.py:

from django.urls import path
from . import views

urlpatterns = [
    path('api/set-csrf-token', views.set_csrf_token, name='set_csrf_token'),
    path('api/login', views.login_view, name='login'),
    path('api/logout', views.logout_view, name='logout'),
    path('api/user', views.user, name='user'),
    path('api/register', views.register, name='register'),
]

Create sim/forms.py:

from django import forms
from django.contrib.auth.models import User

class CreateUserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['email', 'password']

    def save(self, commit=True) -> User:
        user = super().save(commit=False)
        user.username = self.cleaned_data["email"]
        user.set_password(self.cleaned_data["password"])
        if commit:
            user.save()
        return user

Update sim/views.py:

from django.shortcuts import render
from django.http import JsonResponse
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_protect
import json

from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth import authenticate, login, logout
from .forms import CreateUserForm


@ensure_csrf_cookie
@require_http_methods(['GET'])
def set_csrf_token(request):
    """
    We set the CSRF cookie on the frontend.
    """
    return JsonResponse({'message': 'CSRF cookie set'})


@require_http_methods(['POST'])
def login_view(request):
    try:
        data = json.loads(request.body.decode('utf-8'))
        email = data['email']
        password = data['password']
    except json.JSONDecodeError:
        return JsonResponse(
            {'success': False, 'message': 'Invalid JSON'}, status=400
        )

    user = authenticate(request, username=email, password=password)

    if user:
        login(request, user)
        return JsonResponse({'success': True})
    return JsonResponse(
        {'success': False, 'message': 'Invalid credentials'}, status=401
    )


def logout_view(request):
    logout(request)
    return JsonResponse({'message': 'Logged out'})


@require_http_methods(['GET'])
def user(request):
    if request.user.is_authenticated:
        return JsonResponse(
            {'username': request.user.username, 'email': request.user.email}
        )
    return JsonResponse(
        {'message': 'Not logged in'}, status=401
    )


@require_http_methods(['POST'])
def register(request):
    data = json.loads(request.body.decode('utf-8'))
    form = CreateUserForm(data)
    if form.is_valid():
        form.save()
        return JsonResponse({'success': 'User registered successfully'}, status=201)
    else:
        errors = form.errors.as_json()
        return JsonResponse({'error': errors}, status=400)

Set up the Vue.js frontend:

In a new terminal, run the following commands:

npm create vite@latest frontend -- --template vue
cd frontend
npm install
npm install vue-router pinia

Update frontend/src/main.js:

We need to add Pinia and Vue Router to our Vue app. We'll also import our global styles.

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import './style.css' // Using the default Vite CSS. Replace with your own global styles.
import router from './router'
import App from './App.vue'
import { useAuthStore } from './store/auth'

const app = createApp(App)

app.use(createPinia())
app.use(router)

const authStore = useAuthStore()
authStore.setCsrfToken()

app.mount('#app')

Create your vue router

  • Create a new file called router.js in the frontend/src folder.
  • Add the following code to create a simple router with three routes:
import {createRouter, createWebHistory} from 'vue-router'
import Home from './pages/Home.vue'
import Login from './pages/Login.vue'
import Register from "./pages/Register.vue";

const routes = [
    {
        path: '/',
        name: 'home',
        component: Home
    },
    {
        path: '/login',
        name: 'login',
        component: Login
    },
    {
        path: '/register',
        name: 'register',
        component: Register
    }
]

const router = createRouter({
    history: createWebHistory(),
    routes
})

export default router

Update frontend/src/App.vue:

  • Update the App.vue file to include the router view and global styles.
<script setup>
</script>

<template>
  <router-view />
</template>

<style scoped>
/* You can add any global styles here */
</style>

Create frontend/src/pages/Home.vue:

  • Create a simple home page with a welcome message, user information, and a logout button.
<script>
import { useAuthStore } from '../store/auth.js'
import { useRouter } from 'vue-router'

export default {
  setup() {
    const authStore = useAuthStore()
    const router = useRouter()

    return {
      authStore, router
    }
  },
  methods: {
    async logout() {
      try {
        await this.authStore.logout(this.$router)
      } catch (error) {
        console.error(error)
      }
    }
  },
  async mounted() {
    await this.authStore.fetchUser()
  }
}
</script>

<template>
  <h1>Welcome to the home page</h1>
  <div v-if="authStore.isAuthenticated">
    <p>Hi there {{ authStore.user?.username }}!</p>
    <p>You are logged in.</p>
    <button @click="logout">Logout</button>
  </div>
  <p v-else>
    You are not logged in. <router-link to="/login">Login</router-link>
  </p>
</template>

Create frontend/src/pages/Login.vue:

Create a login page with a form to submit email and password for authentication.

<template>
  <div class="login">
    <h1>Login</h1>
    <form @submit.prevent="login">
      <div>
        <label for="email">Email:</label>
        <input v-model="email" id="email" type="text" required
               @input="resetError">
      </div>
      <div>
        <label for="password">Password:</label>
        <input v-model="password" id="password" type="password" required
               @input="resetError">
      </div>
      <button type="submit">Login</button>
    </form>
    <p v-if="error" class="error">{{ error }}</p>
  </div>
</template>

<script>
import { useAuthStore } from '../store/auth'

export default {
  setup() {
    const authStore = useAuthStore()
    return {
      authStore
    }
  },
  data() {
    return {
      email: "",
      password: "",
      error: ""
    }
  },
  methods: {
    async login(){
      await this.authStore.login(this.email, this.password, this.$router)
      if (!this.authStore.isAuthenticated){
        this.error = 'Login failed. Please check your credentials.'
      }
    },
    resetError(){
      this.error = ""
    }
  }
}
</script>

Create frontend/src/pages/Register.vue:

We'll create a registration page with a form to submit email and password for user registration.

<template>
  <div>
    <h2>Register</h2>
    <form @submit.prevent="register" >
      <div>
        <label for="email">Email:</label>
        <input v-model="email" id="email" type="email" required>
      </div>
      <div>
        <label for="password">Password:</label>
        <input v-model="password" id="password" type="password" required>
      </div>
      <button type="submit">Register</button>
    </form>
    <p v-if="error">{{ error }}</p>
    <p v-if="success">{{ success }}</p>
  </div>
</template>

<script>
import { getCSRFToken } from '../store/auth'

export default {
  data() {
    return {
      email: '',
      password: '',
      error: '',
      success: ''
    }
  },
  methods: {
    async register() {
      try {
        const response = await fetch('http://localhost:8000/api/register', {
          method: 'POST',
           headers: {
                    'Content-Type': 'application/json',
                    'X-CSRFToken': getCSRFToken()
                },
          body: JSON.stringify({
            email: this.email,
            password: this.password
          }),
          credentials: 'include'
        })
        const data = await response.json()
        if (response.ok) {
          this.success = 'Registration successful! Please log in.'
          setTimeout(() => {
            this.$router.push('/login')
          }, 1000)
        } else {
          this.error = data.error || 'Registration failed'
        }
      } catch (err) {
        this.error = 'An error occurred during registration: ' + err
      }
    }
  }
}
</script>

Create frontend/src/store/auth.js:

We'll create a Pinia store to manage user authentication state and API calls. We'll also save the user state to local storage for persistence. This will allow users to remain logged in even after refreshing the page.

import { defineStore } from 'pinia'

export const useAuthStore = defineStore('auth', {
    state: () => {
        const storedState = localStorage.getItem('authState')
        return storedState ? JSON.parse(storedState) : {
            user: null,
            isAuthenticated: false
        }
    },
    actions: {
        async setCsrfToken() {
            await fetch('http://localhost:8000/api/set-csrf-token', {
                method: 'GET',
                credentials: 'include'
            })
        },

        async login(email, password, router=null) {
            const response = await fetch('http://localhost:8000/api/login', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRFToken': getCSRFToken()
                },
                body: JSON.stringify({ email, password }),
                credentials: 'include'
            })
            const data = await response.json()
            if (data.success) {
                this.isAuthenticated = true
                this.saveState()
                if (router){
                    await router.push({name: "home"})
                }
            } else {
                this.user = null
                this.isAuthenticated = false
                this.saveState()
            }
        },

        async logout(router=null) {
            try {
                const response = await fetch('http://localhost:8000/api/logout', {
                    method: 'POST',
                    headers: {
                        'X-CSRFToken': getCSRFToken()
                    },
                    credentials: 'include'
                })
                if (response.ok) {
                    this.user = null
                    this.isAuthenticated = false
                    this.saveState()
                    if (router){
                        await router.push({name: "login"})
                    }
                }
            } catch (error) {
                console.error('Logout failed', error)
                throw error
            }
        },

        async fetchUser() {
            try {
                const response = await fetch('http://localhost:8000/api/user', {
                    credentials: 'include',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRFToken': getCSRFToken()
                    },
                })
                if (response.ok) {
                    const data = await response.json()
                    this.user = data
                    this.isAuthenticated = true
                }
                else{
                    this.user = null
                    this.isAuthenticated = false
                }
            } catch (error) {
                console.error('Failed to fetch user', error)
                this.user = null
                this.isAuthenticated = false
            }
            this.saveState()
        },

        saveState() {
            /*
            We save state to local storage to keep the
            state when the user reloads the page.

            This is a simple way to persist state. For a more robust solution,
            use pinia-persistent-state.
             */
            localStorage.setItem('authState', JSON.stringify({
                user: this.user,
                isAuthenticated: this.isAuthenticated
            }))
        }
    }
})

export function getCSRFToken() {
    /*
    We get the CSRF token from the cookie to include in our requests.
    This is necessary for CSRF protection in Django.
     */
    const name = 'csrftoken';
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    if (cookieValue === null) {
        throw 'Missing CSRF cookie.'
    }
    return cookieValue;
}

Final steps to setup Django:

  • Run database migrations:
python manage.py makemigrations
python manage.py migrate

Run the development servers:

  • Start the Django development server:
python manage.py runserver
  • In a separate terminal, start the Vue.js development server:
cd frontend
npm run dev

Congrats! 🎉

You've made a Django-Vue.js app with user authentication!

The backend provides API endpoints for registration, login, logout, and fetching user information. The frontend uses Vue Router for navigation and Pinia for state management, with proper authentication flows.

This ability to register and authenticate users can serve as your foundation for your app. Build from here 🚀

Note: Remember to replace http://localhost:8000 with your production backend URL when deploying the app. I normally do this with environment variables.

FAQ - Questions people have asked about this guide

Q. Thank you, this is very helpful for leveraging the power of both vue and django. One question: Does localstorage leave a security vulnerability since the user can modify its state?

A. If you just used local storage (as in the local storage on the browser), you might create a security issue that you'd need to handle.

However, we don't do that. In this guide, we use:

  1. Session storage. This sets a httpsecure cookie that only servers can modify with the session id (which Django uses to set the user details into the request). We make a request to Django from Vue to set the session id, but Vue can't access this session id.
  2. Csrftoken. This is a random token that Django sends to Vue to check that only our intended site (our Vue app) is accessing Django. We access this token with Vue, but Django is expecting a certain csrftoken back. The csrftoken contains no sensitive data and changing it would cause django to throw an error.

Let's get visual.

Do you want to create beautiful frontends effortlessly?
Click below to book your spot on our early access mailing list (as well as early adopter prices).
Copied link to clipboard 📋

Made with care by Tom Dekan

© 2024 Photon Designer