Les Server Actions Next.js ont changé la façon dont on gère les mutations dans les applications React. Fini les Route Handlers /api/* pour chaque formulaire ou bouton qui écrit en base : avec les Server Actions, vous exécutez du code serveur directement depuis vos composants, avec une sécurité de typage bout en bout et un accès natif aux sessions, aux cookies et à la base de données.
Ce guide est le résultat de mois d'utilisation en production chez Kreio. On va couvrir les patterns qui marchent, les pièges qui coûtent cher à déboguer, et comment intégrer les Server Actions proprement avec Zod, useActionState et useOptimistic.
Comment fonctionnent les Server Actions Next.js
Une Server Action est une fonction asynchrone marquée avec la directive 'use server'. Next.js la compile séparément : côté client, un appel réseau chiffré est généré automatiquement ; côté serveur, la fonction s'exécute dans le runtime Node.js avec accès complet à l'environnement — variables d'environnement, base de données, système de fichiers.
Ce qui distingue les Server Actions des Route Handlers, c'est leur intégration avec le modèle React : elles se composent avec useActionState pour gérer l'état de chargement et les erreurs, avec useOptimistic pour les mises à jour optimistes, et avec revalidatePath / revalidateTag pour invalider le cache Next.js directement depuis la mutation.
En pratique, cela signifie qu'un formulaire de création de tâche peut être entièrement typé — de la validation Zod côté serveur jusqu'à l'affichage de l'erreur côté client — sans une seule ligne de code API manuelle. Le compilateur Next.js s'occupe de la sérialisation et de la désérialisation des données entre les deux environnements.
Définir une Server Action : inline vs fichier dédié
Il existe deux façons de définir une Server Action. La première, inline dans un Server Component :
// app/tasks/page.tsx — 'use server' dans la fonction, pas en tête de fichier
export default function TasksPage() {
async function createTask(formData: FormData) {
'use server'
// S'exécute sur le serveur, accès direct à la DB sans passer par une API
const title = formData.get('title') as string
await db.task.create({ data: { title } })
revalidatePath('/tasks')
}
return <form action={createTask}>{/* ... */}</form>
}
La seconde approche consiste à créer un fichier actions.ts avec 'use server' en tête de fichier. C'est ce qu'on privilégie chez Kreio dès que la logique dépasse un effet de bord simple, parce qu'elle est testable isolément et réutilisable par plusieurs composants. Gardez les actions inline pour les cas triviaux comme un bouton "marquer comme lu" ; pour tout ce qui touche à la base de données ou à la session, un fichier dédié évite les fuites logiques et rend les revues de code plus lisibles.
Une règle empirique : si votre Server Action est invoquée par plus d'un composant, elle a sa place dans un fichier actions.ts. Si elle contient plus de dix lignes, idem.
Valider les données avec Zod avant d'écrire en base
La première erreur qu'on voit dans les codebases Next.js est l'absence de validation côté serveur. TypeScript vous donne une illusion de sécurité : un utilisateur malveillant peut appeler votre Server Action avec n'importe quelle payload via une requête HTTP directe. Le typage disparaît au runtime.
// app/tasks/actions.ts
'use server'
import { z } from 'zod/v4'
import { revalidatePath } from 'next/cache'
const CreateTaskSchema = z.object({
title: z.string().min(1).max(255),
priority: z.enum(['low', 'medium', 'high']),
})
// Le type de retour explicite force la gestion d'erreur côté client
export async function createTask(
_prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const parsed = CreateTaskSchema.safeParse({
title: formData.get('title'),
priority: formData.get('priority'),
})
if (!parsed.success) {
// flatten() donne un objet champ → messages[] directement utilisable dans le JSX
return { success: false, errors: parsed.error.flatten().fieldErrors }
}
await db.task.create({ data: parsed.data })
revalidatePath('/tasks')
return { success: true }
}
L'article sur la validation avec Zod v4 dans Next.js App Router couvre en détail les patterns de schéma réutilisables — notamment comment mutualiser les schémas entre Server Actions et les formulaires côté client pour éviter la duplication.
Gérer l'état de chargement avec useActionState
useActionState est le hook React 19 conçu pour consommer les Server Actions dans un formulaire. Il remplace l'ancien useFormState et retourne un tuple [state, action, isPending]. L'état initial est passé en second argument.
// app/tasks/TaskForm.tsx
'use client'
import { useActionState } from 'react'
import { createTask } from './actions'
const initialState = { success: false, errors: {} }
export function TaskForm() {
const [state, action, isPending] = useActionState(createTask, initialState)
return (
<form action={action}>
<input name="title" required aria-describedby="title-error" />
{state.errors?.title && (
<p id="title-error" className="text-red-500">{state.errors.title[0]}</p>
)}
<select name="priority">
<option value="low">Basse</option>
<option value="medium">Moyenne</option>
<option value="high">Haute</option>
</select>
<button type="submit" disabled={isPending}>
{isPending ? 'Création...' : 'Créer la tâche'}
</button>
</form>
)
}
isPending est true pendant toute la durée de l'appel réseau — pas besoin de useState supplémentaire ni de gestionnaire onSubmit manuel. L'article sur les nouveaux hooks React 19 détaille les cas d'usage avancés, notamment la gestion des mutations en séquence et les patterns de reset de formulaire après succès.
Mises à jour optimistes avec useOptimistic
Pour les interactions où la latence réseau est perceptible — toggle de statut, like, archivage — useOptimistic permet d'afficher immédiatement le résultat escompté pendant que la Server Action s'exécute en arrière-plan. Si l'action échoue, React revient automatiquement à l'état précédent.
'use client'
import { useOptimistic } from 'react'
import { toggleTask } from './actions'
export function TaskItem({ task }: { task: Task }) {
// La fonction de mise à jour optimiste reçoit l'état actuel et la valeur de transition
const [optimisticTask, addOptimistic] = useOptimistic(
task,
(state, completed: boolean) => ({ ...state, completed })
)
return (
<form
action={async () => {
// Mise à jour UI immédiate — n'attend pas la réponse serveur
addOptimistic(!task.completed)
await toggleTask(task.id)
}}
>
<button type="submit">
{optimisticTask.completed ? '✓ Terminée' : 'Marquer terminée'}
</button>
</form>
)
}
Le pattern "optimiste par défaut, cohérent sur erreur" améliore significativement la perception de performance. Sur un réseau mobile à 3G, la différence entre un toggle instantané et un toggle avec 800ms de latence est la frontière entre une application fluide et une application qui "lag".
Sécurité : une Server Action est un endpoint HTTP public
C'est l'angle mort le plus fréquent dans les codebases Next.js qu'on audite. Une Server Action est compilée en un endpoint HTTP POST accessible par son ID hashé. Next.js génère cet ID de façon non-déterministe entre les builds, ce qui empêche l'énumération — mais un attaquant qui capture une requête dans les DevTools peut rejouer l'appel avec n'importe quelle payload.
// Pattern à adopter systématiquement pour les actions protégées
export async function deleteTask(taskId: string) {
'use server'
const session = await getSession()
if (!session?.user) throw new Error('Unauthorized')
// Vérifier la propriété de la ressource — l'ID en paramètre ne suffit pas
const task = await db.task.findUnique({ where: { id: taskId } })
if (task?.userId !== session.user.id) throw new Error('Forbidden')
await db.task.delete({ where: { id: taskId } })
revalidatePath('/tasks')
}
L'article sur l'authentification dans Next.js App Router couvre les patterns de session avec Clerk, Auth.js v5 et Better Auth — notamment comment centraliser la vérification dans un helper requireAuth() réutilisable par toutes vos actions.
En pratique
Les Server Actions Next.js suppriment une couche entière de plomberie API dans vos applications. Sur les projets chez Kreio qui adoptent ce pattern en remplacement des Route Handlers pour les mutations, on observe une réduction nette du nombre de fichiers et une meilleure co-localisation de la logique métier avec les composants qui la consomment.
Le pattern recommandé : un fichier actions.ts par domaine fonctionnel, validation Zod systématique en entrée, vérification de session en première ligne pour les actions protégées, et revalidatePath ou revalidateTag en sortie plutôt qu'un rechargement de page complet. Pour les mutations complexes qui touchent plusieurs parties du cache, coupler les Server Actions avec les stratégies de cache Next.js App Router permet de contrôler finement ce qui se revalide sans sur-fetcher.
Chez Kreio, agence Next.js basée à Évreux (Normandie), on applique ces patterns sur des projets clients en production depuis Next.js 14. Besoin d'un audit de votre architecture ou d'un renfort technique ? Parlons-en.