PythonのDjangoでバックエンドを構築し、Vue.jsでリッチなフロントエンドを実現する構成は、2026年現在も多くのプロダクトで採用されています。Django側の堅牢なORM・認証基盤と、Vue.jsのリアクティブなUI構築能力を掛け合わせることで、保守性と開発体験を両立できます。
この記事ではDjango REST Framework(DRF)を用いたバックエンドAPIの構築から、Vue 3 Composition APIによるフロントエンド実装、JWT認証、CORS設定、さらにDocker化やテスト戦略まで、本番運用を見据えた開発フローを網羅的に扱います。
Vue.jsとDjangoを組み合わせるメリットと構成パターン
なぜDjango + Vue.jsが選ばれるのか
DjangoはPython製のフルスタックWebフレームワークで、Admin管理画面・ORM・マイグレーション・認証機構がデフォルトで揃っています。一方のVue.jsは軽量かつ学習コストが低いJavaScriptフレームワークで、段階的導入が可能です。
両者を組み合わせる主な利点は以下のとおりです。
| 観点 | Django単体 | Django + Vue.js |
|---|---|---|
| UI表現力 | テンプレートエンジン依存 | SPAによるリッチUI |
| 開発分業 | フルスタック1名 | フロント/バックの分業が容易 |
| API再利用性 | テンプレート密結合 | モバイルアプリなど他クライアントと共有可能 |
| リアルタイム性 | 限定的 | WebSocket・SSE連携が自然 |
| 既存資産の活用 | Admin・ORM活用可 | Djangoの認証・権限をそのまま活用 |
3つの連携パターン
DjangoとVue.jsの連携方法は大きく3つに分かれます。
パターン1: Djangoテンプレート内にVue.jsを部分適用
Djangoのテンプレートエンジンが描画するHTMLの一部にVue.jsをマウントする方法です。既存のDjangoプロジェクトに段階的にリアクティブUIを追加したいケースに適しています。
# views.py
from django.shortcuts import render
def product_list(request):
products = Product.objects.all()
return render(request, 'products/list.html', {
'products_json': json.dumps(list(products.values()))
})
<!-- list.html -->
<div id="product-app">
<product-filter :initial-products="products"></product-filter>
</div>
<script>
const app = Vue.createApp({
data() {
return {
products: JSON.parse('{{ products_json|escapejs }}')
}
}
})
app.mount('#product-app')
</script>
パターン2: Django REST Framework + Vue.js SPA(完全分離)
フロントエンドとバックエンドを完全に分離し、DRFが提供するREST APIを介して通信します。最も一般的な構成であり、本記事の主軸です。
project-root/
├── backend/ # Django + DRF
│ ├── manage.py
│ ├── config/ # settings, urls
│ └── api/ # DRFアプリ
└── frontend/ # Vue 3 + Vite
├── src/
├── package.json
└── vite.config.ts
パターン3: Django + django-vite による統合ビルド
django-viteパッケージを使うと、Viteのビルド成果物をDjangoの静的ファイルとして配信できます。別サーバーを立てる必要がなく、Djangoの認証・セッション機構をそのまま利用可能です。
# settings.py
DJANGO_VITE = {
"default": {
"dev_mode": DEBUG,
"dev_server_host": "localhost",
"dev_server_port": 5173,
}
}
米国のDjango開発コミュニティでは「API不要な統合パターン(パターン1・3)」も積極的に議論されています。StackOverflowやDjango公式フォーラムでは「小規模プロジェクトならDRFを経由せずDjangoテンプレートにVue.jsを埋め込むほうがシンプル」という意見が多く見られます。プロジェクト規模と要件に応じた選定が重要です。
開発環境のセットアップ
前提条件
- Python 3.11以降
- Node.js 20 LTS以降
- npm または yarn
Djangoバックエンドの構築
# 仮想環境の作成と有効化
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# パッケージのインストール
pip install django djangorestframework django-cors-headers djangorestframework-simplejwt
# Djangoプロジェクトの作成
django-admin startproject config .
python manage.py startapp api
config/settings.pyに必要な設定を追加します。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# サードパーティ
'rest_framework',
'corsheaders',
# 自前アプリ
'api',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # 最上位に配置
'django.middleware.security.SecurityMiddleware',
'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',
]
Vue.jsフロントエンドの構築
npm create vite@latest frontend -- --template vue-ts
cd frontend
npm install
npm install axios vue-router@4 pinia
ViteのデフォルトテンプレートにはTypeScriptが含まれます。Vue 3のComposition APIとTypeScriptを組み合わせることで、IDEの補完が効き、型安全な開発体験を得られます。
CORS設定とAPI通信の基本
DjangoのCORS設定
フロントエンド(http://localhost:5173)とバックエンド(http://localhost:8000)が異なるオリジンで動作するため、CORS(Cross-Origin Resource Sharing)の許可設定が必要です。
# config/settings.py
# 開発環境
CORS_ALLOWED_ORIGINS = [
"http://localhost:5173",
"http://127.0.0.1:5173",
]
# 本番環境では具体的なドメインを指定
# CORS_ALLOWED_ORIGINS = ["https://your-domain.com"]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = Trueは開発時の一時的な対処としてのみ使用し、本番環境では必ず具体的なオリジンを指定します。
axiosによるAPI通信の共通設定
Vue.js側でaxiosのインスタンスを作成し、ベースURLやインターセプターを一元管理します。
// frontend/src/lib/api.ts
import axios from 'axios'
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api',
headers: {
'Content-Type': 'application/json',
},
})
// リクエストインターセプター: JWTトークンを自動付与
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// レスポンスインターセプター: 401時のトークンリフレッシュ
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// トークンリフレッシュ処理(後述)
}
return Promise.reject(error)
}
)
export default apiClient
Django REST FrameworkでCRUD APIを構築する
モデルの定義
タスク管理アプリを例にとり、モデル・シリアライザ・ビューセットを順に実装します。
# api/models.py
from django.db import models
from django.contrib.auth.models import User
class Task(models.Model):
class Status(models.TextChoices):
TODO = 'todo', '未着手'
IN_PROGRESS = 'in_progress', '進行中'
DONE = 'done', '完了'
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
status = models.CharField(
max_length=20,
choices=Status.choices,
default=Status.TODO,
)
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tasks')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
シリアライザの作成
# api/serializers.py
from rest_framework import serializers
from .models import Task
class TaskSerializer(serializers.ModelSerializer):
owner_name = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Task
fields = ['id', 'title', 'description', 'status', 'owner', 'owner_name', 'created_at', 'updated_at']
read_only_fields = ['owner', 'created_at', 'updated_at']
ビューセットとルーティング
# api/views.py
from rest_framework import viewsets, permissions
from .models import Task
from .serializers import TaskSerializer
class TaskViewSet(viewsets.ModelViewSet):
serializer_class = TaskSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Task.objects.filter(owner=self.request.user)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
# api/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import TaskViewSet
router = DefaultRouter()
router.register(r'tasks', TaskViewSet, basename='task')
urlpatterns = [
path('', include(router.urls)),
]
# config/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
]
マイグレーションを実行してテーブルを作成します。
python manage.py makemigrations
python manage.py migrate
この時点で http://localhost:8000/api/tasks/ にアクセスすると、DRFのブラウザブルAPIが表示されます。
JWT認証の実装
Django側の設定
djangorestframework-simplejwtを使用します。
# config/settings.py
from datetime import timedelta
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
}
# config/urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
Vue.js側の認証ストア(Pinia)
Vue 3では状態管理にPiniaが推奨されています。Vuexの後継として設計されており、TypeScript対応・軽量・直感的なAPIが特徴です。
// frontend/src/stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import apiClient from '@/lib/api'
export const useAuthStore = defineStore('auth', () => {
const accessToken = ref<string | null>(localStorage.getItem('access_token'))
const refreshToken = ref<string | null>(localStorage.getItem('refresh_token'))
const isAuthenticated = computed(() => !!accessToken.value)
async function login(username: string, password: string) {
const response = await apiClient.post('/token/', { username, password })
accessToken.value = response.data.access
refreshToken.value = response.data.refresh
localStorage.setItem('access_token', response.data.access)
localStorage.setItem('refresh_token', response.data.refresh)
}
async function refresh() {
if (!refreshToken.value) throw new Error('No refresh token')
const response = await apiClient.post('/token/refresh/', {
refresh: refreshToken.value,
})
accessToken.value = response.data.access
localStorage.setItem('access_token', response.data.access)
if (response.data.refresh) {
refreshToken.value = response.data.refresh
localStorage.setItem('refresh_token', response.data.refresh)
}
}
function logout() {
accessToken.value = null
refreshToken.value = null
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
}
return { accessToken, refreshToken, isAuthenticated, login, refresh, logout }
})
トークンリフレッシュの自動化
axiosのレスポンスインターセプターでアクセストークンの期限切れを検知し、自動的にリフレッシュします。
// frontend/src/lib/api.ts(レスポンスインターセプターの完全版)
import { useAuthStore } from '@/stores/auth'
let isRefreshing = false
let failedQueue: Array<{
resolve: (value: unknown) => void
reject: (reason?: unknown) => void
}> = []
const processQueue = (error: unknown) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error)
} else {
prom.resolve(undefined)
}
})
failedQueue = []
}
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject })
}).then(() => apiClient(originalRequest))
}
originalRequest._retry = true
isRefreshing = true
const authStore = useAuthStore()
try {
await authStore.refresh()
processQueue(null)
return apiClient(originalRequest)
} catch (refreshError) {
processQueue(refreshError)
authStore.logout()
window.location.href = '/login'
return Promise.reject(refreshError)
} finally {
isRefreshing = false
}
}
return Promise.reject(error)
}
)
米国のWebアプリ開発コミュニティでは、このようなサイレント認証(ページリフレッシュ時のトークン自動更新)パターンが標準的に実装されています。セッション途中で認証が切れるUX劣化を防ぐため、本番環境では必ず取り入れるべき設計です。
Vue 3 Composition APIによるフロントエンド実装
ルーティング設定
// frontend/src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/LoginView.vue'),
},
{
path: '/',
name: 'TaskList',
component: () => import('@/views/TaskListView.vue'),
meta: { requiresAuth: true },
},
{
path: '/tasks/:id',
name: 'TaskDetail',
component: () => import('@/views/TaskDetailView.vue'),
meta: { requiresAuth: true },
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
router.beforeEach((to) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
return { name: 'Login' }
}
})
export default router
タスク一覧のコンポーネント
Composition APIの<script setup>構文を使い、テンプレートとロジックを簡潔に記述します。
<!-- frontend/src/views/TaskListView.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import apiClient from '@/lib/api'
interface Task {
id: number
title: string
description: string
status: 'todo' | 'in_progress' | 'done'
owner_name: string
created_at: string
}
const tasks = ref<Task[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
const newTitle = ref('')
async function fetchTasks() {
loading.value = true
error.value = null
try {
const response = await apiClient.get<Task[]>('/tasks/')
tasks.value = response.data
} catch (e) {
error.value = 'タスクの取得に失敗しました'
} finally {
loading.value = false
}
}
async function addTask() {
if (!newTitle.value.trim()) return
try {
const response = await apiClient.post<Task>('/tasks/', {
title: newTitle.value,
})
tasks.value.unshift(response.data)
newTitle.value = ''
} catch (e) {
error.value = 'タスクの追加に失敗しました'
}
}
async function updateStatus(task: Task, status: Task['status']) {
try {
await apiClient.patch(`/tasks/${task.id}/`, { status })
task.status = status
} catch (e) {
error.value = 'ステータスの更新に失敗しました'
}
}
async function deleteTask(id: number) {
try {
await apiClient.delete(`/tasks/${id}/`)
tasks.value = tasks.value.filter((t) => t.id !== id)
} catch (e) {
error.value = 'タスクの削除に失敗しました'
}
}
onMounted(fetchTasks)
</script>
<template>
<div class="task-list">
<h1>タスク一覧</h1>
<div v-if="error" class="error">{{ error }}</div>
<form @submit.prevent="addTask" class="add-form">
<input v-model="newTitle" placeholder="新しいタスク" />
<button type="submit">追加</button>
</form>
<div v-if="loading">読み込み中...</div>
<ul v-else>
<li v-for="task in tasks" :key="task.id" class="task-item">
<div>
<strong>{{ task.title }}</strong>
<span class="status" :class="task.status">{{ task.status }}</span>
</div>
<div class="actions">
<select :value="task.status" @change="updateStatus(task, ($event.target as HTMLSelectElement).value as Task['status'])">
<option value="todo">未着手</option>
<option value="in_progress">進行中</option>
<option value="done">完了</option>
</select>
<button @click="deleteTask(task.id)">削除</button>
</div>
</li>
</ul>
</div>
</template>
ファイルアップロードの実装
タスクへの添付ファイル機能を例に、バイナリデータのやり取りを実装します。
Django側
# api/models.py(追加)
class TaskAttachment(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name='attachments')
file = models.FileField(upload_to='attachments/%Y/%m/')
uploaded_at = models.DateTimeField(auto_now_add=True)
# api/serializers.py(追加)
class TaskAttachmentSerializer(serializers.ModelSerializer):
class Meta:
model = TaskAttachment
fields = ['id', 'task', 'file', 'uploaded_at']
# api/views.py(追加)
from rest_framework.parsers import MultiPartParser, FormParser
class TaskAttachmentViewSet(viewsets.ModelViewSet):
serializer_class = TaskAttachmentSerializer
parser_classes = [MultiPartParser, FormParser]
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return TaskAttachment.objects.filter(task__owner=self.request.user)
Vue.js側
// ファイルアップロード関数
async function uploadFile(taskId: number, file: File) {
const formData = new FormData()
formData.append('file', file)
formData.append('task', String(taskId))
const response = await apiClient.post('/attachments/', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
return response.data
}
Reactとの比較: Djangoとの相性
Djangoのフロントエンドとして、Vue.js以外にReactも候補になります。プロジェクトの特性に応じた選定基準を整理します。
| 比較項目 | Vue.js | React |
|---|---|---|
| 学習曲線 | 緩やか(HTML拡張ベース) | やや急(JSX独自記法) |
| テンプレート構文 | Djangoテンプレートとの親和性が高い | JSXのため分離が明確 |
| 段階的導入 | CDN1行で既存ページに追加可能 | ビルドツール前提 |
| エコシステム規模 | 中規模(実用十分) | 大規模(npm packages豊富) |
| TypeScript対応 | Vue 3でネイティブ対応 | 元々TypeScriptと親和的 |
| 状態管理 | Pinia(公式推奨) | Redux / Zustand / Jotai など複数 |
| 日本語ドキュメント | 公式翻訳が充実 | コミュニティ翻訳中心 |
| 求人数(国内) | React比で少ない | フロントエンド求人の主流 |
Djangoテンプレートの一部にリアクティブUIを埋め込む用途(パターン1)ではVue.jsが圧倒的に導入しやすいです。一方、大規模SPAで多数のサードパーティライブラリを活用したい場合はReactのエコシステムが有利です。
Docker化による開発環境の統一
チーム開発では「手元で動かない」問題を防ぐため、Docker Composeで環境を統一します。
# docker-compose.yml
services:
backend:
build: ./backend
command: python manage.py runserver 0.0.0.0:8000
volumes:
- ./backend:/app
ports:
- "8000:8000"
environment:
- DEBUG=1
- DATABASE_URL=postgres://postgres:postgres@db:5432/app
depends_on:
- db
frontend:
build: ./frontend
command: npm run dev -- --host 0.0.0.0
volumes:
- ./frontend:/app
- /app/node_modules
ports:
- "5173:5173"
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
# backend/Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# frontend/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
docker compose up -d
これでバックエンド・フロントエンド・データベースが一括で起動します。新しいチームメンバーが参画した際もdocker compose upだけで開発環境が再現されます。
テスト戦略
Django APIのテスト
DRFにはAPIテスト用のクライアントが付属しています。
# api/tests.py
from django.contrib.auth.models import User
from rest_framework.test import APITestCase
from rest_framework import status
from .models import Task
class TaskAPITest(APITestCase):
def setUp(self):
self.user = User.objects.create_user(username='testuser', password='testpass')
self.client.force_authenticate(user=self.user)
def test_create_task(self):
response = self.client.post('/api/tasks/', {'title': 'テストタスク'})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Task.objects.count(), 1)
self.assertEqual(Task.objects.first().owner, self.user)
def test_list_tasks_only_own(self):
Task.objects.create(title='自分のタスク', owner=self.user)
other_user = User.objects.create_user(username='other', password='pass')
Task.objects.create(title='他人のタスク', owner=other_user)
response = self.client.get('/api/tasks/')
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data[0]['title'], '自分のタスク')
def test_unauthenticated_access(self):
self.client.force_authenticate(user=None)
response = self.client.get('/api/tasks/')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
python manage.py test api
Vue.jsコンポーネントのテスト
VitestとVue Test Utilsで単体テストを実行します。
npm install -D vitest @vue/test-utils jsdom
// frontend/src/views/__tests__/TaskListView.spec.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import TaskListView from '../TaskListView.vue'
vi.mock('@/lib/api', () => ({
default: {
get: vi.fn().mockResolvedValue({
data: [
{ id: 1, title: 'テスト', status: 'todo', owner_name: 'user', created_at: '2026-01-01' }
]
}),
post: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
}
}))
describe('TaskListView', () => {
it('タスク一覧が表示される', async () => {
const wrapper = mount(TaskListView, {
global: {
plugins: [createPinia()],
},
})
await vi.dynamicImportSettled()
expect(wrapper.text()).toContain('テスト')
})
})
本番デプロイ時の構成
推奨インフラ構成
本番環境ではフロントエンドのビルド成果物を静的ファイルとしてCDNから配信し、バックエンドAPIはGunicorn + Nginxで運用します。
[ユーザー] → [CDN / Nginx]
├── / → Vue.js SPA(静的ファイル)
└── /api/ → Gunicorn(Django)
└── PostgreSQL
Vue.jsのビルドとDjango静的ファイルの統合
フロントエンドのビルド成果物をDjangoのstaticfilesとして配信する方法です。
# フロントエンドのビルド
cd frontend
npm run build
# dist/ にビルド成果物が生成される
# config/settings.py(本番用)
import os
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'frontend', 'dist'),
]
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
環境変数の管理
開発環境と本番環境で異なる設定値を環境変数で切り替えます。
# frontend/.env.production
VITE_API_BASE_URL=https://api.your-domain.com/api
# config/settings.py
import os
DEBUG = os.environ.get('DEBUG', '0') == '1'
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', 'localhost').split(',')
htmxという選択肢: DRFを使わないインタラクティブUI
Django + Vue.js(DRF経由)はSPA構築の定番ですが、2024年以降、htmxを使う軽量アプローチも注目されています。htmxはHTMLの属性だけでAJAXリクエストを宣言的に記述でき、JavaScriptのビルドツールが不要です。
| 比較項目 | Django + Vue.js (DRF) | Django + htmx |
|---|---|---|
| ビルドツール | Vite / webpack 必要 | 不要 |
| JavaScript記述量 | 多い | 最小限 |
| SEO | SSR or プリレンダリング必要 | テンプレート描画で対応 |
| リアルタイムUI | WebSocket連携 | SSE / WebSocket |
| 適用規模 | 中〜大規模SPA | 小〜中規模 |
| 学習コスト | Vue.js + DRF + axios | htmx属性のみ |
htmxはDjango公式フォーラムでも話題が増えており、検索ボリューム「Django-htmx」は月間50件に達しています。小規模プロジェクトや管理画面のインタラクティブ化にはhtmxを、ユーザー向けのリッチUIにはVue.jsを、という使い分けが実践的です。
よくあるトラブルと対処法
CORS関連エラー
症状: Access to XMLHttpRequest at 'http://localhost:8000/api/' from origin 'http://localhost:5173' has been blocked by CORS policy
対処: django-cors-headersのインストールと設定を確認します。MIDDLEWARE内でCorsMiddlewareがCommonMiddlewareより上に配置されていることが重要です。
CSRFトークンエラー
症状: POSTリクエストで403 Forbidden - CSRF verification failed
対処: DRFのJWT認証を使用している場合、SessionAuthenticationを無効化するか、@csrf_exemptを適用します。JWT認証ではCSRFトークンは不要です。
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
# SessionAuthentication を含めないことでCSRF不要に
],
}
Viteのプロキシ設定
開発時にCORS問題を回避する代替手段として、Viteのプロキシ機能があります。
// frontend/vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
},
},
},
resolve: {
alias: {
'@': '/src',
},
},
})
この設定により、フロントエンドから/api/tasks/へリクエストすると、Vite開発サーバーがhttp://localhost:8000/api/tasks/に転送します。オリジンが同一になるためCORS設定が不要になり、開発体験が向上します。
まとめ
DjangoとVue.jsの組み合わせは、バックエンドの堅牢さとフロントエンドの柔軟なUI構築を両立する構成です。
プロジェクト規模に応じた最適な連携パターンを以下に整理します。
- 小規模・既存Djangoへの部分追加 → Djangoテンプレート内にVue.jsを埋め込み(CDN利用)
- 中規模・新規SPA開発 → DRF + Vue 3 + Pinia + TypeScriptの完全分離構成
- 管理画面のインタラクティブ化 → Django + htmxが軽量で効果的
- 大規模・マイクロサービス指向 → DRF APIをVue.jsだけでなくモバイルアプリからも呼び出す設計
実装に際して押さえるべきポイントは、CORS設定・JWT認証のトークンリフレッシュ・テスト自動化・Docker化の4点です。これらを初期段階から整備しておくことで、チーム開発でのスケーラビリティを確保できます。
