Back to Read More
LaravelVueInertia.js

Full-Stack App with Laravel, Inertia & Vue

Dec 20, 2025

Building a modern full-stack application with Laravel, Inertia.js, and Vue gives you the best of both worlds: Laravel's powerful backend with Vue's reactive frontend - without building a separate API. Inertia.js acts as the glue, letting you build single-page apps using classic server-side routing.

What is Inertia.js?

Inertia.js is not a framework - it's a routing library that connects your server-side framework (Laravel) with your client-side framework (Vue). Instead of building an API + SPA separately, Inertia lets you:

  • ● Use Laravel routes and controllers as usual
  • ● Return Vue pages instead of Blade views
  • ● Get SPA-like navigation without page reloads
  • ● No need to build a REST/GraphQL API

1. Project Setup

Start by creating a new Laravel project and installing the Inertia.js server-side adapter, then set up Vue with the client-side adapter.

Install Laravel & Inertia Server-Side

# Create a new Laravel project
composer create-project laravel/laravel my-app
cd my-app

# Install Inertia server-side
composer require inertiajs/inertia-laravel

# Publish the middleware
php artisan inertia:middleware

Install Vue & Inertia Client-Side

# Install Vue 3, Inertia client adapter & Vite plugin
npm install vue@3 @inertiajs/vue3
npm install -D @vitejs/plugin-vue

2. Configuration

Root Blade Template

Create the root template that Inertia uses to boot your Vue app. This replaces your usual Blade layout.

<!-- resources/views/app.blade.php -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    @vite('resources/js/app.js')
    @inertiaHead
</head>
<body>
    @inertia
</body>
</html>

Vite Configuration

// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/js/app.js'],
            refresh: true,
        }),
        vue({
            template: {
                transformAssetUrls: {
                    base: null,
                    includeAbsolute: false,
                },
            },
        }),
    ],
});

Vue App Entry Point

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

createInertiaApp({
    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);
    },
});

3. Routing & Controllers

With Inertia, you use standard Laravel routes. Instead of returning a Blade view, you return an Inertia::render() response that maps to a Vue page component.

Routes

// routes/web.php
use App\Http\Controllers\PostController;

Route::get('/', function () {
    return Inertia::render('Home');
});

Route::resource('posts', PostController::class);

Controller

// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;
use Inertia\Inertia;

class PostController extends Controller
{
    public function index()
    {
        return Inertia::render('Posts/Index', [
            'posts' => Post::latest()
                ->paginate(10)
                ->through(fn ($post) => [
                    'id'    => $post->id,
                    'title' => $post->title,
                    'date'  => $post->created_at->format('M d, Y'),
                ]),
        ]);
    }

    public function show(Post $post)
    {
        return Inertia::render('Posts/Show', [
            'post' => $post->only('id', 'title', 'body', 'created_at'),
        ]);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|max:255',
            'body'  => 'required',
        ]);

        Post::create($validated);

        return redirect()->route('posts.index')
            ->with('success', 'Post created!');
    }
}

4. Vue Page Components

Vue pages live in resources/js/Pages/. Props passed from the controller are automatically available. Use defineProps to type them.

Posts List Page

<!-- resources/js/Pages/Posts/Index.vue -->
<script setup>
import { Link } from '@inertiajs/vue3';

defineProps({
    posts: Object,
});
</script>

<template>
    <div class="max-w-4xl mx-auto py-12 px-6">
        <h1 class="text-3xl font-bold mb-8">All Posts</h1>

        <div v-for="post in posts.data" :key="post.id"
             class="mb-4 p-6 bg-white rounded-xl shadow-sm">
            <Link :href="`/posts/${post.id}`"
                  class="text-xl font-semibold hover:text-blue-600">
                {{ post.title }}
            </Link>
            <p class="text-gray-500 text-sm mt-1">{{ post.date }}</p>
        </div>
    </div>
</template>

Create Post Page with Form

<!-- resources/js/Pages/Posts/Create.vue -->
<script setup>
import { useForm } from '@inertiajs/vue3';

const form = useForm({
    title: '',
    body: '',
});

const submit = () => {
    form.post('/posts');
};
</script>

<template>
    <div class="max-w-2xl mx-auto py-12 px-6">
        <h1 class="text-3xl font-bold mb-8">Create Post</h1>

        <form @submit.prevent="submit" class="space-y-6">
            <div>
                <label class="block font-medium mb-1">Title</label>
                <input v-model="form.title" type="text"
                       class="w-full border rounded-lg px-4 py-2" />
                <p v-if="form.errors.title"
                   class="text-red-500 text-sm mt-1">
                    {{ form.errors.title }}
                </p>
            </div>

            <div>
                <label class="block font-medium mb-1">Body</label>
                <textarea v-model="form.body" rows="6"
                          class="w-full border rounded-lg px-4 py-2" />
                <p v-if="form.errors.body"
                   class="text-red-500 text-sm mt-1">
                    {{ form.errors.body }}
                </p>
            </div>

            <button type="submit"
                    :disabled="form.processing"
                    class="bg-blue-600 text-white px-6 py-2 rounded-lg">
                {{ form.processing ? 'Saving...' : 'Create Post' }}
            </button>
        </form>
    </div>
