React · Full Stack · Laravel

React + Laravel: The Full Stack Setup I Use on Every Project

AuthorKarthik Mani
PublishedMarch 18, 2025
Read Time10 min read
CategoryReact · Laravel · TypeScript
React and Laravel full stack development TypeScript code
The React + Laravel stack — two mature, opinionated tools that work remarkably well together
Table of Contents
  1. Why React + Laravel in 2025
  2. SPA vs Inertia.js: Making the Right Choice
  3. Authentication with Laravel Sanctum
  4. TypeScript API Contracts
  5. Vite, TanStack Query and the Frontend Layer
  6. API Design Principles That Age Well
  7. Deployment: Keeping the Stack Lean
  8. Final Thoughts

There is no shortage of full stack opinions on the internet. Everyone has a take on which framework you should be using, which ORM is most performant, which frontend library will make your code sing. After 20 years of building web applications professionally — across PHP, Laravel, React, Node, and more stacks than I care to count — I've converged on one combination that I reach for by default on almost every new project: Laravel on the backend, React on the frontend.

This isn't a hot take and it isn't tribal loyalty. It's the output of shipping real products to real users and learning what breaks down under pressure and what doesn't. The React + Laravel stack is mature, well-documented, has a massive hiring pool, and has precisely the right level of convention — enough to move fast, not so much that it gets in your way.

This article is a precise, opinionated description of the setup I actually use. Not a survey of every option; a concrete recommendation with the reasoning behind it.

"Picking a stack is like picking a city to build your home in. Don't optimise for the newest or the trendiest. Optimise for the one that has infrastructure, community, and a future — and that your team can actually move fast in."

Why React + Laravel in 2025

The argument for Laravel as a full stack developer's backend has only gotten stronger over the years. Its ecosystem is exceptionally rich: queues, websockets via Reverb, scheduled tasks, file storage, email, testing — it's all there and it all follows consistent conventions. When I pick up a Laravel project someone else started, I can orient myself in minutes. That's priceless in a team environment.

React's case is equally strong on the frontend. The component model maps naturally to how modern UIs are actually structured. The hook system is genuinely ergonomic once you've internalized it. And the ecosystem — TanStack Query for server state, Zustand for client state, React Router or TanStack Router for navigation — is stable and battle-tested.

Together they form a clean separation: Laravel is a headless API engine; React is the rendering layer. Each does what it's best at with a well-defined boundary between them. That boundary is where most of the interesting architectural decisions live, and it's what we'll spend most of this article on.

SPA vs Inertia.js: Making the Right Choice

Before you wire React and Laravel together, you have a fundamental architectural question to answer: do you want a fully decoupled SPA communicating with a JSON API, or do you want to use Inertia.js to keep the backend driving your routes while React handles the rendering?

FactorDecoupled SPAInertia.js
Auth complexityHigher (CORS, tokens, cookie config)Low (Laravel session auth)
SEONeeds SSR or prerenderingGood out of the box
DeploymentTwo separate apps/hostsSingle Laravel app
Team flexibilityFrontend team can work fully independentlyCloser coupling
API reusabilitySame API serves mobile apps, third partiesPurpose-built for one frontend
Best forSaaS with mobile app, public APIInternal tools, admin panels, content apps

My decision tree is simple: if the product will eventually have a mobile app or needs to expose a public API, I build a decoupled SPA from day one. If it's an internal tool, admin panel, or a product where SEO is important and we don't anticipate a mobile app, I reach for Inertia.js with React — it removes so much ceremony and lets the team ship faster.

Authentication with Laravel Sanctum

If you're going the decoupled SPA route, Laravel Sanctum is the right authentication layer. Not Passport — Sanctum. Passport is OAuth2 and it's genuinely complex; you don't need OAuth2 for your own first-party SPA. Sanctum gives you cookie-based session authentication for browser clients and token-based authentication for mobile or third-party API clients, through the same middleware.

The most common mistake I see developers make with Sanctum is not configuring the stateful domains correctly. Here's exactly what that looks like:

// config/sanctum.php
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS',
    sprintf('%s%s', 'localhost,localhost:3000,127.0.0.1',
    env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),

// config/cors.php — critical pieces
'allowed_origins'     => [env('FRONTEND_URL', 'http://localhost:3000')],
'supports_credentials' => true,  // Never forget this

On the React side, you must configure your HTTP client to send credentials with every request. If you're using Axios:

// lib/axios.ts
import axios from 'axios';

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  withCredentials: true,   // Send cookies cross-origin
  headers: {
    'Accept': 'application/json',
    'X-Requested-With': 'XMLHttpRequest',
  },
});

export default api;

The authentication flow for cookie-based Sanctum is: hit /sanctum/csrf-cookie first to get the XSRF token, then POST credentials to /login. After that, every subsequent request automatically includes the session cookie and XSRF header, and Laravel treats you as authenticated. Clean, stateless on the client, stateful on the server.

Security Note

