Next.js 16 with App Router: practical guide for Laravel developers
Why Laravel Developers Should Care About Next.js 16
If you have spent years building applications with Laravel, you are likely comfortable with its elegant syntax, robust ecosystem, and opinionated structure. The shift to modern frontend frameworks can feel overwhelming, especially when you encounter the fragmented landscape of React tooling. Next.js 16 with the App Router offers a compelling path forward that will feel surprisingly familiar to Laravel developers who appreciate convention over configuration.
This guide assumes you understand Laravel's routing, Blade templating, Eloquent ORM, and middleware concepts. We will map these familiar patterns to Next.js 16's App Router architecture, providing practical code examples you can implement immediately.
Understanding the Mental Model Shift
Laravel structures applications around server-rendered pages with optional JavaScript sprinkles. Routes point to controllers that return views. Data fetching happens on the server. The App Router in Next.js 16 inverts some of this thinking while preserving what makes server-side rendering powerful.
In Laravel, your mental model looks like this:
- Routes define URL patterns
- Controllers handle business logic
- Blade views render HTML
- Middleware intercepts requests
Next.js 16 with App Router collapses some of these layers. Files in your app/ directory simultaneously define routes, data fetching, and rendering logic. This sounds chaotic until you recognize the convention-based pattern Laravel developers already love.
File-Based Routing vs Laravel's Route Definitions
Laravel developers define routes explicitly in routes/web.php:
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PostController;
Route::get('/posts', [PostController::class, 'index']);
Route::get('/posts/{slug}', [PostController::class, 'show']);
Next.js 16 eliminates this configuration file entirely. Instead, the filesystem becomes your router:
// app/posts/page.tsx
export default async function PostsPage() {
const posts = await fetchPosts();
return (
<main>
<h1>All Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<a href={`/posts/${post.slug}`}>{post.title}&;</a>
</li>
))}
</ul>
</main>
);
}
// app/posts/[slug]/page.tsx
export default async function PostPage({
params
}: {
params: { slug: string }
}) {
const post = await fetchPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
The dynamic segment [slug] corresponds directly to Laravel's route parameters. Nested folders create nested URL paths, identical to how Laravel's route grouping works with dot notation. The critical difference: every page component in Next.js 16 defaults to Server Component behavior, meaning your data fetching executes on the server without explicit API endpoints.
Server Components: Replacing Controller Logic
Laravel controllers encapsulate request handling. Consider a typical show method:
namespace App\Http\Controllers;
class PostController extends Controller
{
public function show($slug)
{
$post = Post::where('slug', $slug)
->with('author', 'tags')
->firstOrFail();
return view('posts.show', compact('post'));
}
}
Next.js 16 Server Components absorb this responsibility. The component itself executes exclusively on the server, executing database queries or API calls directly:
import { notFound } from 'next/navigation';
import { db } from '@/lib/db';
export default async function PostPage({
params
}: {
params: { slug: string }
}) {
// This executes only on the server
const post = await db.query.posts.findFirst({
where: (posts, { eq }) => eq(posts.slug, params.slug),
with: {
author: true,
tags: true,
},
});
if (!post) {
notFound(); // Equivalent to abort(404)
}
return (
<article className="max-w-2xl mx-auto">
<header className="mb-8">
<h1 className="text-3xl font-bold">{post.title}</h1>
<p className="text-gray-600">By {post.author.name}</p>
</header>
<div className="prose">
{post.content}
</div>
<ul className="flex gap-2 mt-6">
{post.tags.map(tag => (
<li key={tag.id} className="badge">
{tag.name}
</li>
))}
</ul>
</article>
);
}
Notice how the database query lives directly in the component. No separate controller file, no route binding, no explicit JSON serialization. The server executes this component, produces HTML, and streams it to the browser. For Laravel developers, this resembles treating every route as a controller that returns a view, except the "view" is a React component tree.
Middleware: Laravel to Next.js Parallels
Laravel middleware intercepts HTTP requests for authentication, authorization, and request transformation:
namespace App\Http\Middleware;
class EnsureUserIsSubscribed
{
public function handle(Request $request, Closure $next)
{
if (! $request->user()?->subscribed()) {
return redirect('billing');
}
return $next($request);
}
}
Next.js 16 implements middleware at the edge through middleware.ts in your project root:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const session = request.cookies.get('session');
// Verify JWT or session token
const isAuthenticated = verifySession(session?.value);
if (!isAuthenticated && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Attach user context to headers for downstream access
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', getUserIdFromSession(session?.value));
return NextResponse.next({
request: { headers: requestHeaders },
});
}
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};
The matcher configuration replaces Laravel's middleware assignment in routes/web.php or Kernel.php. Edge middleware runs before your application code, enabling redirects, rewrites, and header manipulation. For Laravel developers, this maps cleanly to HTTP middleware with the added benefit of executing at the network edge before requests reach your application servers.
Data Fetching Patterns: Eloquent to Server Actions
Laravel developers love Eloquent's expressive syntax. When building APIs for SPAs, you typically create Resources or serialize models directly. Next.js 16 Server Actions (now stable) eliminate this API layer for form submissions and mutations:
'use server';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
import { posts } from '@/db/schema';
interface CreatePostInput {
title: string;
slug: string;
content: string;
published: boolean;
}
export async function createPost(data: CreatePostInput) {
// Server-side validation (Laravel Validation equivalent)
const validated = createPostSchema.parse(data);
// Direct database insertion
const [post] = await db.insert(posts)
.values(validated)
.returning();
// Revalidate cache (similar to cache:flush)
revalidatePath('/posts');
revalidatePath(`/posts/${post.slug}`);
return { success: true, post };
}
Call this from a client component form:
'use client';
import { createPost } from './actions';
import { useFormState } from 'react-dom';
export function CreatePostForm() {
const [state, formAction] = useFormState(createPost, null);
return (
<form action={formAction} className="space-y-4">
<input
name="title"
placeholder="Post title"
className="input"
required
/>
<input
name="slug"
placeholder="URL slug"
className="input"
required
/>
<textarea
name="content"
rows={10}
className="textarea"
required
/>
<label className="flex items-center gap-2">
<input type="checkbox" name="published" />
Publish immediately
</label>
<button type="submit" className="btn-primary">
Create Post
</button>
{state?.success && <p className="text-green-600">Post created!</p>}
</form>
);
}
This pattern replaces Laravel's form requests, controller store methods, and API resource responses with a single server function colocated with your component. The useFormState hook provides progressive enhancement: forms work without JavaScript, yet gain interactive feedback when JavaScript loads.
Layouts: Blade's @extends and @section Equivalent
Laravel's Blade layout system uses template inheritance:
<!-- layouts/app.blade.php -->
<!DOCTYPE html>
<html>
<head>
<title>@yield('title', 'My App')</title>
@stack('styles')
</head>
<body>
<nav>@include('partials.navigation')</nav>
<main>
@yield('content')
</main>
@stack('scripts')
</body>
</html>
<!-- posts/show.blade.php -->
@extends('layouts.app')
@section('title', $post->title)
@section('content')
<article>{{ $post->content }}</article>
@endsection
Next.js 16 layouts use composition instead of inheritance, but achieve identical results:
// app/layout.tsx (Root layout)
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="antialiased">
<Navigation />
<main className="container mx-auto py-8">
{children}
</main>
<Footer />
</body>
</html>
);
}
// app/posts/layout.tsx (Segment-specific layout)
export default function PostsLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex gap-8">
<aside className="w-64 shrink-0">
<PostSidebar />
</aside>
<div className="flex-1">{children}</div>
</div>
);
}
Layouts automatically wrap nested routes. A layout in app/posts/layout.tsx applies to all routes under /posts, equivalent to Laravel's route group middleware or view composers. The children prop represents the variable content, directly replacing @yield('content').
Styling: From Laravel Mix to Built-in Solutions
Laravel developers historically configure Webpack via Mix or Vite for asset compilation. Next.js 16 eliminates this configuration entirely. Tailwind CSS, popular in both ecosystems, configures with a single file:
// tailwind.config.ts
import type { Config } from 'tailwindcss';
const config: Config = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
},
},
},
},
plugins: [],
};
export default config;
Import your global styles in the root layout, and use utility classes directly in components. No build configuration, no asset versioning headaches, no mix() helper functions.
Deployment and Server Architecture
Laravel traditionally deploys to servers running PHP-FPM and Nginx, or platforms like Forge and Vapor. Next.js 16 deploys optimally to serverless platforms: Vercel, AWS Lambda, or self-hosted Node.js environments.
The critical architectural difference: Laravel maintains persistent server processes with session state, while Next.js 16 applications are fundamentally stateless. Move session storage to Redis, database, or encrypted cookies. Database connections should use connection pooling (PgBouncer for PostgreSQL, ProxySQL for MySQL) to handle serverless connection limits.
For Laravel developers transitioning existing APIs, Next.js 16 works excellently as a frontend layer consuming your Laravel API. Alternatively, migrate entirely by replacing Eloquent with Drizzle ORM or Prisma, and run database migrations through standalone scripts.
Practical Migration Strategy
Rather than rewriting everything simultaneously, adopt this incremental approach:
- Phase 1: Build new features in Next.js 16 while keeping Laravel API intact
- Phase 2: Replace Blade pages progressively, starting with marketing sites and public content
- Phase 3: Migrate authenticated dashboard functionality using Server Actions
- Phase 4: Consolidate database access, potentially unifying on a single technology stack
Conclusion
Next.js 16 with the App Router rewards Laravel developers who appreciate structural conventions and server-side rendering. The learning curve focuses on React component patterns and TypeScript rather than arbitrary framework configuration. Server Components eliminate API boilerplate, Server Actions replace form controllers, and file-based routing mirrors Laravel's intuitive URL structure.
Start your next greenfield project with Next.js 16 to internalize these patterns. The conceptual overlap with Laravel's philosophy of developer happiness and convention over configuration makes this transition smoother than migrating to many alternative frontend frameworks.