Next.js 14 introduces stable Server Actions and Partial Prerendering for optimal performance. Combined with Laravel APIs, they create powerful, streamlined applications. At ZIRA Software, this stack delivers exceptional user experiences.
Server Actions Overview
Traditional Flow:
Client → API Route → Laravel API → Database
Server Actions Flow:
Client → Server Action → Laravel API → Database
(No separate API route needed)
Form Handling with Server Actions
// app/contact/page.tsx
import { submitContact } from './actions';
export default function ContactPage() {
return (
<form action={submitContact}>
<input type="text" name="name" placeholder="Your name" required />
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<button type="submit">Send Message</button>
</form>
);
}
// app/contact/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function submitContact(formData: FormData) {
const data = {
name: formData.get('name') as string,
email: formData.get('email') as string,
message: formData.get('message') as string,
};
// Call Laravel API
const response = await fetch(`${process.env.LARAVEL_API_URL}/contact`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Failed to submit contact form');
}
revalidatePath('/contact');
redirect('/contact/success');
}
Server Actions with Validation
// app/posts/actions.ts
'use server';
import { z } from 'zod';
import { revalidateTag } from 'next/cache';
const PostSchema = z.object({
title: z.string().min(3).max(200),
content: z.string().min(10),
category_id: z.coerce.number().positive(),
});
export async function createPost(prevState: any, formData: FormData) {
const validatedFields = PostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
category_id: formData.get('category_id'),
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Validation failed',
};
}
const response = await fetch(`${process.env.LARAVEL_API_URL}/posts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await getToken()}`,
},
body: JSON.stringify(validatedFields.data),
});
if (!response.ok) {
return { message: 'Failed to create post' };
}
revalidateTag('posts');
return { message: 'Post created successfully' };
}
// app/posts/new/page.tsx
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { createPost } from '../actions';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
);
}
export default function NewPostPage() {
const [state, formAction] = useFormState(createPost, null);
return (
<form action={formAction}>
<input type="text" name="title" placeholder="Title" />
{state?.errors?.title && (
<span className="error">{state.errors.title}</span>
)}
<textarea name="content" placeholder="Content" />
{state?.errors?.content && (
<span className="error">{state.errors.content}</span>
)}
<SubmitButton />
{state?.message && <p>{state.message}</p>}
</form>
);
}
Partial Prerendering
// app/dashboard/page.tsx
import { Suspense } from 'react';
// Static shell - prerendered at build
export default function DashboardPage() {
return (
<div className="dashboard">
{/* Static header - instant load */}
<header className="dashboard-header">
<h1>Dashboard</h1>
<nav>{/* Static navigation */}</nav>
</header>
{/* Dynamic content - streams in */}
<Suspense fallback={<StatsSkeleton />}>
<DashboardStats />
</Suspense>
<div className="dashboard-grid">
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</div>
</div>
);
}
// Dynamic component - fetches from Laravel
async function DashboardStats() {
const stats = await fetch(`${process.env.LARAVEL_API_URL}/dashboard/stats`, {
next: { tags: ['dashboard-stats'] },
}).then(r => r.json());
return (
<div className="stats-grid">
<StatCard label="Revenue" value={stats.revenue} />
<StatCard label="Orders" value={stats.orders} />
<StatCard label="Users" value={stats.users} />
</div>
);
}
Optimistic Updates
// app/posts/[id]/actions.ts
'use server';
export async function likePost(postId: number) {
await fetch(`${process.env.LARAVEL_API_URL}/posts/${postId}/like`, {
method: 'POST',
headers: { Authorization: `Bearer ${await getToken()}` },
});
revalidateTag(`post-${postId}`);
}
// app/posts/[id]/LikeButton.tsx
'use client';
import { useOptimistic, useTransition } from 'react';
import { likePost } from './actions';
export function LikeButton({ postId, initialLikes }: Props) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(state) => state + 1
);
const [isPending, startTransition] = useTransition();
return (
<button
onClick={()=> {
startTransition(async ()=> {
addOptimisticLike(null);
await likePost(postId);
});
}}
disabled={isPending}
>
❤️ {optimisticLikes}
</button>
);
}
Caching with Tags
// lib/api.ts
export async function getPosts() {
const res = await fetch(`${process.env.LARAVEL_API_URL}/posts`, {
next: { tags: ['posts'], revalidate: 60 },
});
return res.json();
}
export async function getPost(slug: string) {
const res = await fetch(`${process.env.LARAVEL_API_URL}/posts/${slug}`, {
next: { tags: ['posts', `post-${slug}`] },
});
return res.json();
}
// app/posts/actions.ts
'use server';
import { revalidateTag } from 'next/cache';
export async function updatePost(slug: string, data: PostData) {
await fetch(`${process.env.LARAVEL_API_URL}/posts/${slug}`, {
method: 'PUT',
body: JSON.stringify(data),
});
// Revalidate specific post and list
revalidateTag(`post-${slug}`);
revalidateTag('posts');
}
Conclusion
Next.js 14 Server Actions with Laravel APIs streamline full-stack development. Partial prerendering delivers instant page loads while Server Actions simplify mutations without separate API routes.
Building Next.js applications? Contact ZIRA Software for full-stack development.