公開日 2026-04-08

LaravelでInertia + Vue.js を始める

Laravel 13 の fresh app に Inertia.js と Vue 3 を導入し、middleware、root template、Book 一覧ページまで独立構成で確認する。

目次

  1. 前提環境
  2. 1. ゴールと非対象
  3. 2. 先に全体像をつかむ
  4. 3. 本題を進める
  5. 3-1. 作業ディレクトリを作成して fresh app を用意する
  6. 3-2. compose.yml を作成し、.env の SQLite パスを固定する
  7. 3-3. Inertia と Vue の依存を導入し、middleware を生成する
  8. 3-4. bootstrap/app.php と root template を Inertia 用に更新する
  9. 3-5. resources/js/app.js と vite.config.js を更新する
  10. 3-6. Book 一覧ページとサンプルデータを作る
  11. 3-7. build、route:list、tinker、ブラウザで確認する
  12. 4. 確認とつまずきやすい点
  13. DB_DATABASE のコメント解除を忘れる
  14. npm run build を忘れて /books が崩れる
  15. tinker --execute に長い配列を直接書く
  16. 5. まとめ

Blade の画面は追えるが、Inertia を足す段階になると middleware、root template、Vue 側の入口、Vite build が同時に出てきます。この記事では Laravel 13 の fresh app を空ディレクトリから作り、Book 一覧ページを Inertia + Vue 3 で返すところまでの手順をまとめます。

既存記事の成果物を前提にしない構成です。compose.yml.envcomposer requirenpm installartisanroute:listtinker、画面確認をひと通り通し、Laravel 側と Vue 側がどこでつながるかを確認します。

対象読者: Laravel の Route / Controller / View / artisan は追えるが、Inertia + Vue を fresh app からどう始めるかで止まりやすい PHP 学習者・実務者。

前提環境

  • Windows 11
  • WSL2(Ubuntu)
  • VS Code(Remote - WSL)
  • Docker Desktop
  • composer:2 コンテナ
  • node:24 コンテナ
  • Laravel 13
  • Inertia.js 3
  • Vue 3
  • SQLite

コマンドは、特記がない限り WSL 側ターミナルで実行します。

1. ゴールと非対象

この記事で到達する状態は次のとおりです。

  • Laravel 13 の fresh app に Inertia.jsVue 3 を導入できる
  • HandleInertiaRequestsbootstrap/app.php へ追加する
  • resources/views/app.blade.phpresources/js/app.js の役割を説明できる
  • /booksBook 一覧ページを返し、route:listtinker、ブラウザで確認できる

焦点は Inertia 導入の最小導線です。次の内容は扱いません。

  • Breeze / Starter Kit の導入
  • 認証付き画面
  • useForm を使った保存処理
  • React 版 Inertia
  • SSR、テスト、CI、デプロイ

2. 先に全体像をつかむ

作業ディレクトリは ~/projects/laravel-inertia-vue-intro-demo にそろえます。そこで fresh app を作り、compose.ymlappnode を起動し、Laravel 側と Vue 側の接続点を順番に埋めるのがこの記事の流れです。

flowchart LR
    A[fresh app 作成] --> B[compose.yml と .env を整える]
    B --> C[Inertia と Vue を導入する]
    C --> D[HandleInertiaRequests を追加する]
    D --> E[root template と app.js を差し替える]
    E --> F[Book 一覧ページを作る]
    F --> G[migrate と db:seed]
    G --> H[route:list / tinker / browser で確認]

「なぜ Blade / Livewire / Inertia を分けるのか」を先に整理したい場合は、LaravelでBlade / Livewire / Inertia をどう使い分けるか を補助導線として読むと位置づけがつかみやすくなります。今回の主題は比較ではなく、Inertia を fresh app へつなぐ最小手順です。

3. 本題を進める

3-1. 作業ディレクトリを作成して fresh app を用意する

開始位置は ~/projects 配下です。まず作業ディレクトリを作成し、composer:2 コンテナから fresh app を展開します。

mkdir -p ~/projects/laravel-inertia-vue-intro-demo
cd ~/projects/laravel-inertia-vue-intro-demo
code .

docker run --rm -u "$(id -u):$(id -g)" -v "$(pwd):/app" -w /app composer:2 create-project laravel/laravel .

