Laravel×Vue.jsでSPAを構築する:API分離・認証・ルーティング・デプロイまでの実務パターン
- 作成日 2026.02.18
- その他
Laravelをバックエンド(API)として、Vue.jsをフロント(SPA)として構築すると、画面遷移の高速化・UIのリッチ化・フロント/バックの責務分離が進めやすい。一方で、ルーティングの衝突、認証(Cookie/トークン)、CORS/CSRF、ビルド成果物の配信、404の扱いを雑にすると、開発環境では動くのに本番で崩れる。この記事は、最小構成から実務で詰まりやすいポイントとエラー発生条件、サンプルコードまでをまとめる。
- 1. 構成の決め方:同一ドメインSPAか、API分離(別ドメイン)か
- 2. ディレクトリ構成:Laravel(API)とVue(SPA)の置き方
- 3. Laravel側:APIの土台(ルーティング・コントローラ・レスポンス形式)
- 4. SPAのルーティング:Laravelの404とVue Routerの404を衝突させない
- 5. Vue側:Vue Router+AxiosでAPIを叩く最小サンプル
- 6. 認証の考え方:SPAで詰まりやすいCookie/CSRF/CORSの境界
- 7. ビルドと配信:Viteの成果物・キャッシュ・本番デプロイでハマる点
- 8. トラブルシュート:よくある症状→原因の当たり所
- 9. まとめ:Laravel×Vue SPAを崩さず作るための最小ルール
構成の決め方:同一ドメインSPAか、API分離(別ドメイン)か
同一ドメイン(LaravelがVueのビルド成果物も配信):
・CORSが不要になりやすい
・Cookieベース認証が簡単
・デプロイが一体化しやすい
API分離(フロント別ホスト):
・フロントを静的ホスティングに載せやすい
・CORS/CSRF設計が必須
・環境変数/URL管理が重要
最初は同一ドメイン構成に寄せると、エラー切り分けが楽。
ディレクトリ構成:Laravel(API)とVue(SPA)の置き方
よくあるパターン:
・Laravelプロジェクト内にVue(Vite)を同居(resources/js)
・フロント別リポジトリでAPI叩く
この記事のサンプルは「同居」を前提にしつつ、API分離でも使える設計に寄せる。
Laravel側:APIの土台(ルーティング・コントローラ・レスポンス形式)
SPAは「画面のデータはAPIから取る」が基本。レスポンス形式(JSONの形)を揃えると、フロント実装が安定する。
// routes/api.php(例)
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TodoController;
Route::get('/health', fn () => ['ok' => true]);
Route::get('/todos', [TodoController::class, 'index']);
Route::post('/todos', [TodoController::class, 'store']);
Route::patch('/todos/{todo}', [TodoController::class, 'update']);
Route::delete('/todos/{todo}', [TodoController::class, 'destroy']);
// app/Http/Controllers/Api/TodoController.php(例)
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Models\Todo;
class TodoController extends Controller
{
public function index(): JsonResponse
{
return response()->json([
'data' => Todo::query()->latest()->get()
]);
}
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:200'],
]);
$todo = Todo::create([
'title' => $validated['title'],
'done' => false,
]);
return response()->json(['data' => $todo], 201);
}
public function update(Request $request, Todo $todo): JsonResponse
{
$validated = $request->validate([
'title' => ['sometimes', 'string', 'max:200'],
'done' => ['sometimes', 'boolean'],
]);
$todo->update($validated);
return response()->json(['data' => $todo]);
}
public function destroy(Todo $todo): JsonResponse
{
$todo->delete();
return response()->json([], 204);
}
}エラー発生条件:
・バリデーションエラー(422)がフロントで未処理 → 画面が黙って失敗
・暗黙ルートモデルバインドの対象が存在しない → 404(フロントで「見つからない」表示が必要)
・JSON形式がエンドポイントごとにバラバラ → フロントの型/分岐が増殖
SPAのルーティング:Laravelの404とVue Routerの404を衝突させない
SPAはURL直打ち(/app/todos など)でサーバーが返すHTMLが必要。Vue Routerが担当するURLでも、Laravel側は「SPAのindex.html(Blade)」を返す必要がある。
// routes/web.php(例)
// /app 配下はすべてSPAへ(APIは /api に寄せる)
use Illuminate\Support\Facades\Route;
Route::view('/app/{any?}', 'spa')
->where('any', '.*');
<!-- resources/views/spa.blade.php(例:最小) -->
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@vite('resources/js/app.js')
</head>
<body>
<div id="app"></div>
</body>
</html>エラー発生条件:
・このキャッチオールが無い → 本番でURL直打ちするとLaravelの404
・APIも同じ配下に混ぜる → /app/api/… がSPAに吸われて壊れる(APIは /api に統一)
Vue側:Vue Router+AxiosでAPIを叩く最小サンプル
Vueは「ルート(画面)」「APIクライアント」「状態」を分けると崩れにくい。
// resources/js/app.js(例)
import { createApp } from 'vue';
import App from './components/App.vue';
import router from './router';
createApp(App).use(router).mount('#app');
// resources/js/router/index.js(例)
import { createRouter, createWebHistory } from 'vue-router';
import TodoPage from '../views/TodoPage.vue';
export default createRouter({
history: createWebHistory('/app/'),
routes: [
{ path: '/', redirect: '/todos' },
{ path: '/todos', component: TodoPage },
{ path: '/:pathMatch(.*)*', component: { template: '<div>Not Found</div>' } },
],
});
// resources/js/views/TodoPage.vue(例)
<template>
<div>
<h3>Todo</h3>
<form @submit.prevent="createTodo">
<input v-model="title" placeholder="title" />
<button type="submit">Add</button>
</form>
<ul>
<li v-for="t in todos" :key="t.id">
<label>
<input type="checkbox" :checked="t.done" @change="toggle(t)" />
{{ t.title }}
</label>
<button @click="remove(t)">Delete</button>
</li>
</ul>
<div v-if="error" style="white-space: pre-wrap;">{{ error }}</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { api } from '../lib/api';
const todos = ref([]);
const title = ref('');
const error = ref('');
async function load() {
error.value = '';
try {
const res = await api.get('/todos');
todos.value = res.data.data;
} catch (e) {
error.value = e?.response?.data?.message ?? String(e);
}
}
async function createTodo() {
error.value = '';
try {
await api.post('/todos', { title: title.value });
title.value = '';
await load();
} catch (e) {
// 422(バリデーション)をそのまま見える化
error.value = JSON.stringify(e?.response?.data ?? {}, null, 2);
}
}
async function toggle(t) {
error.value = '';
try {
await api.patch(`/todos/${t.id}`, { done: !t.done });
await load();
} catch (e) {
error.value = e?.response?.data?.message ?? String(e);
}
}
async function remove(t) {
error.value = '';
try {
await api.delete(`/todos/${t.id}`);
await load();
} catch (e) {
error.value = e?.response?.data?.message ?? String(e);
}
}
onMounted(load);
</script>エラー発生条件:
・baseURL が本番とズレる(/api ではなく別ドメイン等) → Network Error / 404
・Vue Routerのbase(/app/)を合わせていない → 画面遷移やリロードで崩れる
認証の考え方:SPAで詰まりやすいCookie/CSRF/CORSの境界
同一ドメイン構成(LaravelがSPA配信):
・Cookieセッション + CSRF の流れが作りやすい
・CORSを意識しなくて済むことが多い
API分離(別ドメイン):
・CORS許可(Access-Control-Allow-Origin等)を適切に設定
・Cookieを跨ぐなら SameSite/secure が絡む
・トークン方式(Bearer)に寄せると設計が単純化する場合がある
エラー発生条件(典型):
・419 Page Expired(CSRF不一致)
・CORS error(ブラウザがブロック)
・401 Unauthorized(認証情報が送られていない)
ビルドと配信:Viteの成果物・キャッシュ・本番デプロイでハマる点
本番は「ビルド成果物が正しく配信される」ことが最重要。
・@vite が参照するmanifestが無い/古い → 画面真っ白
・静的ファイルのキャッシュが強すぎる → 更新したのに反映されない
・.env の API URL をビルド時に埋め込んでいる → 環境差で壊れる
同居構成なら、デプロイ手順で “npm build と PHP側キャッシュ” の順序を固定する。
トラブルシュート:よくある症状→原因の当たり所
・URL直打ちで404:Laravel側のキャッチオール不足、Nginx/Apacheの設定不足
・画面が真っ白:Viteビルド未実行、manifest不整合、JSエラー(Console確認)
・APIだけ404:/api のルーティング、prefix、ベースURL、プロキシ設定
・419/CSRF:トークン取得/送信不足、Cookieドメイン/パス、SameSite
・CORSで止まる:許可Origin/Methods/Headers、プリフライトOPTIONS未対応
まず「Networkタブ」「Console」「Laravelログ」を同時に見ると切り分けが速い。
まとめ:Laravel×Vue SPAを崩さず作るための最小ルール
・APIは /api、SPAは /app などで責務を分離して衝突を避ける
・SPA用キャッチオールをLaravel側に用意し、URL直打ちに耐える
・レスポンスJSONの形を揃え、422/401/404をフロントで明示的に扱う
・認証方式(同一ドメインCookieか、分離トークンか)を先に決める
・本番はビルド成果物とキャッシュ(manifest/静的配信)を最優先で確認する
-
前の記事
Laravelのカスタムミドルウェア作成と活用法:認証以外の横断処理を安全に差し込む 2026.02.17
-
次の記事
記事がありません
コメントを書く