技術資料

Laravel 13+Vue.jsで登録・編集フォームを非同期化する:fetchとaxiosを比較

作成日:2026.08.26

Laravel 13のBlade画面にVue.jsを組み込み、membersテーブルのnameとemailを登録・編集するフォームを作ります。Vue.jsからLaravelのWebルートへJSONを送信し、CSRFトークン、バリデーションエラー、送信中・通信失敗の状態を扱います。リクエスト送信にはfetchとaxiosを使う方法を比較し、追加パッケージの有無やエラー処理の違いを整理します。

Laravelで画面を作っていると、Bladeでページ全体を構成しつつ、登録や編集のフォームだけをVue.jsで動かしたくなることがあります。

今回は、membersテーブルにnameemailを登録・編集する画面を作ります。Vue.jsのフォームからLaravelのバックエンドへ非同期リクエストを送り、バリデーションエラーや送信中の状態を画面内に表示します。

リクエストの送信には、ブラウザ標準のfetchと、npmから導入するaxiosを使う方法を比較します。認証・認可は扱わず、画面とバックエンドの基本的な連携に範囲を絞ります。

前提

今回の前提環境は以下の通りです。

  • OS: Windows 11
  • PHP: 8.3以上
  • Composer
  • Laravel Framework: 13.x
  • Node.js、npm
  • Vue.js、Vite
  • ターミナル: PowerShell

Laravelの環境構築や、BladeへVue.jsを組み込む基本手順については、次の記事を前提にします。

今回は、アプリケーション全体をSPAへ変更せず、Bladeで表示するページの一部にVue.jsのフォームを配置します。

今回の処理の全体像

登録と編集では、次のような処理を行います。

ブラウザ
  ↓ Bladeのページへアクセス
LaravelのRoute
  ↓
MemberController
  ├─ 登録・編集画面を表示
  └─ JSONを返す登録・更新処理
       ↓
Vue.jsのフォーム
  ├─ 入力値を管理
  ├─ fetchまたはaxiosで送信
  ├─ バリデーションエラーを表示
  └─ 成功時に完了画面へ移動

登録・編集処理のバックエンドは、通常のLaravelのWebルートへ用意します。レスポンスはJSONで返し、Vue.js側で結果を受け取ります。

なお、認証・認可を実装していないため、このまま実際の会員情報を扱う画面へ利用することはできません。実際のアプリケーションでは、ログインユーザーの確認や操作権限の確認が必要です。

membersテーブルとModelを用意する

まず、説明用のmembersテーブルを用意します。前回の記事で作成したテーブルを利用する場合は、この章を読み飛ばして構いません。

Migrationを作成する
php artisan make:migration create_members_table

作成されたMigrationへ、次のように記述します。

<?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('members', function (Blueprint $table): void {
            $table->id();
            $table->string('name');
            $table->string('email');
            $table->timestamps();
        });
    }

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

Migrationを実行します。

php artisan migrate
Member Modelを作成する
php artisan make:model Member

app/Models/Member.phpへ、登録・更新を許可する項目を指定します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Member extends Model
{
    protected $fillable = [
        'name',
        'email',
    ];
}

fillableへ指定していない値を、そのまま配列でModelへ渡さないようにします。入力値をバリデーションしたうえで、登録・更新を許可した項目だけをModelへ渡します。

Laravel側のルートとControllerを作成する

ルートを追加する

まず、Controllerを作成します。

php artisan make:controller MemberController

routes/web.phpへ、画面表示、登録、編集、更新、完了画面のルートを追加します。

<?php

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

Route::get('/members/create', [MemberController::class, 'create'])
    ->name('members.create');
Route::post('/members', [MemberController::class, 'store'])
    ->name('members.store');
Route::get('/members/{member}/edit', [MemberController::class, 'edit'])
    ->name('members.edit');
Route::put('/members/{member}', [MemberController::class, 'update'])
    ->name('members.update');
Route::get('/members/complete', [MemberController::class, 'complete'])
    ->name('members.complete');

今回は認証・認可を設定していないため、これらのURLへアクセスできる人を制限していません。動作確認用のローカル環境で使用する例として扱います。

MemberControllerを実装する

app/Http/Controllers/MemberController.phpへ、次のように記述します。

<?php

namespace App\Http\Controllers;

use App\Models\Member;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;

class MemberController extends Controller
{
    public function create(): View
    {
        return view('members.form', [
            'mode' => 'create',
            'member' => null,
        ]);
    }

    public function edit(Member $member): View
    {
        return view('members.form', [
            'mode' => 'edit',
            'member' => $member->only(['id', 'name', 'email']),
        ]);
    }

