By day 70 of your PHP and Laravel journey, you've built controllers, models, and APIs—now comes the question every full-stack team faces: how do we connect our backend to modern frontend components? Working with Laravel and Vue or React: a practical guide walks through the three most viable paths we've used in production, each with trade-offs that matter more than any framework benchmark.
The choice between these approaches shapes your team's velocity, your deployment pipeline, and your ability to hire. We'll cover real scenarios you'll encounter, the code you'll actually write, and the mistakes that cost teams weeks of refactoring.
Separate SPA + API — Sanctum cookie auth on the same domain
The cleanest mental model: build Laravel as a pure JSON API, serve Vue or React as a separate single-page application from the same domain. Your frontend lives in /public/spa, your API lives in /api. They're technically separate but unified under one domain in production.
This works best when your frontend team is strong and independent. They can develop offline, run their own tests, and deploy on their own schedule. You avoid server-side rendering complexity and lean into the modern SPA ecosystem.
Authentication stays stateless with Laravel Sanctum. When the user logs in, Sanctum creates a cookie-based token stored in the browser automatically.
// Laravel API route
Route::post('/login', function (Request $request) {
$user = User::where('email', $request->email)->first();
if (!Hash::check($request->password, $user->password)) {
return response()->json(['message' => 'Invalid credentials'], 401);
}
return response()->json([
'token' => $user->createToken('auth-token')->plainTextToken,
'user' => $user,
]);
});
// React component
import { useState } from 'react';
import axios from 'axios';
export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleLogin = async () => {
const response = await axios.post('/api/login', { email, password });
localStorage.setItem('token', response.data.token);
window.location.href = '/dashboard';
};
return (
<>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button onClick={handleLogin}>Login</button>
</>
);
}
The upside: your team can reason about the API independently from UI concerns. A Vue developer and a React developer could both consume the same endpoints. You get full control over API versioning and documentation.
The downside: you're managing two separate build pipelines, two deployments, and two dependency trees. CORS becomes a concern (though mitigated by same-domain setup). You need solid API contracts or miscommunications happen fast.
We've used this pattern for a ride-hailing CRM where the frontend team was 60% of the engineering staff and needed autonomy. The separation forced us to document our API early, which actually saved debugging time later.
Inertia — Server-side routing with client-side components
Inertia blurs the line between a Laravel monolith and a modern SPA. Your Laravel routes return Vue or React components directly, and Inertia handles the handoff seamlessly. It feels like you're still doing server-side rendering, but the interactivity is client-side.
The mental model: Laravel owns the routing and data fetching. Vue or React owns the rendering. You keep them tightly coupled within a single deploy artifact.
This is where we reach for Inertia when the frontend and backend are equally skilled in their domains but want to stay in the same codebase. Your Laravel developers can keep writing controllers, but they're now returning component props instead of blade templates.
// Laravel controller
class PostController extends Controller
{
public function index()
{
$posts = Post::paginate(15);
return Inertia::render('Posts/Index', [
'posts' => $posts,
]);
}
public function store(Request $request)
{
$post = Post::create($request->validated());
return redirect()->route('posts.show', $post);
}
}
// resources/js/Pages/Posts/Index.jsx
import React from 'react';
import { Link } from '@inertiajs/react';
export default function Index({ posts }) {
return (
<div>
<h1>Posts</h1>
{posts.data.map(post => (
<div key={post.id}>
<Link href={`/posts/${post.id}`}>
{post.title}
</Link>
</div>
))}
</div>
);
}
Inertia handles navigation without full page reloads. When you follow a Link, it fetches the new component props and re-renders. It feels like a SPA, but you're not writing API endpoints—your controllers return data structures directly.
The upside: simpler mental model than separate APIs. You stay in Laravel for routing, validation, and authorization. Deployment is one pipeline, one database, one server. Great for teams that are full-stack or lean toward backend.
The downside: you can't easily separate frontend and backend work. Mobile apps need a separate API. Your frontend has less independence. Inertia adds some magic that takes time to internalize.
We've used Inertia for e-commerce projects where the team was 70% backend and the frontend work was primarily UI and form handling. It kept them shipping faster than setting up two separate systems.
Livewire — Stay in PHP for most interactivity
If you want to stay in PHP and Blade templates for as much as possible, Livewire lets you build reactive components without writing JavaScript. You define component state and actions in PHP, and Livewire syncs them to the frontend via WebSockets or AJAX.
Livewire is a good fit when your frontend needs are moderate: form validation, filtering, pagination, toggling visibility. It's not a great fit for highly interactive UIs or animations.
// app/Livewire/FilterPosts.php
namespace App\Livewire;
use Livewire\Component;
use Livewire\Attributes\Url;
use App\Models\Post;
class FilterPosts extends Component
{
#[Url]
public string $search = '';
public function render()
{
$posts = Post::where('title', 'like', "%{$this->search}%")
->paginate(15);
return view('livewire.filter-posts', [
'posts' => $posts,
]);
}
}
// resources/views/livewire/filter-posts.blade.php
<div>
<input wire:model.live="search" placeholder="Search posts..." />
@foreach ($posts as $post)
<div>
<h3>{{ $post->title }}</h3>
<p>{{ $post->excerpt }}</p>
</div>
@endforeach
{{ $posts->links() }}
</div>
Livewire updates happen over the wire. When the user types in the search field, wire:model.live sends the input to the server, the component re-renders, and Livewire patches the DOM with just what changed. No manual API calls, no state management library.
The upside: you can build surprisingly interactive apps without leaving PHP or Blade. Onboarding new developers is fast if they know Laravel. You get real-time reactivity without WebSocket complexity.
The downside: Livewire requests add latency. For every keystroke or interaction, there's a round-trip to the server. It's not suitable for apps that need snappy, offline-first interactions. You're still constrained by Blade's templating limitations.
We've used Livewire for internal dashboards and admin tools where the team was PHP-first and the users could tolerate 50-100ms latency on interactions. It got us shipping features in half the time compared to building a React dashboard.
Pick by team — Use what your frontend developers know best
The "best" approach depends on your team's composition and constraints, not on the framework itself.
If your team has strong React developers and you need mobile apps, go with a separate SPA + API. You'll have redundancy in your API, but that's often worth it for team velocity.
If your team is full-stack and you want to ship a web app quickly, use Inertia. It's the fastest path from zero to production for teams that can do both backend and frontend competently.
If your team is PHP-first, conservative about dependencies, and building internal tools or simple web apps, use Livewire. You'll spend less time debugging WebSocket issues than learning React.
There's no single correct answer. We've shipped production systems using all three in the same year, on different projects, with different team compositions. The question isn't "which is best?" but "which fits us best right now?"
Common mistakes
- Over-engineering the API: teams build a complex API layer when Inertia or Livewire would serve the need. You don't need REST perfection for a dashboard.
- Assuming authentication is simple: cookie-based auth, token refresh, CORS, same-site policies—they intersect in ways that surprise people. Test your auth flow early.
- Mixing approaches mid-project: starting with Inertia and adding a separate React app for "future mobile support" creates debt. Choose one path and commit to it for the sprint.
- Not documenting the contract: in a separate SPA + API setup, without documentation, you get desynchronization fast. Write it down early, even if it's simple.
- Deploying frontend and backend separately without versioning: if you separate them, version your API. Breaking changes break users.
Key takeaways
- Separate SPA + API gives maximum team independence but requires two deployments and careful API contract management.
- Inertia keeps you in one Laravel codebase with modern SPA interactivity, ideal for full-stack or backend-heavy teams.
- Livewire stays in PHP and Blade, great for internal tools and moderate interactivity, but adds server latency.
- Your choice should be driven by team strengths, not framework hype. A team shipping with Livewire beats a team stuck debating React architecture.
- Authentication and API contracts are the real complexity in all three approaches—invest time there early.






Comments
Loading comments…