Always set SESSION_DOMAIN to .yourdomain.com (note the leading dot) in production so the session cookie is accessible to your frontend subdomain. And always run both your Laravel API and React frontend on the same top-level domain in production — Sanctum's cookie auth breaks across different domains by design.

TypeScript API Contracts

This is the part of the React + Laravel full stack setup that pays the biggest dividends over time. Most teams treat the API contract as implicit — the backend returns whatever it returns, and the frontend figures it out. This works until someone renames a field, and then you have a runtime bug in production that a type system would have caught at compile time.

My approach is to define TypeScript interfaces that mirror your Laravel API Resource output, and treat these as the single source of truth for the frontend:

// types/api.ts — mirrors Laravel API Resources

export interface User {
  id:         number;
  name:       string;
  email:      string;
  avatar_url: string | null;
  tenant_id:  number;
  created_at: string;
}

export interface Tenant {
  id:        number;
  name:      string;
  subdomain: string;
  plan:      'starter' | 'pro' | 'enterprise';
}

export interface PaginatedResponse<T> {
  data:  T[];
  meta: {
    current_page: number;
    last_page:    number;
    per_page:     number;
    total:        number;
  };
}

// API error shape from Laravel's validation
export interface ValidationError {
  message: string;
  errors:  Record<string, string[]>;
}

On the Laravel side, keep your API Resources aligned with these interfaces. When you change a Resource, update the TypeScript interface. This discipline sounds tedious but it's far less tedious than debugging a production data mismatch at midnight.

For teams that want to automate this, tools like Scramble (a Laravel package) can generate OpenAPI specs from your Laravel routes and controllers, which you can then run through openapi-typescript to generate TypeScript types automatically. I've used this on larger teams and it's a genuine quality-of-life improvement.

Vite, TanStack Query and the Frontend Layer

Laravel ships with Vite configured out of the box since Laravel 9, and it's genuinely fast. For a decoupled SPA, I scaffold the React project separately with npm create vite@latest and keep it in a sibling directory or a separate repository depending on team size.

For server state management — fetching, caching, invalidating, and synchronising API data — I use TanStack Query (formerly React Query). It handles loading states, error states, cache invalidation, background refetching, and optimistic updates out of the box. The difference in DX versus managing this manually with useEffect and useState is staggering.

// hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '../lib/axios';
import type { PaginatedResponse, User } from '../types/api';

export function useUsers(page = 1) {
  return useQuery({
    queryKey: ['users', page],
    queryFn:  () =>
      api.get<PaginatedResponse<User>>('/api/users', { params: { page } })
         .then(r => r.data),
    staleTime: 30_000, // 30 seconds before re-fetch
  });
}

export function useDeleteUser() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: (id: number) => api.delete(`/api/users/${id}`),
    onSuccess: () => qc.invalidateQueries({ queryKey: ['users'] }),
  });
}

For global client state — things like UI preferences, sidebar open/closed, current theme — I use Zustand. It's tiny, it doesn't require a provider, and its API is the simplest of any state library I've used. Don't reach for Redux in 2025 unless you have a genuinely complex, deeply interactive UI that needs time-travel debugging.

API Design Principles That Age Well

The Laravel API layer is where discipline pays the most dividends over a long project. A few principles I follow without exception:

Deployment: Keeping the Stack Lean

For most Laravel + React SaaS applications, my deployment setup is deliberately simple. The Laravel app runs on a VPS or a managed platform like Render or Railway, fronted by Nginx. The React SPA is built with npm run build and deployed to a CDN — Cloudflare Pages or Vercel work perfectly.

Environment-specific configuration lives in .env files on the server and in the platform's environment variable settings for the frontend. The key environment variable on the React side is VITE_API_URL — point it at your Laravel API domain and everything else flows from there.

For the Laravel side, a standard production checklist looks like this:

  1. Run php artisan config:cache, route:cache, view:cache on every deploy
  2. Run migrations with php artisan migrate --force
  3. Restart the queue workers after each deploy
  4. Confirm APP_DEBUG=false and APP_ENV=production are set
  5. Set SANCTUM_STATEFUL_DOMAINS to your production frontend domain

Final Thoughts

The React + Laravel stack is not exciting in the way that new JavaScript frameworks are exciting. It doesn't generate conference talks or Twitter threads. What it does do is let you ship reliable, maintainable software to real users, iterate quickly when requirements change, and hand the codebase off to a new team member without a week of onboarding.

I've built on a lot of stacks over two decades. This is the one I keep coming back to. If you're starting a new full stack web application in 2025 and you want something that will be productive today and still make sense two years from now, React + Laravel is a genuinely excellent choice.

The setup I've described above isn't the only way to wire these two tools together — but it's the result of many iterations, many production bugs, and many projects. It's what I'd build with if I were starting from scratch tomorrow.

Karthik Mani — Full Stack Developer PHP Laravel React Tamil Nadu
Karthik Mani
Full Stack Developer & IT Lead · SWOT Solutions
20+ years building PHP, Laravel, React and SaaS applications from Tamil Nadu, India. Currently available for full-time and contract work. Connect on LinkedIn →