    public function store(Request $request): JsonResponse
    {
        $member = Member::create($this->validated($request));

        return response()->json([
            'message' => '登録が完了しました。',
            'redirect_url' => route('members.complete'),
            'member_id' => $member->id,
        ], 201);
    }

    public function update(Request $request, Member $member): JsonResponse
    {
        $member->update($this->validated($request));

        return response()->json([
            'message' => '更新が完了しました。',
            'redirect_url' => route('members.complete'),
            'member_id' => $member->id,
        ]);
    }

    public function complete(): View
    {
        return view('members.complete');
    }

    private function validated(Request $request): array
    {
        return $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255'],
        ]);
    }
}

$request->validate()で入力値を検証し、検証済みの配列だけをMember::create()update()へ渡しています。

非同期リクエストでバリデーションエラーをJSONとして受け取るには、後述するJavaScript側でAccept: application/jsonを送信します。LaravelがJSONレスポンスを期待するリクエストとして処理するため、バリデーション失敗時に項目別エラーを受け取れます。

BladeでVue.jsのマウント先とCSRFトークンを用意する

resources/views/members/form.blade.phpを作成します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Memberフォーム</title>
</head>
<body>
    <h1>Memberフォーム</h1>

    <div id="member-form"></div>

    <script>
        window.memberFormProps = {
            mode: @json($mode),
            member: {{ Js::from($member) }},
        };
    </script>

    @vite('resources/js/app.js')
</body>
</html>

CSRFトークンはmeta要素へ埋め込み、Vue.jsからリクエストヘッダーへ設定します。今回のようにfetchaxiosを比較する場合、どちらからも同じmeta要素を参照できるため、実装の比較がしやすくなります。

Laravelでは、X-CSRF-TOKENリクエストヘッダーにトークンを設定する方法が利用できます。XSRF-TOKEN Cookieを使う方式はAxiosなどが利用しやすい方法ですが、今回は共通の実装としてmeta要素とリクエストヘッダーを採用します。詳しくはLaravel 13.xのCSRF Protectionを参照してください。

Vue.jsの登録・編集フォームを作成する

エントリーポイントからコンポーネントを読み込む

resources/js/app.jsへ、Vue.jsのアプリケーションを作成する処理を追加します。

import { createApp } from 'vue';
import MemberForm from './components/MemberForm.vue';

const target = document.querySelector('#member-form');

if (target) {
    createApp(MemberForm, window.memberFormProps).mount(target);
}
フォームコンポーネントを作成する

resources/js/components/MemberForm.vueを作成します。

<script setup>
import { reactive, ref } from 'vue';

const props = defineProps({
    mode: {
        type: String,
        required: true,
    },
    member: {
        type: Object,
        default: null,
    },
});

const form = reactive({
    name: props.member?.name ?? '',
    email: props.member?.email ?? '',
});

const errors = ref({});
const generalError = ref('');
const processing = ref(false);

const csrfToken = document
    .querySelector('meta[name="csrf-token"]')
    ?.getAttribute('content');

const submit = async () => {
    errors.value = {};
    generalError.value = '';
    processing.value = true;

    const isEdit = props.mode === 'edit';
    const url = isEdit
        ? `/members/${props.member.id}`
        : '/members';
    const method = isEdit ? 'PUT' : 'POST';

    try {
        const response = await fetch(url, {
            method,
            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
                'X-CSRF-TOKEN': csrfToken,
            },
            body: JSON.stringify(form),
        });

        const data = await response.json();

        if (response.status === 422) {
            errors.value = data.errors ?? {};
            return;
        }

        if (!response.ok) {
            throw new Error('リクエストに失敗しました。');
        }

        window.location.assign(data.redirect_url);
    } catch {
        generalError.value = '通信に失敗しました。時間をおいて再度お試しください。';
    } finally {
        processing.value = false;
    }
};
</script>

<template>
    <form @submit.prevent="submit">
        <div v-if="generalError">{{ generalError }}</div>

        <div>
            <label for="name">名前</label>
            <input id="name" v-model="form.name" type="text">
            <p v-for="message in errors.name" :key="message">
                {{ message }}
            </p>
        </div>

        <div>
            <label for="email">メールアドレス</label>
            <input id="email" v-model="form.email" type="email">
            <p v-for="message in errors.email" :key="message">
                {{ message }}
            </p>
        </div>

        <button type="submit" :disabled="processing">
            {{ processing ? '送信中...' : '保存する' }}
        </button>
    </form>
</template>

編集画面の場合は、Bladeから渡されたmemberの値を初期値として使います。登録画面ではmembernullなので、空のフォームになります。

送信中はprocessingtrueにしてボタンを無効化します。これにより、利用者がボタンを連続して押したときの二重送信を防ぎやすくなります。

