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:
- How to add page routing (using Vue Router) to your Vue.js frontend
- How to add centralised state management in Vue.js using Pinia
- How to add persistent state (e.g., refreshing the page and still being logged in) using simple local storage
- How to register new users, with Vue and Django
- 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:
- 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.
- 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.