-u "$(id -u):$(id -g)" を付けているのは、生成ファイルの所有者を WSL 側の自分にそろえるためです。ここを外すと、あとで composer requirenpm install をしたときに root 所有が混ざりやすくなります。

3-2. compose.yml を作成し、.env の SQLite パスを固定する

compose.yml は次の内容で作成します。

services:
  app:
    image: composer:2
    user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
    working_dir: /app
    environment:
      HOME: /tmp
      XDG_CONFIG_HOME: /tmp/.config
      COMPOSER_HOME: /tmp/.composer
    volumes:
      - ./:/app
    ports:
      - "8000:8000"
    command: sh -lc "php artisan serve --host=0.0.0.0 --port=8000"

  node:
    image: node:24-bookworm
    user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
    working_dir: /app
    volumes:
      - ./:/app
    command: sh -lc "tail -f /dev/null"

app は Laravel の内蔵サーバー用、nodenpm installvite build 用です。node を常駐させ、必要なときだけ docker compose exec node ... で入る形にすると、ホスト側へ Node.js を直接入れずに進められます。

.env の該当箇所は次のように更新してください。Laravel 13 の fresh app では DB_CONNECTION=sqlite が既定値ですが、DB_DATABASE はコメントアウトされていました。コンテナ内から見えるパスへ固定するため、コメントを外しておきます。

APP_URL=http://localhost:8000
DB_CONNECTION=sqlite
DB_DATABASE=/app/database/database.sqlite

起動は次のコマンドです。

export LOCAL_UID="$(id -u)"
export LOCAL_GID="$(id -g)"
docker compose up -d
curl -I http://localhost:8000
  • 期待結果: HTTP/1.1 200 OK

ここでは Laravel の Welcome 画面が返れば十分です。Bind for 0.0.0.0:8000 failed: port is already allocated が出る場合は、compose.yml の左側ポートと APP_URL を一緒に 8010 などへ変えてください。

3-3. Inertia と Vue の依存を導入し、middleware を生成する

導入コマンドは次の 4 つです。

docker compose exec app composer require inertiajs/inertia-laravel
docker compose exec node npm install vue @inertiajs/vue3
docker compose exec node npm install -D @vitejs/plugin-vue
docker compose exec app php artisan inertia:middleware

この手順で app/Http/Middleware/HandleInertiaRequests.php が生成されます。npm install vue @inertiajs/vue3 の時点で既定の npm 依存も一緒に入るため、別途 npm install を先に打つ手順は不要です。

Laravel 13 の fresh app には tailwindcss@tailwindcss/vite が最初から含まれています。この記事で追加するのは @inertiajs/vue3vue@vitejs/plugin-vue だけです。

3-4. bootstrap/app.php と root template を Inertia 用に更新する

HandleInertiaRequests を Web middleware へ追加します。bootstrap/app.php は次の内容です。

<?php

use App\Http\Middleware\HandleInertiaRequests;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->web(append: [
            HandleInertiaRequests::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();

続いて、Inertia の最初の描画で使う root template を用意します。resources/views/app.blade.php は次の内容で作成してください。

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title inertia>{{ config('app.name', 'Laravel') }}</title>
        @vite(['resources/css/app.css', 'resources/js/app.js'])
        @inertiaHead
    </head>
    <body class="bg-slate-50 text-slate-900 antialiased">
        @inertia
    </body>
</html>

確認するのは 3 点です。

  • HandleInertiaRequests を Web middleware へ追加すること
  • root template 側で @inertiaHead を置くこと
  • @inertia を描画位置に置くこと

どれかが欠けると、/books を開いても白い画面のまま止まるか、Vite 読み込みエラーで崩れます。ブラウザの DevTools を開き、コンソールとネットワークで app.js と CSS が読めているかを確認してください。

3-5. resources/js/app.jsvite.config.js を更新する

Vue 側の入口を差し替えます。resources/js/app.js は次の内容です。

import './bootstrap';
import '../css/app.css';
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';

const appName = 'Laravel Inertia Vue Demo';

createInertiaApp({
    title: (title) => title ? title + ' - ' + appName : appName,
    resolve: (name) =>
        resolvePageComponent(
            './Pages/' + name + '.vue',
            import.meta.glob('./Pages/**/*.vue'),
        ),
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .mount(el);
    },
    progress: {
        color: '#2563eb',
    },
});

vite.config.js は Vue plugin を追加して次の内容へ更新します。

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

export default defineConfig({
    plugins: [
        vue(),
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: [
                'resources/views/**',
                'resources/js/**',
                'routes/**',
            ],
        }),
        tailwindcss(),
    ],
    server: {
        watch: {
            ignored: ['**/storage/framework/views/**'],
        },
    },
});