fetchでは、サーバーから422や500などのエラーが返ってきても、レスポンス自体を受け取れれば処理が続きます。そのため、response.okやステータスコードを確認し、エラーとして処理する必要があります。

fetchとaxiosを比較する

ここまでの例では、追加パッケージが不要なfetchを使いました。ブラウザのFetch APIは、リクエストを送信してResponseを受け取るための標準APIです。ただし、HTTPエラーの扱いやレスポンスの解析は、自分で記述する必要があります。

axiosを使う場合は、プロジェクトへインストールします。

npm install axios

同じ登録・編集処理をAxiosで書く場合の要点は、次のようになります。

import axios from 'axios';

const submitWithAxios = async () => {
    errors.value = {};
    generalError.value = '';
    processing.value = true;

    const isEdit = props.mode === 'edit';
    const url = isEdit
        ? `/members/${props.member.id}`
        : '/members';
    const method = isEdit ? 'put' : 'post';

    try {
        const response = await axios({
            method,
            url,
            data: form,
            headers: {
                'Accept': 'application/json',
                'X-CSRF-TOKEN': csrfToken,
            },
        });

        window.location.assign(response.data.redirect_url);
    } catch (error) {
        if (error.response?.status === 422) {
            errors.value = error.response.data.errors ?? {};
        } else {
            generalError.value = '通信に失敗しました。';
        }
    } finally {
        processing.value = false;
    }
};

Axiosでは、2xx以外のレスポンスをエラーとして扱いやすく、error.responseからステータスやレスポンスデータを取得できます。また、リクエストの共通設定やタイムアウトなどをAxiosの設定としてまとめられます。

一方で、Axiosを使うには依存パッケージの追加が必要です。ブラウザ標準APIだけで完結する構成や、依存関係を増やしたくない小さな画面では、fetchの方が適しています。

比較項目 fetch axios
導入 ブラウザ標準のため追加インストール不要 npmでパッケージを追加する
HTTPエラー response.okやステータスを自分で確認する 2xx以外をエラーとして扱いやすい
レスポンス処理 response.json()などを明示的に呼び出す response.dataから取得する
共通設定 共通処理を自分で用意する インスタンスや初期設定へまとめやすい
選択の目安 依存を増やさず、小さく実装したい場合 複数画面で通信処理を共通化したい場合

今回のような小さな画面では、まずfetchで十分に実装できます。すでにプロジェクトでAxiosを使っている場合や、通信処理の共通設定・エラー処理を複数画面で共有する場合は、Axiosを選択してもよいかと思います。

完了画面を作成する

登録・編集が成功したときに移動する完了画面を作成します。

resources/views/members/complete.blade.phpへ、次の内容を記述します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>完了</title>
</head>
<body>
    <h1>Memberの保存が完了しました</h1>
    <p><a href="{{ route('members.create') }}">登録画面へ戻る</a></p>
</body>
</html>

バックエンドのJSONレスポンスへredirect_urlを含め、Vue.js側でwindow.location.assign()を実行しています。リダイレクト先をJavaScriptへ固定的に書かず、Laravelのルートから生成したURLを利用する形です。

動作を確認する

登録画面を表示する

ViteとLaravelの開発サーバーを起動します。

npm run dev
php artisan serve

ブラウザで次のURLへアクセスします。

http://127.0.0.1:8000/members/create

名前とメールアドレスを入力して保存すると、Vue.jsからPOST /membersへJSONが送信されます。保存に成功すると、完了画面へ移動します。

編集画面を表示する

登録済みのIDを指定して編集画面へアクセスします。

http://127.0.0.1:8000/members/1/edit

編集画面では、ControllerからBladeへ渡したMemberの値がVue.jsの初期値として表示されます。保存すると、PUT /members/1へJSONが送信されます。

バリデーションエラーを確認する

名前を空にしたり、メールアドレスとして解釈できない値を入力したりすると、Laravelのバリデーションに失敗します。

Accept: application/jsonを付けているため、Laravelからは422のJSONレスポンスが返ります。Vue.jsはerrors.nameerrors.emailを使い、各入力欄の近くへエラーメッセージを表示します。

CSRFエラーを確認する

meta要素から取得したトークンをヘッダーへ設定できていない場合、CSRF検証に失敗します。開発者ツールのNetworkタブで、リクエストへ次のヘッダーが含まれているか確認します。

X-CSRF-TOKEN: LaravelのCSRFトークン

CSRF対策を無効にして動かすのではなく、meta要素の値とリクエストヘッダーの設定を確認します。

通信失敗を確認する

Laravelの開発サーバーを停止した状態で保存すると、通信失敗として扱われます。Vue.js側のgeneralErrorが表示され、送信ボタンが再び押せる状態になることを確認します。

