Bibliothèque de playbooks
Conventions Next.js App Router
Les défaillances de l'App Router sont des défaillances de frontière : un secret inliné dans le bundle client, une server action traitée comme une fonction privée, une page marquée `"use client"` qui fait sortir tout l'arbre du rendu serveur. Les modèles, entraînés surtout sur du code Pages Router, se trompent ici d'une façon qui compile proprement et part en production.
- Écrit pour
- Les équipes produit sur Next.js 14+ avec l'App Router.
- Non couvert
- Il s'agit ici de la frontière du framework, pas de votre design system ni de votre standard d'accessibilité. Complétez-le par un playbook au niveau des composants si c'est de cela que vos revues débattent réellement.
11 règles4 bloquantes6 warning1 info
01 Ce qui est serveur reste serveur
errorUn fichier portant `"use client"` ne doit pas lire de variable d'environnement hors `NEXT_PUBLIC_`, ni importer le client de base de données, ni importer un SDK serveur — et aucun module qu'il tire ne le doit non plus. Marquez les modules serveur avec `import "server-only"` pour que l'erreur fasse échouer le build au lieu de partir en production. Le bundler inline tout ce que le graphe client peut atteindre, sans le moindre avertissement.
Bloque : le mode de défaillance, c'est un secret publié — et il est invisible en revue sans ce contrôle.
Portéeapp/**src/app/**components/**
Tagssecuritynextjsboundary
02 Les server actions se valident et s'autorisent elles-mêmes
errorUne fonction marquée `"use server"` est un endpoint HTTP public. Parsez ses arguments avec un schéma et contrôlez la session et les permissions à l'intérieur de l'action — pas dans le composant qui affiche le formulaire. L'action peut être invoquée directement, avec n'importe quel payload, par quiconque connaît son identifiant, sans que votre interface ne s'affiche jamais.
Bloque : une server action non protégée est un endpoint d'écriture ouvert sur votre propre domaine.
Portéeapp/**src/app/****/actions/**
Tagssecuritynextjsvalidation
03 Assainissez avant `dangerouslySetInnerHTML`
errorChaque usage passe par le helper d'assainissement partagé, dans la même expression. Interpoler du contenu utilisateur, du contenu de CMS ou une sortie de modèle sans traitement est refusé. « Le CMS est de confiance » cesse d'être vrai à la première intégration collée par un rédacteur.
Bloque : un XSS stocké sur une surface partagée touche tous les visiteurs, pas seulement l'auteur.
Portéeapp/**src/**components/**
Tagssecurityreactxss
04 Les contrôles d'accès s'exécutent côté serveur
errorLes routes protégées sont filtrées dans un middleware, un layout ou un composant serveur. Rediriger depuis un `useEffect` après le rendu est refusé : le payload protégé a déjà été sérialisé et envoyé au navigateur avant que la redirection ne s'exécute — les données sont dans le source de la page, quoi que voie l'utilisateur.
Bloque : les données ont déjà quitté le serveur — c'est une divulgation, pas un défaut d'UX.
Portéeapp/**src/app/**middleware.*
Tagssecuritynextjsauth
05 `"use client"` se pose sur la feuille
warningPlacez la directive sur le plus petit composant qui a réellement besoin d'état, d'effets ou de gestionnaires d'événements. Un `page.tsx` ou un `layout.tsx` qui la porte est refusé. La directive est héritée par tout ce qui se trouve en dessous : un placement trop haut déplace un sous-arbre entier — et ses dépendances — dans le bundle client.
Avertit : c'est une régression de performance et de poids — correct, mais coûteux.
Portéeapp/**src/app/**
Tagsnextjsperformance
06 Chaque `fetch` serveur déclare sa mise en cache
warningUn `fetch` dans un composant serveur passe une option `cache` explicite ou `next: { revalidate }`. S'en remettre au défaut du framework est refusé. Ce défaut a changé dans les deux sens entre versions majeures de Next : un choix implicite fait dépendre votre correction de votre lockfile.
Avertit : la conséquence, ce sont des données périmées ou non mises en cache — pénible, mais rattrapable.
Portéeapp/**src/app/**
Tagsnextjscaching
07 Une mutation revalide ce qu'elle a changé
warningToute server action ou tout route handler qui écrit doit appeler `revalidatePath` ou `revalidateTag` pour les données concernées, ou bien le client doit appeler `router.refresh()`. Une mutation qui laisse un cache périmé se lit, côté utilisateur, comme une écriture perdue — et il la refera une seconde fois.
Avertit : cela produit des données fausses visibles par l'utilisateur, pas une faille de sécurité.
Portéeapp/**src/app/****/actions/**
Tagsnextjscachingcorrectness
08 Ne chargez pas dans un effet ce que le serveur peut rendre
warningCharger des données dans un `useEffect` alors qu'elles sont disponibles au moment du rendu est refusé — chargez-les dans un composant serveur et passez-les en descendant. La version en effet coûte une cascade de requêtes, un état de chargement, un flash d'hydratation et un second chemin de code pour les erreurs, sans rien apporter en échange.
Avertit : c'est un défaut d'architecture au coût réel mais non urgent.
Portéeapp/**src/**components/**
Tagsnextjsreactperformance
09 Un effet nettoie ce qu'il démarre
warningTout `useEffect` qui ajoute un écouteur d'événement, ouvre un socket, lance un intervalle ou crée un `AbortController` renvoie le nettoyage correspondant. Le Strict Mode exécute les effets deux fois en développement et les changements de route les démontent en permanence — sans nettoyage, les gestionnaires s'accumulent pour toute la durée de vie de l'onglet.
Avertit : les fuites dégradent une session longue plutôt qu'elles ne cassent la première.
Portéeapp/**src/**components/**
Tagsreactmemory
10 Les clés de liste sont des identités stables
warningLa `key` vient de l'id propre à l'élément. `key={index}` est refusé pour toute liste qui peut être réordonnée, filtrée, paginée ou amputée d'un élément. React réutilisera le mauvais nœud du DOM : l'état des champs non contrôlés et le focus se déplacent alors sur une autre ligne que celle que l'utilisateur éditait.
Avertit : cela produit un bug déroutant et difficile à reproduire, pas une panne.
Portéeapp/**src/**components/**
Tagsreactcorrectness
11 Les images passent par `next/image`
infoUtilisez `next/image` avec des `width`/`height` explicites, ou `fill`. Une balise `<img>` nue pour un asset que vous maîtrisez est refusée. Les images sans dimensions sont la première cause de décalage de mise en page, et la balise brute se prive complètement de la négociation de format et du redimensionnement.
Indicatif : c'est un rappel de qualité, avec des exceptions légitimes (SVG inline, intégrations tierces).
Portéeapp/**src/**components/**
Tagsnextjsperformanceux
Avant de tout importer
La sévérité est le levier qui décide si l'équipe garde la gate activée. Seul error bloque un merge — importez d'abord ces règles en warning, regardez ce qui se déclenche réellement sur quelques semaines de vraies pull requests, et promouvez celles qui l'ont mérité. Et réécrivez les globs de portée : les chemins ci-dessus sont des exemples, et une règle ne s'applique qu'aux fichiers correspondant à ses globs.
Les règles entrent dans Normik une par une — via le formulaire du playbook, ou en laissant un agent de code les proposer par MCP à partir d'un repo existant. Il n'y a pas d'import markdown en masse : le bouton de copie ci-dessus sert à votre AGENTS.md, à une PR où l'équipe débat de la liste, et à garder les règles sous les yeux pendant que vous saisissez celles que vous avez retenues.
Chaque règle ici est jugée comme Normik la juge : sur les lignes que la pull request ajoute. Rien dans ce playbook ne dépend de la description de la PR, d'un fichier que le diff ne touche pas, ni de l'historique d'un fichier — ce sont de vrais standards de revue, mais une gate qui n'en voit pas la preuve ne pourrait que rester muette ou deviner.
L'anatomie d'une règle, le comportement des sévérités et des globs, le versionnage et l'export AGENTS.md sont détaillés dans la documentation du playbook.