SD
Next.jsServer ActionsTypeScriptApp Router

Server Actions Next.js 15 : remplacez vos API routes sans effort

Découvrez comment les Server Actions de Next.js 15 simplifient vos mutations côté serveur et remplacent avantageusement vos API routes avec TypeScript.

AMAlexis Mouchon8 min de lecture

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 :

En dehors de ces cas, les Server Actions sont généralement supérieures pour les mutations internes à votre application Next.js :

CritèreAPI RouteServer Action
BoilerplateÉlevé (fetch + handler)Minimal
Typage TypeScriptManuel (types partagés)Natif (inférence directe)
Gestion d'erreurCodes HTTP + parsingTry/catch + état retourné
Revalidation cacheManuellerevalidatePath / revalidateTag
Protection CSRFÀ implémenterInté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 :


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

❌ À éviter


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 :

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.