注意点

  • 今回の例では認証・認可を実装していないため、実際の会員情報を扱う画面へそのまま利用しない。
  • CSRFトークンをURLやlocalStorageへ保存せず、今回のようにmeta要素から取得してリクエストヘッダーへ設定する。
  • 入力値はブラウザ側だけで検証せず、Laravel側でも必ずバリデーションする。
  • Modelへ渡す値はバリデーション済みの項目に限定し、意図しないカラムを更新しない。
  • Axiosの共通ヘッダーへ秘密情報を設定する場合は、送信先が意図したURLに限定されているか確認する。
  • 本番環境では、エラー内容、ログ、個人情報、メールアドレスの扱いを別途設計する。

まとめ

今回は、Bladeをページ全体の土台として残し、Vue.jsの登録・編集フォームからLaravelのWebルートへ非同期リクエストを送る構成を確認しました。

Laravel側では、Route、Controller、バリデーション、Model、JSONレスポンスを用意しました。Vue.js側では、入力値、送信中、バリデーションエラー、通信失敗を管理し、登録・編集が成功した場合は完了画面へ移動しています。

fetchは追加パッケージなしで利用でき、処理を小さく保ちやすい方法です。axiosは依存の追加が必要ですが、HTTPエラー、レスポンス、共通設定、タイムアウトなどを整理しやすい特徴があります。

CSRFトークンはBladeのmeta要素から取得し、fetchaxiosの両方でX-CSRF-TOKENヘッダーへ設定しました。どちらを使う場合でも、Laravel側のバリデーションとアクセス制御を省略してよいわけではありません。

参考資料

この記事を書いた人

※上が私です。

奈良市を拠点に、27年以上の経験を持つフリーランスWebエンジニア、阿部辰也です。

これまで、ECサイトのバックエンド開発や業務効率化システム、公共施設の予約システムなど、多彩なプロジェクトを手がけ、企業様や制作会社様のパートナーとして信頼を築いてまいりました。

【制作会社・企業様向けサポート】
  • 専任エンジニアのいない企業様に対するシステム面の不安を解消
  • 柔軟な契約形態や短納期での対応により、急なニーズにも迅速にサポート
  • システムの企画段階から運用まで、ワンストップでのサービスを提供

Webシステムの開発やサイト改善でお困りの際は、どうぞお気軽にご相談ください。小さな疑問から大規模プロジェクトまで、最適なご提案を心を込めてさせていただきます。

ぜひ、プロフィールWeb制作会社様向け業務案内一般企業様向け業務案内もご覧くださいね。

Laravel 13のBladeにVue.jsを組み込む

2026.08.18

Laravel 13のBlade画面にVue.jsを部分的に組み込み、Controllerから渡した商品データを使って商品名を検索する方法を解説します。Viteの設定、BladeからVue.jsへの初期データの渡し方、Js::fromとdata-*属性の比較、npm run devによる確認方法、ブラウザ内検索とサーバー側検索の使い分けを扱います。

JavaScript Laravel PHP Vue.js

CodeIgniter 4のViewにVue.jsを組み込む

2026.08.20

CodeIgniter 4のView全体はサーバー側で描画したまま、一部分だけVue.jsを組み込む方法を解説します。Viteの開発サーバーからJavaScriptを読み込む方法、本番ビルドしたアセットをmanifestから取得してApacheで配信する方法、ControllerからViewへ渡したデータをVue.jsで検索する方法を確認します。

CodeIgniter JavaScript PHP Vue.js

MarkdownをHTMLへ変換する方法とXSS対策:PHP・JavaScript・Vue.jsの実装例

2026.08.12

PHP のleague/commonmarkと、Node.js のMarked・DOMPurifyを使い、MarkdownをHTMLへ変換する方法を紹介します。ユーザー入力やAI生成Markdownを未信頼入力として扱い、Vue.js のv-htmlへ安全に表示するための設定と注意点も整理します。

JavaScript PHP Vue.js

Laravel 13をApache・XAMPPで動かす:VirtualHost・.htaccess・public_htmlの配置方法

2026.08.27

Laravel 13のプロジェクトをWindowsのXAMPP Apacheで動かすために、VirtualHostでpublicをDocumentRootに指定する方法、.htaccessでpublicへ振り分ける方法、public_htmlへpublicの中身を配置してindex.phpの参照先を変更する方法を解説します。.envやstorageなどを公開しないための注意点、Viteのビルド成果物、ApacheとLaravelのログを使ったトラブルシューティングも整理します。

.htaccess Apache Laravel PHP XAMPP

阿部辰也へのお仕事の依頼・お問い合わせ

軽いご相談もお気軽にどうぞ!

個人情報の取り扱いについて *必須 プライバシーポリシーをご確認いただき、同意いただける場合は「同意する」にチェックをしてください。

keyboard_double_arrow_up
TOP