Inertia & Starter Kits
Building a Vue front end without an API layer, and what the starter kits give you.
There are two common ways to build a Vue front end on Laravel. The first is the one this site's tutorials use: Laravel serves a JSON API (/api/demo/...) and Vue fetches from it. The second is Inertia: you keep Laravel's routes and controllers exactly as for a Blade app, but each controller returns a Vue page component with props instead of a Blade view. There's no API to design, and no client-side router to maintain. Laravel's official Vue starter kit is built on Inertia, so it's how most new Laravel + Vue apps start.
Try it: the Inertia protocol, step by step
Click the links inside the simulated app. Each visit shows the request your browser would send and the page object Laravel would return. The project data is loaded from this site's real /api/demo/projects endpoint.
The first visit returns HTML because the browser needs a page to boot Vue. After that, every <Link> click sends X-Inertia: true, and Laravel answers with just { component, props, url, version }. The client looks up the component by name, passes it the props, and swaps it in without a full reload.
The controller: same as Blade, different return
Here is this app's real API controller for the projects list, which returns JSON for Vue to fetch:
app/Http/Controllers/Api/ProjectController.php
public function index(): AnonymousResourceCollection
{
$projects = Project::query()
->with('owner')
->withCount(['tasks', 'tasks as open_tasks_count' => fn (Builder $query) => $query->open()])
->orderBy('name')
->get();
return ProjectResource::collection($projects);
}
The Inertia version of the same page keeps the query and returns a page component instead:
// Illustrative — app/Http/Controllers/ProjectController.php in an Inertia app
use Inertia\Inertia;
use Inertia\Response;
public function index(): Response
{
$projects = Project::query()
->with('owner')
->withCount(['tasks', 'tasks as open_tasks_count' => fn (Builder $query) => $query->open()])
->orderBy('name')
->get();
return Inertia::render('Projects/Index', [
'projects' => ProjectResource::collection($projects),
'canCreate' => request()->user()->can('create', Project::class),
]);
}
Authentication, middleware, policies, validation, sessions and redirects all work exactly as in the Blade lessons. That's the main appeal: you write a classic server-side app and get a single-page-app experience.
The page component
'Projects/Index' maps to resources/js/pages/Projects/Index.vue. Props arrive through defineProps, and <Link> replaces <a> for in-app navigation:
<!-- Illustrative — resources/js/pages/Projects/Index.vue -->
<script setup lang="ts">
import { Head, Link } from '@inertiajs/vue3'
import AppLayout from '@/layouts/AppLayout.vue'
defineProps<{
projects: { data: Array<{ id: number; name: string; slug: string; open_tasks_count: number }> }
canCreate: boolean
}>()
</script>
<template>
<AppLayout>
<Head title="Projects" />
<h1>Projects</h1>
<Link v-if="canCreate" href="/projects/create">New project</Link>
<ul>
<li v-for="project in projects.data" :key="project.id">
<Link :href="`/projects/${project.slug}`">{{ project.name }}</Link>
({{ project.open_tasks_count }} open)
</li>
</ul>
</AppLayout>
</template>
Forms and validation errors
Inertia's useForm posts to a normal Laravel route. When validation fails, Laravel redirects back as usual, and Inertia puts the errors on form.errors, field by field. There's no 422 handling to write:
<!-- Illustrative — resources/js/pages/Tasks/Create.vue -->
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3'
const props = defineProps<{ projectId: number }>()
const form = useForm({ project_id: props.projectId, title: '', priority: 'medium' })
function submit() {
form.post('/tasks', { onSuccess: () => form.reset('title') })
}
</script>
<template>
<form @submit.prevent="submit">
<input v-model="form.title" />
<p v-if="form.errors.title" class="error">{{ form.errors.title }}</p>
<button :disabled="form.processing">Add task</button>
</form>
</template>
On the server, the controller is the same one you'd write for Blade, using this app's real StoreTaskRequest:
// Illustrative
public function store(StoreTaskRequest $request): RedirectResponse
{
$task = Task::create($request->validated());
return to_route('projects.show', $task->project)->with('success', 'Task added.');
}
Compare that with the Form Validation lesson, where the Vue side maps a JSON 422 response onto fields by hand, because this site uses a separate API.
Shared data, partial reloads and Inertia 2 features
Shared data is available to every page: the logged-in user, flash messages, app name. It's defined once in the HandleInertiaRequests middleware:
// Illustrative — app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => ['user' => $request->user()?->only('id', 'name', 'email')],
'flash' => ['success' => fn () => $request->session()->get('success')],
];
}
Pages read it with usePage().props.auth.user.
Inertia 2 adds tools for large pages:
- Partial reloads:
router.reload({ only: ['tasks'] })re-runs the controller but sends back only the named props. Wrap expensive props in closures so they're only computed when requested. - Deferred props:
Inertia::defer(fn () => $stats->get())renders the page first and loads slow data in a follow-up request. - Prefetching:
<Link prefetch>loads the next page on hover. - Polling (
usePoll(5000)) and infinite scrolling with merged props.
The Vue starter kit
laravel new my-app, then choose Vue when asked for a starter kit, and you get a working Inertia app:
- Inertia 2, Vue 3 with
<script setup>and TypeScript, Vite and Tailwind CSS - UI components from shadcn-vue that you own and can edit
- Login, registration, password reset, email verification and profile/settings pages, all wired up
- Pest or PHPUnit tests for the authentication flows
- Optional WorkOS AuthKit for social login and SSO
laravel new my-app # choose "Vue" as the starter kit
cd my-app && composer run dev
The parts you'll edit first:
resources/js/pages/ # one .vue file per page, e.g. Dashboard.vue, auth/Login.vue
resources/js/layouts/ # AppLayout.vue, AuthLayout.vue
resources/js/components/ui/ # the shadcn-vue components
routes/web.php, routes/auth.php, routes/settings.php
app/Http/Controllers/Auth/ # the controllers behind the auth pages
React and Livewire starter kits work the same way. Breeze and Jetstream, the older kits, no longer receive new features.
Inertia or a separate API?
| Choose Inertia when… | Choose a JSON API when… |
|---|---|
| One Laravel app serves its own Vue front end | Several clients use the data (mobile apps, partners, other services) |
| You want server-side routing, auth and validation | The front end is deployed separately or built by another team |
| SEO matters and you can use Inertia's SSR | You need a public, versioned, documented API |
| You'd rather not design and version endpoints | Pages are embedded widgets inside a server-rendered site (like this one) |
This site is the second case: Blade pages with independent Vue widgets that each call /api/demo. For the front-end side of both approaches, see Vue + Laravel in Production.
Key takeaways
- Inertia lets controllers return
Inertia::render('Page/Name', $props). Laravel keeps routing, auth and validation; Vue renders the page. - The first request returns HTML with the page object embedded; later navigations send
X-Inertia: trueand receive JSON. useFormposts to normal routes and receives validation errors automatically. Shared props come fromHandleInertiaRequests::share().- Inertia 2 adds partial reloads, deferred props, prefetching and polling for heavier pages.
- The official Vue starter kit (
laravel new, choose Vue) gives you Inertia, TypeScript, Tailwind and complete auth pages to build on. - Use Inertia for a single app's own front end; build a JSON API when other clients need the data.