Data fetching et cache dans Next.js (App Router)

1. Aperçu générale

Next.js App Router cache automatiquement les résultats des appels fetch sur le serveur pour réduire la charge du backend et accélérer les pages. Mais ce mécanisme de cache est très différent du cache HTTP standard auquel vous êtes habitué, et si vous ne le comprenez pas bien, vous risquez de vous retrouver dans des situations où « les données anciennes ne veulent pas se mettre à jour » ou inversement « chaque page se réaffiche depuis le début, terriblement lent ».

Cet article couvre les différentes couches de cache de Next.js : le cache du fetch, la revalidation selon le temps, la revalidation selon les tags, et le dynamic rendering. Une fois que vous l’aurez lu, vous saurez comment choisir la bonne stratégie de fetch pour chaque type de page (statique, ISR, dynamique) et comment debugger quand le cache « fait du zèle ».

Note : cet article s’applique à l’App Router (Next.js 13+, version 14/15 recommandée). Le Pages Router utilise un mécanisme différent (getStaticProps, getServerSideProps) qui ne fait pas partie de ce sujet.

2. Quatre couches de cache dans Next.js

Next.js dispose de 4 mécanismes de cache distincts, faciles à confondre si on les mélange :

Mécanisme Localisation Objectif
Request Memoization Serveur, pendant un rendu Fusionner les appels fetch identiques dans la même requête
Data Cache Serveur, entre les requêtes Cache les résultats des fetch longtemps, peut être revalidé
Full Route Cache Serveur, au build Cache le HTML + payload RSC de la route statique
Router Cache Client, pendant la session Cache les résultats de la navigation pour accélérer retour/avant

Cet article se concentre sur le Data Cache car c’est celui que vous contrôlez directement via les options du fetch.

3. Fetch par défaut : cache « illimité »

Dans un Server Component, le fetch est patché par Next.js pour être automatiquement mis en cache :

// app/products/page.tsx
async function getProducts() {
  const res = await fetch("https://api.shop.example.com/products");
  if (!res.ok) throw new Error("Impossible d'appeler l'API des produits");
  return res.json();
}

