Next.js 14 a stabilisé Server Actions. Next.js 15 (release 2024) les a affinés. En 2026, ils remplacent ~70 % des API routes pour mutations form-driven. Mais patterns production = peu documentés. Voici les 8 patterns essentiels.
TL;DR
- Server Actions = fonctions serveur appelables directement depuis composants React.
- Avantages : type-safe, no API endpoint, progressive enhancement.
- Patterns : validation Zod, optimistic UI, revalidation, error handling.
Server Action basique
`tsx
// app/actions/posts.ts
'use server';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
import { prisma } from '@/lib/prisma';
const CreatePostSchema = z.object({
title: z.string().min(3).max(200),
content: z.string().min(10),
});
export async function createPost(formData: FormData) {
const parsed = CreatePostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
});
if (!parsed.success) {
return { error: parsed.error.flatten() };
}
const post = await prisma.post.create({ data: parsed.data });
revalidatePath('/posts');
return { success: true, post };
}
`
`tsx
// Usage dans component
import { createPost } from '@/app/actions/posts';
export function CreatePostForm() {
return (
);
}
`
Pattern 1 — useActionState pour error handling + loading
`tsx
'use client';
import { useActionState } from 'react';
import { createPost } from '@/app/actions/posts';
export function CreatePostForm() {
const [state, formAction, isPending] = useActionState(createPost, null);
return (
{state?.error?.fieldErrors?.title && (
{state.error.fieldErrors.title}
)}
{state?.error?.fieldErrors?.content && (
{state.error.fieldErrors.content}
)}
{isPending ? 'Creating...' : 'Create'}
{state?.success && Post created!
);
}
`
Pattern 2 — optimistic UI
`tsx
'use client';
import { useOptimistic } from 'react';
import { addComment } from '@/app/actions/comments';
export function CommentList({ initial, postId }) {
const [optimisticComments, addOptimistic] = useOptimistic(
initial,
(state, newComment) => [...state, { ...newComment, pending: true }]
);
async function handleSubmit(formData: FormData) {
const text = formData.get('text') as string;
addOptimistic({ id: 'temp', text, author: 'You' });
await addComment(postId, text);
}
return (
{optimisticComments.map(c => ( {c.text} ))}
);
}
`
Pattern 3 — auth + tenant scoping
`tsx
'use server';
import { auth } from '@/lib/auth';
import { tenantPrisma } from '@/lib/prisma-tenant';
export async function updatePost(postId: string, data: UpdateInput) {
const session = await auth();
if (!session?.user) throw new Error('Unauthorized');
const prisma = tenantPrisma(session.organizationId);
// Vérifier ownership
const post = await prisma.post.findUnique({
where: { id: postId },
});
if (!post) throw new Error('Not found');
if (post.authorId !== session.user.id) {
throw new Error('Forbidden');
}
await prisma.post.update({
where: { id: postId },
data,
});
revalidatePath(/posts/${postId});
}
Besoin d'un site web professionnel ?
Kolonell crée des sites web qui attirent des clients, optimisés pour le marché sénégalais. Devis gratuit en 2 minutes.
`
Pattern 4 — file upload
`tsx
'use server';
import { put } from '@vercel/blob';
export async function uploadAvatar(formData: FormData) {
const file = formData.get('file') as File;
if (!file || file.size > 5 * 1024 * 1024) {
return { error: 'File too large' };
}
const blob = await put(avatars/${session.user.id}.png, file, {
access: 'public',
});
await prisma.user.update({
where: { id: session.user.id },
data: { avatarUrl: blob.url },
});
revalidatePath('/profile');
return { success: true, url: blob.url };
}
`
Pattern 5 — rate limiting
`tsx
'use server';
import { Ratelimit } from '@upstash/ratelimit';
import { headers } from 'next/headers';
const limiter = new Ratelimit({
redis: redisClient,
limiter: Ratelimit.slidingWindow(10, '1 m'),
});
export async function submitContactForm(formData: FormData) {
const ip = (await headers()).get('x-forwarded-for') ?? 'unknown';
const { success } = await limiter.limit(ip);
if (!success) {
return { error: 'Rate limit exceeded' };
}
// ... process form
}
`
Pattern 6 — progressive enhancement
Form fonctionne SANS JavaScript :
`tsx
// Pas de 'use client' = Server Component
import { createPost } from './actions';
export default function NewPostPage() {
return (
);
}
`
Si JS désactivé : form submit normal (POST). JavaScript activé : Server Action streaming. UX moderne sans JS dependency.
Pattern 7 — revalidation granulaire
`tsx
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function updateProduct(productId: string, data: any) {
await prisma.product.update({ where: { id: productId }, data });
// Revalider pages touchées
revalidatePath(/products/${productId}); // page produit
revalidatePath('/products', 'page'); // liste produits
revalidatePath('/admin/inventory'); // dashboard admin
// OU revalider tags (plus précis)
revalidateTag(product:${productId});
revalidateTag('inventory');
}
`
Pattern 8 — error boundary + Sentry
`tsx
'use server';
import * as Sentry from '@sentry/nextjs';
export async function dangerousAction(input: string) {
try {
const result = await actuallyDoIt(input);
return { success: true, result };
} catch (error) {
Sentry.captureException(error, {
tags: { action: 'dangerousAction' },
extra: { input },
});
if (error instanceof PrismaError) {
return { error: 'Database error' };
}
return { error: 'Unknown error' };
}
}
`
Quand NE PAS utiliser Server Actions
`
❌ Endpoint public consommé par third-party (mobile app, intégrations) → API route
❌ Webhooks externes (Stripe, GitHub) → API route
❌ Streaming responses très longues → API route avec stream
❌ Caching agressif (CDN) → API route avec cache headers
`
`
✓ Form submissions → Server Actions
✓ User-triggered mutations dans UI → Server Actions
✓ Toggle/like/comment dans UI → Server Actions
`
Cas réel — SaaS Dakar migration
Migration 80 API routes → Server Actions :
- Code réduit -45 % (no fetch + JSON serialization manuel)
- Type safety 100 % (input + output typés via TypeScript)
- Bugs CSRF éliminés (Server Actions includes CSRF protection)
- Bundle frontend -12 % (no fetch wrapper code)
Pièges fréquents
- 'use server' file-level vs function-level — préférer file pour clarté.
- Pas de revalidation — UI stale post-mutation. Revalider toujours.
- No type safety pour FormData — utiliser Zod parse pour validation runtime.
- Server Actions dans Client Components — OK, mais imports doivent être server.
- Pas de error boundary — uncaught error = page crashed. Wrap dans try/catch.
FAQ
Q : Server Actions vs tRPC ?
R : Server Actions plus simple Next.js native. tRPC plus puissant + cross-app. Pour app Next.js seul : Server Actions.
Q : Migration API routes ?
R : Progressive. Garder routes pour external + webhooks. Migrer mutations UI à Server Actions.
Q : Performance vs API routes ?
R : Quasi-identique. Server Actions = wrapper API route avec serialization auto.
Conclusion
Server Actions Next.js 15 en 2026 = patron standard pour mutations form-driven. -45 % code vs API routes traditional. Type safe + progressive enhancement. À adopter pour tout nouveau projet Next.js.
Mohamed Bah
Fondateur, Kolonell
Passionné par le digital et l'entrepreneuriat en Afrique, Mohamed accompagne les entreprises sénégalaises dans leur transformation digitale depuis 2020. Fondateur de Kolonell, il croit que chaque PME mérite une présence en ligne professionnelle et accessible.
