Server Actions Next.js 15 : remplacez vos API routes sans effort
Pendant longtemps, chaque mutation de données dans une application Next.js impliquait la création d'une API route dédiée. Un formulaire d'inscription ? Un endpoint /api/auth/register. Une mise à jour de profil ? Un endpoint /api/user/update. Le résultat : des dizaines de fichiers route.ts dispersés dans votre projet, souvent redondants et difficiles à maintenir.
Avec Next.js 15 et les Server Actions, cette époque est révolue. Les Server Actions permettent d'exécuter du code côté serveur directement depuis vos composants React, sans passer par une API route intermédiaire. Le tout avec un typage TypeScript natif, une intégration parfaite avec React useActionState, et des performances optimisées.
Dans cet article, on va voir concrètement comment fonctionnent les Server Actions, pourquoi elles remplacent avantageusement les API routes dans la majorité des cas, et comment les intégrer dans une stack Next.js 15 + TypeScript.
Qu'est-ce qu'un Server Action dans Next.js 15 ?
Un Server Action est une fonction asynchrone qui s'exécute exclusivement sur le serveur, mais qui peut être appelée depuis un composant React (client ou serveur). Elle est identifiée par la directive "use server" placée en tête de fichier ou de fonction.
Concrètement, quand un utilisateur soumet un formulaire ou déclenche une action, Next.js génère un appel HTTP sécurisé vers le serveur en coulisses — sans que vous ayez à configurer quoi que ce soit.
// app/actions/user.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function updateUserProfile(
prevState: { message: string } | null,
formData: FormData
): Promise<{ message: string }> {
const name = formData.get('name') as string
const email = formData.get('email') as string
// Validation côté serveur
if (!name || name.length < 2) {
return { message: 'Le nom doit contenir au moins 2 caractères.' }
}
// Mise à jour en base de données (MongoDB via NestJS ou direct)
await db.user.update({ where: { email }, data: { name } })
// Revalidation du cache Next.js
revalidatePath('/profile')
return { message: 'Profil mis à jour avec succès !' }
}
La directive "use server" au début du fichier marque toutes les fonctions exportées comme des Server Actions. Vous pouvez aussi l'appliquer à une seule fonction si vous préférez cohabiter code serveur et code client dans le même fichier.
Server Actions vs API Routes : quand choisir quoi ?
Les API routes (fichiers route.ts dans le dossier app/api/) restent pertinentes dans certains cas précis :
- API publique consommée par des clients tiers (applications mobiles, services externes)
- Webhooks entrants (Stripe, GitHub, etc.)
- Endpoints consommés par plusieurs frontends (microfrontends, SaaS multi-tenant)
- Streaming de données ou réponses SSE complexes
En dehors de ces cas, les Server Actions sont généralement supérieures pour les mutations internes à votre application Next.js :
| Critère | API Route | Server Action |
|---|---|---|
| Boilerplate | Élevé (fetch + handler) | Minimal |
| Typage TypeScript | Manuel (types partagés) | Natif (inférence directe) |
| Gestion d'erreur | Codes HTTP + parsing | Try/catch + état retourné |
| Revalidation cache | Manuelle | revalidatePath / revalidateTag |
| Protection CSRF | À implémenter | Intégrée par Next.js |
| Progressif sans JS | ❌ | ✅ (formulaires HTML natifs) |
Ce dernier point est particulièrement important pour l'accessibilité et la résilience : un formulaire utilisant une Server Action fonctionne même si JavaScript est désactivé dans le navigateur, car il s'appuie sur le comportement natif des formulaires HTML.
Intégration avec useActionState (React 19)
Next.js 15 s'appuie sur React 19, qui introduit le hook useActionState pour gérer l'état d'une Server Action de manière réactive. C'est le remplacement du deprecated useFormState de React 18.
// app/profile/ProfileForm.tsx
"use client";
import { useActionState } from "react";
import { updateUserProfile } from "@/app/actions/user";
interface ProfileFormState {
message: string;
}
export function ProfileForm({ currentName }: { currentName: string }) {
const [state, formAction, isPending] = useActionState<
ProfileFormState | null,
FormData
>(updateUserProfile, null);
return (
<form action={formAction} className="flex flex-col gap-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Nom complet
</label>
<input
id="name"
name="name"
type="text"
defaultValue={currentName}
className="mt-1 w-full rounded-md border border-gray-300 px-3 py-2"
disabled={isPending}
/>
</div>
{state?.message && (
<p
role="alert"
className={
state.message.includes("succès")
? "text-green-600"
: "text-red-600"
}
>
{state.message}
</p>
)}
<button
type="submit"
disabled={isPending}
className="rounded-md bg-indigo-600 px-4 py-2 text-white disabled:opacity-50"
>
{isPending ? "Mise à jour..." : "Enregistrer"}
</button>
</form>
);
}
Quelques points clés dans cet exemple :
isPending: état booléen automatiquement géré par React pendant l'exécution de l'action. Plus besoin deuseStatepour gérer un loading.state: la valeur retournée par la Server Action. Ici un objet{ message: string }qui contient soit un message d'erreur, soit un message de succès.formAction: la fonction passée directement à l'attributactiondu formulaire. C'est la magie de l'intégration React 19.
Validation et sécurité avec Zod
Les Server Actions s'exécutent côté serveur, mais elles restent accessibles via le réseau. Il est donc crucial de valider toutes les entrées avec une librairie comme Zod avant tout traitement.
// app/actions/contact.ts
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
const ContactSchema = z.object({
name: z.string().min(2, 'Nom trop court').max(100),
email: z.string().email('Email invalide'),
message: z.string().min(10, 'Message trop court').max(2000),
})
type ContactState = {
errors?: Record<string, string[]>
success?: boolean
}
export async function submitContact(
prevState: ContactState,
formData: FormData
): Promise<ContactState> {
// Parsing et validation avec Zod
const parsed = ContactSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
})
if (!parsed.success) {
// Retourner les erreurs structurées par champ
return {
errors: parsed.error.flatten().fieldErrors,
}
}
// Ici : appel vers NestJS ou MongoDB directement
// await contactService.create(parsed.data);
return { success: true }
}
Cette approche avec safeParse de Zod est idéale : elle retourne les erreurs par champ sous forme d'un objet typé, que vous pouvez ensuite afficher à côté de chaque input dans votre formulaire.
Server Actions dans les composants serveur
Les Server Actions ne sont pas limitées aux formulaires côté client. Vous pouvez aussi les appeler dans des composants serveur, notamment pour des boutons d'action simples :
// app/dashboard/PostCard.tsx (Server Component)
import { deletePost } from "@/app/actions/posts";
export function PostCard({ post }: { post: Post }) {
return (
<article className="rounded-lg border p-4">
<h2 className="text-lg font-semibold">{post.title}</h2>
<p className="text-gray-600">{post.excerpt}</p>
{/* Formulaire minimal pour une action de suppression */}
<form action={deletePost.bind(null, post.id)}>
<button
type="submit"
className="mt-2 text-sm text-red-600 hover:underline"
>
Supprimer
</button>
</form>
</article>
);
}
// app/actions/posts.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function deletePost(postId: string): Promise<void> {
await db.post.delete({ where: { id: postId } })
revalidatePath('/dashboard')
}
L'utilisation de .bind(null, post.id) permet de pré-remplir le premier argument de la Server Action avec l'identifiant du post, sans avoir besoin d'un champ caché dans le formulaire.
Bonnes pratiques et pièges à éviter
✅ À faire
- Toujours valider les entrées avec Zod ou une librairie équivalente, même si vous avez déjà une validation côté client.
- Retourner des états descriptifs plutôt que de lever des exceptions — cela facilite l'affichage des erreurs dans l'UI.
- Utiliser
revalidatePathourevalidateTagaprès chaque mutation pour garder le cache Next.js cohérent. - Séparer les Server Actions dans des fichiers dédiés (ex:
app/actions/) pour une meilleure lisibilité et réutilisabilité.
❌ À éviter
- Ne jamais faire confiance aux données du formulaire sans validation serveur — les Server Actions sont accessibles via HTTP.
- Éviter les secrets dans les Server Actions exposées au client (connexion DB directe, clés API) — préférez un appel vers votre backend NestJS ou une couche service isolée.
- Ne pas oublier la gestion d'erreur : un
try/catchest indispensable autour de vos appels base de données.
En résumé
Les Server Actions de Next.js 15 représentent une évolution majeure dans la façon d'écrire des mutations dans une application React. Voici ce qu'il faut retenir :
- Elles éliminent le besoin d'API routes pour la majorité des cas d'usage internes
- Elles s'intègrent nativement avec React 19 et
useActionStatepour une gestion d'état simple et typée - Elles offrent une protection CSRF intégrée et supportent les formulaires sans JavaScript
- La validation avec Zod est indispensable côté serveur
revalidatePathetrevalidateTagpermettent de garder le cache cohérent après chaque mutation
Dans une stack Next.js + NestJS, les Server Actions sont idéales pour les mutations "front-only" (préférences UI, gestion de sessions) tandis que NestJS reste le bon endroit pour la logique métier complexe, les microservices et les API exposées à des clients tiers.