export default async function ProductsPage() {
  const products = await getProducts();
  return (
    <ul>
      {products.map((p: { id: string; name: string }) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}
  • fetch sans option cache → Next.js utilise par défaut force-cache.
  • Le résultat est sauvegardé dans le Data Cache, utilisé pour toutes les requêtes suivantes, sans expiration sauf si vous effectuez une revalidation.
  • Cette page est mise en Full Route Cache : construite en HTML statique, servie comme une page statique.

C’est pourquoi beaucoup de gens sont surpris : modifier les données au backend mais la page ne change pas — parce que Next.js la considère comme une donnée statique.

4. Contrôler le cache avec l’option cache

Pour toujours récupérer les données fraîches, utilisez no-store :

async function getOrderStatus(orderId: string) {
  const res = await fetch(
    `https://api.shop.example.com/orders/${orderId}`,
    { cache: "no-store" }
  );
  return res.json();
}
  • cache: "no-store" désactive complètement le Data Cache pour cette requête.
  • La route contenant ce fetch sera marquée comme dynamique, réaffichée à chaque requête.
  • Parfait pour les données qui changent constamment : commandes en cours de traitement, prix en temps réel, tableaux de bord.

Note : il suffit qu’un seul fetch utilise no-store dans une page pour que toute la page devienne dynamique — même les autres fetch de la page utilisent toujours force-cache.

5. Revalidation selon le temps (ISR)

Pour mettre en cache mais rafraîchir automatiquement après N secondes (Incremental Static Regeneration), utilisez next.revalidate :

async function getCategories() {
  const res = await fetch(
    "https://api.shop.example.com/categories",
    { next: { revalidate: 300 } } // 5 minutes
  );
  return res.json();
}
  • revalidate: 300 signifie : les données sont considérées comme « anciennes » après 300 secondes.
  • La première requête après le délai de 300 secondes retourne toujours les données anciennes (stale-while-revalidate) mais Next.js déclenche un fetch frais en arrière-plan.
  • La requête suivante reçoit les données fraîches.

Vous pouvez l’appliquer à toute la route en exportant une variable au début du fichier :

// app/blog/[slug]/page.tsx
export const revalidate = 300;

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const res = await fetch(`https://api.blog.example.com/posts/${params.slug}`);
  return <article>{/* ... */}</article>;
}
  • export const revalidate s’applique à tous les fetch de la route si le fetch ne déclare pas son propre revalidate.
  • Si un fetch particulier déclare un next.revalidate différent, cette valeur prime pour ce fetch.

6. Revalidation selon le tag : revalidateTag

La revalidation basée sur le temps fonctionne bien pour le contenu qui change régulièrement, mais c’est insuffisant quand vous voulez rafraîchir immédiatement quand les données changent (par exemple l’admin vient de mettre à jour le prix d’un produit). Utilisez alors les tags de cache.

// app/products/[id]/page.tsx
async function getProduct(id: string) {
  const res = await fetch(
    `https://api.shop.example.com/products/${id}`,
    { next: { tags: [`product-${id}`] } }
  );
  return res.json();
}

Quand l’admin met à jour un produit, appelez revalidateTag dans un Route Handler ou une Server Action :

// app/actions/update-product.ts
"use server";

import { revalidateTag } from "next/cache";

export async function updateProduct(id: string, formData: FormData) {
  await fetch(`https://api.shop.example.com/products/${id}`, {
    method: "PATCH",
    body: formData,
  });

  revalidateTag(`product-${id}`); // supprime le cache avec ce tag
}
  • tags attache un « label » à l’entrée de cache du fetch.
  • revalidateTag(tag) supprime toutes les entrées du Data Cache ayant ce tag, peu importe quelles routes les utilisent.
  • La prochaine visite récupérera les données fraîches et les mettra en cache avec le nouveau tag.

C’est le mécanisme le plus puissant car il sépare la logique « quand invalider » de « quelle page affiche les données » — comme le cache tagging dans Spring Cache ou Redis.

7. Revalidation selon le chemin : revalidatePath

Si vous ne taggez pas, vous pouvez revalidiser directement selon le chemin de la route :

"use server";

import { revalidatePath } from "next/cache";

export async function publishPost(slug: string) {
  // ... appel API pour publier ...
  revalidatePath(`/blog/${slug}`);   // juste cette page
  revalidatePath("/blog", "page");    // ou la liste entière
}
  • revalidatePath("/blog/[slug]") supprime le Full Route Cache + Data Cache de cette route exacte.
  • Le second paramètre ("page" ou "layout") détermine si les routes enfants utilisant ce layout sont aussi revalidées.
  • Différent de revalidateTag : revalidatePath est lié à une route spécifique, tandis qu’un tag peut être utilisé sur plusieurs routes différentes.

Note : revalidateTag/revalidatePath ne fonctionnent que dans les Server Actions ou Route Handlers, pas directement depuis un Client Component.

8. Dynamic rendering : quand le cache perd son intérêt

Certaines API font automatiquement passer Next.js en mode dynamic, en contournant le Full Route Cache :

import { cookies, headers } from "next/headers";

export default async function AccountPage() {
  const sessionToken = (await cookies()).get("session")?.value;
  const userAgent = (await headers()).get("user-agent");

  const res = await fetch("https://api.shop.example.com/me", {
    headers: { Authorization: `Bearer ${sessionToken}` },
    cache: "no-store",
  });

  return <div>Bienvenue, {(await res.json()).name}</div>;
}
  • Appeler cookies() ou headers() marque automatiquement la route comme dynamique — c’est logique puisque les données dépendent de l’utilisateur spécifique.
  • Combiné avec cache: "no-store" pour garantir que les données sont toujours fraîches selon la session.

Forcer explicitement le mode dynamique (quand vous savez qu’on ne peut pas mettre en cache) :

export const dynamic = "force-dynamic";

9. Route Handlers ont aussi leur propre cache

Les Route Handlers (app/api/.../route.ts) avec la méthode GET suivent aussi les mêmes règles de cache que les Server Components :

// app/api/stock/route.ts
export const revalidate = 60;

export async function GET() {
  const res = await fetch("https://api.shop.example.com/stock");
  const data = await res.json();
  return Response.json(data);
}
  • Un GET qui n’utilise pas l’objet Request (ne lit pas request.url pour les query params) peut être mis en cache statiquement.
  • Si le handler utilise request.headers, request.cookies, ou une autre méthode que GET (POST, PUT…) → toujours dynamique, pas de cache.

10. Client Component : le cache appartient à une autre bibliothèque

Le cache des fetch de Next.js ne s’applique que sur le serveur. Dans un Client Component, vous faites votre propre fetch avec useEffect ou utilisez une bibliothèque de cache comme SWR :

npm install swr
"use client";

import useSWR from "swr";

const fetcher = (url: string) => fetch(url).then((r) => r.json());

export function CartBadge({ userId }: { userId: string }) {
  const { data, isLoading } = useSWR(`/api/cart/${userId}`, fetcher, {
    refreshInterval: 10_000, // poll chaque 10s
  });

  if (isLoading) return <span>...</span>;
  return <span>{data.itemCount} articles</span>;
}
  • SWR a son propre cache, déduplique les requêtes, revalidise au focus — complètement indépendant du Data Cache du serveur.
  • Utilisez-le quand vous avez besoin de données fraîches en continu côté client sans recharger toute la page.

11. Écrire des tests pour la logique de cache key

Le mécanisme de cache de Next.js lui-même est difficile à tester en unit test directement, mais la logique de génération des tags/paths vaut souvent la peine d’être testée séparément — pour éviter la mauvaise orthographe d’un tag qui ferait que revalidateTag ne correspond jamais.

// lib/cache-keys.ts
export function productTag(id: string): string {
  return `product-${id}`;
}

export function categoryTag(id: string): string {
  return `category-${id}`;
}
// lib/cache-keys.test.ts
import { describe, it, expect } from "vitest";
import { productTag, categoryTag } from "./cache-keys";

describe("cache-keys", () => {
  it("génère le tag produit au bon format", () => {
    expect(productTag("sku-123")).toBe("product-sku-123");
  });

  it("le tag produit et catégorie ne se chevauchent pas", () => {
    const id = "42";
    expect(productTag(id)).not.toBe(categoryTag(id));
  });
});
  • Extraire la fonction de génération des tags dans un module séparé vous garantit que l’endroit qui appelle fetch (qui attache le tag) et celui qui appelle revalidateTag (qui supprime le tag) utilisent toujours le même format.
  • C’est une erreur réelle très courante : un côté écrit product-${id}, l’autre écrit products-${id} → la revalidation ne correspond jamais.
✓ cache-keys > génère le tag produit au bon format
✓ cache-keys > le tag produit et catégorie ne se chevauchent pas

12. Erreurs courantes

  • Oublier que fetch met en cache par défaut indéfiniment : après deploiement les données ne changent pas parce qu’on utilise toujours le cache depuis la compilation précédente (avec static export) ou depuis le premier fetch (avec déploiement serveur). Corriger avec revalidate ou no-store.
  • Mélanger inutilement no-store et cache dans le même composant : il suffit d’un seul fetch no-store pour que toute la page devienne dynamique, perdant tous les avantages du cache des autres fetch.
  • Appeler revalidateTag/revalidatePath en dehors d’une Server Action/Route Handler : aucun effet, Next.js signale une erreur ou l’ignore silencieusement selon la version.
  • Utiliser un fetch standard (pas patché par Next.js) à l’intérieur d’une bibliothèque tierce (par exemple un SDK utilisant axios ou undici pur) : pas géré par le Data Cache, il faut revalidiser autrement (par exemple mettre en cache ailleurs ou écrire un wrapper appelant le bon fetch).
  • Ne pas tagger par entité spécifique : attacher un tag trop générique ("products" pour tous les produits) fait qu’une petite mise à jour invalide tout le cache, perdant les avantages du cache par item.

Quand NE PAS mettre en cache : données personnalisées par session (panier, solde, notifications), données en temps réel (prix des actions, statut de paiement), ou toute réponse dépendant de cookies()/headers(). Dans ces cas, utilisez no-store ou force-dynamic dès le départ, n’essayez pas de forcer le cache puis revalidiser constamment — c’est à la fois complexe et mauvais pour la nature des données.

13. Conclusion

Le cache dans l’App Router de Next.js comprend plusieurs couches, mais la plus directement contrôlée par vous est le Data Cache via les options du fetch : cache, next.revalidate, next.tags. Règle rapide pour choisir : données peu changeantes → revalidate selon le temps ; données changeant sur des événements spécifiques (admin update) → tagguer et utiliser revalidateTag ; données selon l’utilisateur ou temps réel → no-store/force-dynamic. Maîtriser ces trois groupes, et vous devrez rarement deviner « pourquoi les données ne veulent pas se mettre à jour ».

nextjsreactcachingapp-routerserver-components