Laravel + Inertia.js: Why I Stopped Building Separate APIs
For about three years, every full-stack project I worked on had the same architecture: a Laravel API on one side, a Vue or React SPA on the other, communicating over JSON. It works. But the overhead is real — you're essentially maintaining two codebases, two routers, two auth systems, and a contract between them that breaks in annoying ways.
Then I used Inertia.js seriously for the first time. That was two years ago. I haven't gone back to the separate API approach for a full-stack web app since.
The Basic Idea
Inertia isn't a framework — it's a protocol that connects your Laravel backend to your frontend framework (Vue, React, or Svelte). You write Laravel controllers that return Inertia responses instead of JSON. The frontend receives those responses as component props. Navigation between pages happens client-side without full reloads — but there's no REST API to maintain.
// Laravel controller — no API, just Inertia
class PostController extends Controller
{
public function index(): Response
{
return Inertia::render('Posts/Index', [
'posts' => Post::with('author')
->published()
->paginate(15),
]);
}
}
// Vue component receives posts as a prop automatically
<script setup>
defineProps({ posts: Object });
</script>
<template>
<article v-for="post in posts.data" :key="post.id">
<h2>{{ post.title }}</h2>
<Link :href="`/posts/${post.slug}`">Read more</Link>
</article>
<Pagination :links="posts.links" />
</template>
Forms Are Dramatically Simpler
Handling form validation errors in a traditional SPA means manually wiring up error display for each field. With Inertia's useForm, the server's validation errors automatically map to the form:
const form = useForm({
title: '',
content: '',
tags: [],
});
// Submit — server-side validation errors appear on $form.errors.fieldName
form.post(route('posts.store'), {
onSuccess: () => form.reset(),
});
And in your Blade/Inertia component:
<InputError :message="form.errors.title" />
That's it. No manual error state. No axios interceptors. The server speaks, the client listens.
Shared Data Across All Pages
Some data needs to be available everywhere — auth user, flash messages, permissions. With a REST API you'd need global state management (Vuex, Pinia). With Inertia, share it once from the middleware:
// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user()?->only('id', 'name', 'email', 'avatar'),
],
'flash' => [
'success' => $request->session()->get('success'),
'error' => $request->session()->get('error'),
],
];
}
Deferred Props for Performance
Inertia v2 added deferred props — you can mark expensive data to load after the initial page render. The page loads fast, then the data fills in:
return Inertia::render('Dashboard', [
'user' => $user,
'stats' => Inertia::defer(fn() => $this->expensiveDashboardStats()),
]);
<Deferred data="stats">
<template #fallback>
<StatsCardSkeleton />
</template>
<StatsCard :stats="stats" />
</Deferred>
What Inertia Isn't Good For
I want to be honest about the limitations. If you're building a public API consumed by mobile apps or third parties — Inertia doesn't help you there. You still need a REST or GraphQL API for that. Inertia is for web applications where the server and the frontend are tightly coupled and you control both sides.
It's also worth noting that SSR (server-side rendering) requires a Node.js process running alongside your PHP server. For most projects it's worth it for SEO, but it's an extra infrastructure piece to manage.
The Stack I Reach For
For new full-stack web applications: Laravel + Inertia.js + React (or Vue) + Tailwind. It's my default. The productivity boost over managing a separate API is significant, and the DX is genuinely excellent.