</template>

5. Shared Data & Persistent Layouts

Share data globally (like the authenticated user) using Inertia's HandleInertiaRequests middleware, and use persistent layouts to keep UI state between page visits.

Shared Data (Middleware)

// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
    return [
        ...parent::share($request),
        'auth' => [
            'user' => $request->user()
                ? $request->user()->only('id', 'name', 'email')
                : null,
        ],
        'flash' => [
            'success' => $request->session()->get('success'),
            'error'   => $request->session()->get('error'),
        ],
    ];
}

Persistent Layout

<!-- resources/js/Layouts/AppLayout.vue -->
<script setup>
import { Link, usePage } from '@inertiajs/vue3';

const { auth, flash } = usePage().props;
</script>

<template>
    <div class="min-h-screen bg-gray-100">
        <nav class="bg-white shadow px-6 py-4 flex justify-between">
            <Link href="/" class="font-bold text-xl">MyApp</Link>
            <div v-if="auth.user">{{ auth.user.name }}</div>
        </nav>

        <!-- Flash Messages -->
        <div v-if="flash.success"
             class="bg-green-100 text-green-700 px-6 py-3">
            {{ flash.success }}
        </div>

        <main>
            <slot />
        </main>
    </div>
</template>

<!-- Use it in a page: -->
<!-- Posts/Index.vue -->
<script>
import AppLayout from '@/Layouts/AppLayout.vue';
export default { layout: AppLayout };
</script>

6. Key Inertia Features

Preserve Scroll & State

<!-- Keep scroll position on navigation -->
<Link href="/posts" preserve-scroll>Posts</Link>

<!-- Partial reloads - only refresh specific props -->
import { router } from '@inertiajs/vue3';

router.reload({ only: ['posts'] }); // Only refresh posts data

// Programmatic navigation
router.visit('/posts');
router.post('/posts', { title: 'Hello', body: 'World' });
router.delete(`/posts/${id}`);

Head & SEO

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

<template>
    <Head>
        <title>My Page Title</title>
        <meta name="description" content="Page description for SEO" />
    </Head>

    <!-- Page content -->
</template>

Project Structure

my-app/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ Http/
β”‚   β”‚   β”œβ”€β”€ Controllers/
β”‚   β”‚   β”‚   └── PostController.php
β”‚   β”‚   └── Middleware/
β”‚   β”‚       └── HandleInertiaRequests.php
β”‚   └── Models/
β”‚       └── Post.php
β”œβ”€β”€ resources/
β”‚   β”œβ”€β”€ js/
β”‚   β”‚   β”œβ”€β”€ app.js              # Vue entry point
β”‚   β”‚   β”œβ”€β”€ Pages/
β”‚   β”‚   β”‚   β”œβ”€β”€ Home.vue
β”‚   β”‚   β”‚   └── Posts/
β”‚   β”‚   β”‚       β”œβ”€β”€ Index.vue
β”‚   β”‚   β”‚       β”œβ”€β”€ Show.vue
β”‚   β”‚   β”‚       └── Create.vue
β”‚   β”‚   β”œβ”€β”€ Components/
β”‚   β”‚   β”‚   β”œβ”€β”€ Pagination.vue
β”‚   β”‚   β”‚   └── FlashMessage.vue
β”‚   β”‚   └── Layouts/
β”‚   β”‚       └── AppLayout.vue
β”‚   └── views/
β”‚       └── app.blade.php       # Inertia root template
β”œβ”€β”€ routes/
β”‚   └── web.php
└── vite.config.js

Why Choose Laravel + Inertia + Vue?

1

No API needed - Controllers pass data directly to Vue pages. No REST endpoints, no serialization boilerplate.

2

Server-side routing - Use Laravel's route model binding, middleware, and policies as normal. No client-side router needed.

3

SPA experience - Page visits happen via XHR, no full page reloads. Browser history, scroll position, and component state are preserved.

4

Form handling - The useForm() helper handles validation errors, progress state, and file uploads out of the box.

5

Full Laravel ecosystem - Authentication (Breeze/Jetstream), queues, notifications, broadcasting - everything works seamlessly with Inertia.

Summary

  • βœ“ Inertia.js bridges Laravel and Vue without building an API
  • βœ“ Controllers return Inertia::render() with props passed to Vue pages
  • βœ“useForm() handles forms, validation errors, and loading states
  • βœ“ Share global data via HandleInertiaRequests middleware
  • βœ“ Use persistent layouts to keep state across navigations
  • βœ“Partial reloads for efficient data fetching

Β© 2026 Koeuk KOS. All rights reserved.

Built with Nuxt.js, Vue.js & Tailwind CSS