createInertiaApp() が Vue 側の入口です。resolvePageComponent()resources/js/Pages/**/*.vue を解決し、Laravel 側の Inertia::render('Books/Index') とつながります。

この例では BookPageController 側で orderBy('title') を入れているため、画面の左右順は ID 順ではありません。スクリーンショットで Book 2 が左端に出ていても正常です。

3-6. Book 一覧ページとサンプルデータを作る

まず Book model と controller を作ります。

docker compose exec app php artisan make:model Book -m
docker compose exec app php artisan make:controller BookPageController

生成された migration の timestamp 部分は実行時刻で変わるため、database/migrations/*_create_books_table.php を次の内容に置き換えてください。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('books', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('author');
            $table->unsignedInteger('price');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('books');
    }
};

app/Models/Book.php は次の内容です。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    protected $fillable = [
        'title',
        'author',
        'price',
    ];
}

app/Http/Controllers/BookPageController.php は Laravel 側で Book 一覧を取得し、Inertia へ props として渡します。

<?php

namespace App\Http\Controllers;

use App\Models\Book;
use Inertia\Inertia;
use Inertia\Response;

class BookPageController extends Controller
{
    public function __invoke(): Response
    {
        return Inertia::render('Books/Index', [
            'books' => Book::query()
                ->orderBy('title')
                ->get()
                ->map(fn (Book $book): array => [
                    'id' => $book->id,
                    'title' => $book->title,
                    'author' => $book->author,
                    'price' => $book->price,
                ])
                ->values()
                ->all(),
        ]);
    }
}

routes/web.php は次の内容です。

<?php

use App\Http\Controllers\BookPageController;
use Illuminate\Support\Facades\Route;

Route::redirect('/', '/books');
Route::get('/books', BookPageController::class)->name('books.index');

resources/js/Pages/Books/Index.vue は次の内容で作成します。

<script setup>
import { Head } from '@inertiajs/vue3';

defineProps({
    books: {
        type: Array,
        required: true,
    },
});

const formatPrice = (price) => new Intl.NumberFormat('ja-JP').format(price);
</script>

<template>
    <Head title="Books" />

    <main class="min-h-screen bg-slate-50 px-6 py-10 text-slate-900">
        <div class="mx-auto max-w-4xl space-y-8">
            <section class="rounded-3xl bg-white p-8 shadow-sm ring-1 ring-slate-200">
                <p class="text-sm font-semibold uppercase tracking-[0.2em] text-blue-700">Laravel 13 + Inertia.js + Vue 3</p>
                <h1 class="mt-3 text-3xl font-bold">Inertia で本一覧ページを返す</h1>
                <p class="mt-4 max-w-2xl text-sm leading-7 text-slate-600">
                    ルーティングとデータ取得は Laravel 側に寄せたまま、画面描画だけを Vue コンポーネントへ切り出した最小構成です。
                </p>
            </section>

            <section class="grid gap-4 md:grid-cols-3">
                <article
                    v-for="book in books"
                    :key="book.id"
                    class="rounded-2xl bg-white p-6 shadow-sm ring-1 ring-slate-200"
                >
                    <p class="text-xs font-semibold uppercase tracking-[0.2em] text-slate-500">Book {{ book.id }}</p>
                    <h2 class="mt-3 text-lg font-semibold">{{ book.title }}</h2>
                    <p class="mt-2 text-sm text-slate-600">著者: {{ book.author }}</p>
                    <p class="mt-4 text-sm font-medium text-blue-700">{{ formatPrice(book.price) }}円</p>
                </article>
            </section>
        </div>
    </main>
</template>

サンプルデータは tinker --execute に長い配列を直接書くより、seeder のほうが手順を固定しやすく、シェルのクォート崩れも避けやすくなります。database/seeders/BookSeeder.php を次の内容で作成してください。

<?php

namespace Database\Seeders;

use App\Models\Book;
use Illuminate\Database\Seeder;

class BookSeeder extends Seeder
{
    public function run(): void
    {
        Book::query()->delete();

        Book::query()->create([
            'title' => 'Laravel Starter',
            'author' => 'Aki Tanaka',
            'price' => 2800,
        ]);

        Book::query()->create([
            'title' => 'Inertia Handbook',
            'author' => 'Mika Sato',
            'price' => 3600,
        ]);

        Book::query()->create([
            'title' => 'Vue Page Patterns',
            'author' => 'Ken Suzuki',
            'price' => 4200,
        ]);
    }
}

3-7. build、route:listtinker、ブラウザで確認する

ここまでできたら、次の順で確認します。

docker compose exec app php artisan migrate
docker compose exec app php artisan db:seed --class=BookSeeder
docker compose exec app php artisan route:list --path=books
docker compose exec app php artisan tinker --execute="dump(App\\Models\\Book::query()->count());"
docker compose exec node npm run build

route:list の期待結果は次の 1 行です。

GET|HEAD  books ........................ books.index › BookPageController

tinker の期待結果は 3 です。

3 // vendor/psy/psysh/src/ExecutionClosure.php(41) : eval()'d code:1

最後にブラウザで http://localhost:8000/books を開いてください。3 件のカードが並び、Laravel StarterInertia HandbookVue Page Patterns が見えれば成功です。

Book一覧ページの表示結果

4. 確認とつまずきやすい点

確認では次の 3 点を見てください。

  • php artisan route:list --path=booksbooks.index が出る
  • php artisan tinker --execute="dump(App\\Models\\Book::query()->count());"3 を返す
  • http://localhost:8000/books で 3 件のカードが見える

詰まりやすい点は次の 3 つです。

DB_DATABASE のコメント解除を忘れる

Laravel 13 の fresh app では DB_CONNECTION=sqlite は有効ですが、DB_DATABASE はコメントアウトされています。コンテナ内パスへ固定していないと、別の SQLite ファイルを参照して books テーブルが見つからない、または tinker の件数確認で 0 が返る、といった症状が出ます。

npm run build を忘れて /books が崩れる

今回は npm run dev を常駐させず、npm run buildpublic/build を作る流れです。route:list は通るのに画面が崩れる場合は、まず docker compose exec node npm run build を打ち直してください。

tinker --execute に長い配列を直接書く

複数レコードを tinker --execute へそのまま流し込むと、WSL とシェルのクォートが崩れやすくなります。データ投入は seeder に寄せ、tinker は件数確認や単発の確認に使うほうが進めやすくなります。

5. まとめ

Inertia 導入で最初に見るべき接続点は 4 つです。Laravel 側では HandleInertiaRequests と route / controller、描画側では root template と resources/js/app.js です。この 4 点を正しく設定すると、Laravel が渡した props を Vue 側で受け取り、Blade から一気に離れなくてもページ単位のコンポーネントを返せるようになります。

次に判断軸を整理したいなら LaravelでBlade / Livewire / Inertia をどう使い分けるか、認証付きの API とつなげたいなら LaravelでSanctum認証APIを作る を続けて読むと流れがつながります。

シリーズ 15/16

このシリーズ

Laravelの基本を最初から通す

  1. 1. Laravel入門(Route / Controller / View / Model 最小構成)
  2. 2. Laravelで最小CRUDを作る(一覧 / 作成 / 編集 / 削除)
  3. 3. Laravelで品質ゲートを敷く(PHPUnit / Larastan / Pint / GitHub Actions)
  4. 4. Laravelで認証を足す(Breeze 最小導入)
  5. 5. Laravelで認可を入れる(Policyで自分の本だけ編集できるようにする)
  6. 6. LaravelでQueueを始める(database queue + worker 最小構成)
  7. 7. Laravelでスケジューラを動かす(Command + Scheduler 最小構成)
  8. 8. Laravelでファイルアップロードを扱う(Storage + validation)
  9. 9. Laravelでリレーションを扱う(User / Book / Category の基本)
  10. 10. Laravelで検索・並び替え・ページネーション付き一覧を作る
  11. 11. LaravelでLivewireを始める
  12. 12. LaravelでLivewire一覧画面を作る(検索・並び替え・ページネーション)
  13. 13. LaravelでSanctum認証APIを作る
  14. 14. LaravelでBlade / Livewire / Inertia をどう使い分けるか
  15. 15. LaravelでInertia + Vue.js を始める 現在の記事
  16. 16. LaravelでInertia + React を始める