$ ls ./menu

© 2025 ESSA MAMDANI

cd ../blog
5 min read
Web Development

From Monolith to Modern: The Ultimate Guide to Migrating Laravel to Next.js

> Transition your Laravel application to a high-performance, headless architecture using Next.js. This guide covers authentication, data fetching strategies, and architectural shifts for elite developers.

Audio version coming soon
From Monolith to Modern: The Ultimate Guide to Migrating Laravel to Next.js
Verified by Essa Mamdani

For years, Laravel has been the undisputed king of developer productivity. Its "batteries-included" philosophy allowed us to build robust monoliths in record time. But the web has evolved. Users demand the instantaneous transitions of a Single Page Application (SPA) and the SEO performance of Server-Side Rendering (SSR). While Inertia.js is a fantastic bridge, if you want to truly scale, leverage the Edge, and tap into the massive React ecosystem, a migration to Next.js is the move.

I’ve overseen several migrations from Laravel Blade/Inertia setups to Next.js. It’s not just a syntax change; it’s a paradigm shift. Here’s how you handle it like a pro.


The Architectural Strategy: Headless Laravel

The biggest mistake developers make is trying to "convert" PHP to JavaScript. Don't do that. Instead, treat your existing Laravel application as a Headless API.

Laravel is an incredible backend framework. Its ORM (Eloquent), Service Container, and Job Queues are world-class. Your migration path should look like this:

  1. Transform Laravel controllers into API endpoints.
  2. Implement a robust Authentication bridge.
  3. Rebuild the UI in Next.js using the App Router.

Phase 1: Preparing the Laravel API

Before touching a single .tsx file, your backend must be ready to serve data. In a traditional Blade setup, you return views. In a headless setup, you return JSON.

Pro Tip: Use API Resources

Don't just return models. Use Laravel’s JsonResource to decouple your database schema from your frontend requirements. This prevents breaking the frontend when you rename a database column.

php
1// app/Http/Resources/UserResource.php
2public function toArray($request)
3{
4    return [
5        'id' => $this->id,
6        'name' => $this->name,
7        'email' => $this->email,
8        'avatar' => $this->profile_photo_url,
9        'joined_at' => $this->created_at->format('Y-m-d'),
10    ];
11}

CORS and Sanctum

You’ll likely be hosting your Next.js app on a different domain (or subdomain) than your API. Configure config/cors.php to allow your frontend URL and ensure EnsureFrontendRequestsAreStateful middleware is active in app/Http/Kernel.php if you are using session-based auth.


Phase 2: The Authentication Bridge

This is the "make or break" part of the migration. Laravel Sanctum is the gold standard here. You have two choices:

  1. Cookie-based Auth: Best if your Next.js app and API share a top-level domain (e.g., app.example.com and api.example.com).
  2. Token-based Auth (Bearer): Better for cross-domain or mobile integration.

For Next.js, I highly recommend using NextAuth.js (Auth.js). You can create a CredentialsProvider that proxies the login request to your Laravel backend.

typescript
1// pages/api/auth/[...nextauth].ts (or App Router equivalent)
2import CredentialsProvider from "next-auth/providers/credentials";
3
4export const authOptions = {
5  providers: [
6    CredentialsProvider({
7      name: "Laravel",
8      credentials: {
9        email: { label: "Email", type: "text" },
10        password: { label: "Password", type: "password" }
11      },
12      async authorize(credentials) {
13        const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/login`, {
14          method: 'POST',
15          body: JSON.stringify(credentials),
16          headers: { "Content-Type": "application/json" }
17        });
18        const user = await res.json();
19
20        if (res.ok && user) {
21          return user; // Contains your API token
22        }
23        return null;
24      }
25    })
26  ],
27  // ... callbacks to inject token into the session
28};

Phase 3: Data Fetching in the App Router

In Laravel, you fetch data in the controller and pass it to the Blade view. In Next.js (App Router), you fetch data directly inside Server Components. This is where the magic happens—no more loading spinners for initial page loads.

Server-Side Fetching (The SEO Winner)

tsx
1// app/dashboard/page.tsx
2async function getProjects() {
3  const res = await fetch(`${process.env.API_URL}/api/projects`, {
4    headers: {
5      'Authorization': `Bearer ${process.env.INTERNAL_API_KEY}`,
6    },
7    next: { revalidate: 3600 } // Cache for 1 hour (ISR)
8  });
9  
10  if (!res.ok) throw new Error('Failed to fetch data');
11  return res.json();
12}
13
14export default async function Dashboard() {
15  const projects = await getProjects();
16
17  return (
18    <main>
19      <h1>Your Projects</h1>
20      {projects.map(project => (
21        <ProjectCard key={project.id} data={project} />
22      ))}
23    </main>
24  );
25}

Client-Side Fetching (The UX Winner)

For highly interactive parts of your app (like a live search), use TanStack Query (React Query) or SWR. They provide caching, revalidation, and optimistic updates that Blade simply cannot match.


Phase 4: Handling Forms and Mutations

In Laravel, you used @csrf and POST actions. In Next.js, use Server Actions. They allow you to define a function that runs on the server, but you call it like a regular JavaScript function.

tsx
1// app/projects/create/page.tsx
2export default function CreateProject() {
3  async function create(formData: FormData) {
4    'use server';
5    
6    const rawFormData = {
7      title: formData.get('title'),
8      description: formData.get('description'),
9    };
10
11    // Send to Laravel API
12    await fetch(`${process.env.API_URL}/api/projects`, {
13      method: 'POST',
14      body: JSON.stringify(rawFormData),
15      // ... headers
16    });
17    
18    revalidatePath('/dashboard'); // Clear cache
19  }
20
21  return (
22    <form action={create}>
23      <input name="title" type="text" />
24      <textarea name="description" />
25      <button type="submit">Create</button>
26    </form>
27  );
28}

Phase 5: The "Essa" Pro-Tips for Elite Migrations

1. Typescript is Mandatory

Don't migrate to Next.js and use plain JavaScript. You're losing 50% of the benefit. Use a tool like spatie/laravel-typescript-transformer to automatically generate TypeScript interfaces from your Laravel Models and DTOs. This ensures your frontend and backend are always in sync.

2. Zod for Validation

Laravel has excellent validation. On the Next.js side, use Zod. It allows you to validate your API responses and form inputs with a schema-first approach.

3. Middleware for Protected Routes

Instead of checking for a session in every component, use Next.js Middleware to protect entire route segments. This mirrors Laravel’s auth middleware.

typescript
1// middleware.ts
2export { default } from "next-auth/middleware";
3
4export const config = { 
5  matcher: ["/dashboard/:path*", "/settings/:path*"] 
6};

4. Deployment Strategy

Host your Laravel API on Laravel Forge or Vapor (AWS Lambda). Host your Next.js frontend on Vercel. This gives you the best of both worlds: a robust, stateful PHP backend and a lightning-fast, globally distributed frontend.

Final Thoughts

The migration from Laravel to Next.js isn't about leaving Laravel behind—it's about promoting Laravel to its most powerful role: the data engine. By decoupling your frontend, you gain the ability to iterate faster, provide a superior user experience, and scale your infrastructure independently.

It’s a challenging transition, but for any elite developer looking to build modern, world-class applications, it’s the right one. Now go build something incredible.