Les Server Actions Next.js ont transformé la façon d'écrire les interactions serveur dans l'App Router. Depuis leur stabilisation en Next.js 14 et leur adoption massive en 2025-2026, elles remplacent la majorité des Route Handlers pour les mutations : création d'enregistrements, soumission de formulaires, déclenchement d'effets de bord. Mais leur puissance s'accompagne de risques sous-estimés, notamment en sécurité et gestion d'erreurs.
Cet article pose les fondations, puis passe aux patterns qui font la différence sur un projet en production : validation stricte avec Zod, gestion des erreurs typées, protection CSRF implicite, et intégration avec useActionState de React 19.
Il ne s'agit pas d'un tour d'horizon débutant. Si vous n'êtes pas encore à l'aise avec l'App Router, lisez d'abord l'article sur le caching Next.js.
Ce que font vraiment les Server Actions
Les Server Actions sont des fonctions asynchrones exécutées côté serveur, déclarées avec la directive "use server". Elles peuvent être appelées directement depuis des Client Components via un prop, ou utilisées comme action d'un <form>. Next.js génère automatiquement un endpoint HTTP POST opaque, signé avec une clé secrète dérivée de votre build. Ce mécanisme fournit une protection CSRF implicite — contrairement à un Route Handler classique exposé sans protection.
Ce qu'elles ne font pas : elles ne remplacent pas les Route Handlers pour les endpoints GET publics, les webhooks tiers, ou les APIs consommées par des clients externes. Pour tout ce qui nécessite un endpoint REST stable ou une API mobile, gardez vos Route Handlers.
Une Server Action sérialise ses arguments via le protocole React Server Components, ce qui impose des contraintes sur les types passables : pas d'objets avec méthodes, pas de Date sérialisées manuellement, pas de Promise non résolues. En pratique, FormData reste le format le plus robuste pour les formulaires.
Validation des entrées avec Zod — le seul pattern acceptable
Ne jamais faire confiance aux données passées à une Server Action, même depuis un formulaire de votre propre application. Un utilisateur malveillant peut appeler l'endpoint généré directement avec des données arbitraires. La validation côté serveur avec Zod est non négociable.
"use server"
import { z } from "zod"
import { auth } from "@/lib/auth"
import { db } from "@/lib/db"
const createProjectSchema = z.object({
name: z.string().min(2).max(100),
description: z.string().max(500).optional(),
})
type ActionResult =
| { success: true; projectId: string }
| { success: false; error: string; fieldErrors?: Record<string, string[]> }
export async function createProject(
prevState: ActionResult | null,
formData: FormData
): Promise<ActionResult> {
// 1. Authentification d'abord
const session = await auth()
if (!session?.user?.id) {
return { success: false, error: "Non authentifié" }
}
// 2. Parsing et validation
const raw = {
name: formData.get("name"),
description: formData.get("description"),
}
const parsed = createProjectSchema.safeParse(raw)
if (!parsed.success) {
return {
success: false,
error: "Données invalides",
fieldErrors: parsed.error.flatten().fieldErrors,
}
}
// 3. Mutation en base
const project = await db.project.create({
data: { ...parsed.data, userId: session.user.id },
select: { id: true }, // éviter le SELECT *
})
return { success: true, projectId: project.id }
}
Le type ActionResult discriminé est clé : il permet au Client Component de différencier succès et erreur sans passer par des exceptions non catchées. Pour les schémas plus complexes (unions, transforms, refinements), voir l'article sur la validation Zod en Next.js.
useActionState — brancher l'état du formulaire sur React
useActionState (introduit en React 19, anciennement useFormState) branche une Server Action sur un état local de formulaire, sans JavaScript supplémentaire côté client :
"use client"
import { useActionState } from "react"
import { createProject } from "./actions"
const initialState = { success: false as const, error: "" }
export function CreateProjectForm() {
const [state, action, isPending] = useActionState(createProject, initialState)
return (
<form action={action}>
<input name="name" required disabled={isPending} />
{state.success === false && state.error && (
<p className="text-red-500">{state.error}</p>
)}
{"fieldErrors" in state && state.fieldErrors?.name && (
<p className="text-red-400 text-sm">{state.fieldErrors.name[0]}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? "Création..." : "Créer"}
</button>
</form>
)
}
L'avantage sur un fetch manuel : le formulaire fonctionne sans JavaScript activé (progressive enhancement), et isPending reflète automatiquement l'état de soumission. Pour les mises à jour optimistes, combinez avec useOptimistic — pattern détaillé dans l'article sur les hooks React 19.
Sécurité — les trois erreurs les plus communes
1. Authentification sans autorisation sur la ressource
Vérifier qu'un utilisateur est connecté ne suffit pas. Vérifiez qu'il peut agir sur la ressource ciblée :
// ❌ Insuffisant : n'importe quel utilisateur connecté peut modifier n'importe quel projet
const project = await db.project.findUnique({ where: { id: projectId } })
// ✅ Filtrer par userId dans le WHERE
const project = await db.project.findUnique({
where: { id: projectId, userId: session.user.id },
select: { id: true },
})
if (!project) return { success: false, error: "Non autorisé" }
2. Exposition des erreurs internes
Ne jamais relayer des messages d'erreur Prisma ou système vers le client. Loguez côté serveur (avec votre logger ou console.error), retournez un message générique au client.
3. Revalidation trop large
revalidatePath("/") invalide tout le cache de toutes les routes — coût significatif sur une app avec du contenu. Préférez revalidatePath("/projects") ou revalidateTag("projects") avec des tags précis assignés à chaque fetch.
Composition : le pattern middleware d'action
Sur un projet de taille moyenne, les Server Actions dupliquent la même logique d'auth et de gestion d'erreurs. Un wrapper utilitaire réduit le boilerplate :
import { auth } from "@/lib/auth"
type AuthedAction<T> = (userId: string, formData: FormData) => Promise<T>
type ActionError = { success: false; error: string }
export function withAuth<T>(action: AuthedAction<T>) {
return async (
_prevState: unknown,
formData: FormData
): Promise<T | ActionError> => {
const session = await auth()
if (!session?.user?.id) {
return { success: false, error: "Non authentifié" }
}
try {
return await action(session.user.id, formData)
} catch (e) {
console.error("[Action error]", e)
return { success: false, error: "Erreur serveur" }
}
}
}
// Usage — plus de boilerplate d'auth dans chaque action
export const createProject = withAuth(async (userId, formData) => {
const parsed = createProjectSchema.safeParse(Object.fromEntries(formData))
// ...
})
Organisez vos actions dans des fichiers par domaine (app/projects/actions.ts, app/invoices/actions.ts) plutôt qu'un seul fichier global. La directive "use server" s'applique au module entier : chaque export devient potentiellement appelable par le client, donc limitez les exports aux seules fonctions effectivement utilisées côté client.
Performance des Server Actions en production
Une Server Action génère une requête HTTP POST vers un endpoint créé par Next.js, acheminé via le React Server Components Protocol. Points critiques :
- Évitez les requêtes DB en série : comme pour les Server Components, le problème N+1 s'applique pleinement. Utilisez
Promise.allpour les lectures parallèles ou desselectprécis — voir l'article sur les optimisations Prisma. - Les tâches longues bloquent l'UX : une Server Action qui prend 5 secondes laisse l'utilisateur avec un formulaire gelé. Pour les envois d'emails, la génération PDF ou le traitement d'images, déléguez à une queue (BullMQ, Upstash QStash).
revalidatePathest synchrone : la réponse n'est envoyée qu'après la revalidation complète du cache. Sur les pages volumineuses avec beaucoup de segments en cache, ça se ressent à l'usage.
En pratique
Sur les projets Kreio, les Server Actions ont remplacé environ 80 % des Route Handlers pour les mutations. Le pattern ActionResult discriminé, combiné à useActionState, couvre la quasi-totalité des cas : formulaires de création, mise à jour inline, suppression avec confirmation.
Les Route Handlers restent indispensables pour les webhooks (Stripe, Clerk, GitHub), les exports de fichiers, et les endpoints consommés par des apps mobiles tierces. Pour l'authentification complète, ce pattern s'intègre directement avec Better Auth ou Clerk — voir l'article sur l'authentification Next.js App Router.
Testing : les Server Actions étant de simples fonctions Node.js, elles se testent avec Vitest sans simuler Request/Response — un avantage concret sur les Route Handlers.
Chez Kreio, agence Next.js basée à Évreux (Normandie), on applique ces patterns sur des projets clients en production. Besoin d'un audit ou d'un renfort tech ? Parlons-en.
Sources
- Next.js — Server Actions and Mutations — documentation officielle
- React 19 — useActionState — référence API React
- OWASP — Testing for Function Level Access Control — sécurité applicative