Démarrage d'un projet Next.js
Il est moins coûteux de poser les fondations le premier jour : chaque règle doit ensuite être appliquée sur un code qui la viole déjà. Si le modèle frontend-starter existe — commence par une copie du modèle : les étapes ci-dessous y sont déjà effectuées et vérifiées, ce document reste une explication des décisions et une checklist d'audit.
Ce document définit l'ordre des étapes et le socle minimal. La manière d'écrire le code lui-même est décrite dans le skill fsd-frontend (s'il existe dans le projet), l'authentification dans frontend-auth-session, et la vérification avant commit dans anti-slop-review.
Les configurations prêtes sont dans references/configs.md, la structure Next + FSD avec exemples de code dans references/next-structure.md.
Priorité des règles. Les instructions explicites de l'utilisateur et les fichiers d'agent du projet (AGENTS.md, CLAUDE.md) sont plus importants que ce document. Les versions des bibliothèques changent : avant de copier une configuration, vérifie-la avec la documentation de la version installée et ne l'affirme pas comme fonctionnelle tant que tu ne l'as pas exécutée.
Les faits sur Next.js viennent de la documentation dans le paquet, pas de ce document ni de la mémoire. Depuis la version 16.2, la documentation se trouve dans node_modules/next/dist/docs/, et le fichier AGENTS.md du projet y fait référence. En cas de divergence avec les exemples de ce document, la documentation de la version installée prévaut.
Notations : MUST — ne pas enfreindre ; SHOULD — par défaut oui, toute dérogation nécessite une justification.
1. Décisions avant le code
Chaque décision du tableau est consignée dans le README (section « Décisions ») ou dans un ADR, avec sa justification. Ne pas s'écarter de la valeur par défaut sans raison écrite.
| Question | Par défaut | Sinon |
|---|---|---|
| Next.js ou SPA | Next.js si au moins un besoin parmi : SSR/SEO, BFF (secrets, HttpOnly-cookie), route handlers | Application interne derrière un login sans SEO ni BFF → Vite + React Router : build et déploiement plus simples |
| Routeur | App Router | Pages Router — uniquement dans un projet existant |
| Mono-repo | Non, une seule application | Une deuxième application ou un paquet apparaît réellement → pnpm workspaces ; Nx/Turborepo — quand le build ou le CI commencent à ralentir |
| Multi-Zones, micro-frontends | Non | Équipes indépendantes avec des releases indépendantes |
| Données serveur | Server Components pour le chargement initial ; TanStack Query pour les données client interactives et les mutations | Application purement client → seulement TanStack Query |
| Textes et i18n | Module de traduction dès le premier jour (next-intl ou équivalent), même pour une seule locale, si le projet suit fsd-frontend | Décision explicite « sans i18n » consignée dans le README |
| Gestionnaire de paquets | pnpm, version dans packageManager | — |
| Déploiement | Décider immédiatement : Vercel, serveur Node (output: "standalone") ou Docker | Les env, le cache et le CI en dépendent |
MUST : ne pas ajouter une technologie « pour le futur ». Le mono-repo, le deuxième routeur, Storybook, le state manager, MSW « comme un type de backend » apparaissent seulement quand ils ont leur premier consommateur réel.
2. Ordre des étapes
- Créer le projet (
pnpm create next-app@latest) : TypeScript, ESLint, Tailwind, App Router. FigerpackageManager,engines.node(Next 16 nécessite Node 20.9+) et.nvmrc. - Supprimer immédiatement le modèle du générateur : la page d'accueil, les SVG dans
public/, le README par défaut, les styles de démonstration. Remplacer le README par le squelette dereferences/configs.md. Un modèle laissé « pour plus tard » survit jusqu'en production.AGENTS.mdetCLAUDE.mddecreate-next-appne sont pas supprimés : le bloc entre<!-- BEGIN:nextjs-agent-rules -->et<!-- END:nextjs-agent-rules -->indique aux agents la documentation de la version installée,next devle maintient automatiquement. Les règles propres au projet sont ajoutées en dehors de ces marqueurs. - TypeScript strict (section 4 de
references/configs.md) et scripttypecheck(tsc --noEmit). - Structure FSD au-dessus de Next (section 3 ci-dessous).
- Linter et formatage : ESLint flat config de
eslint-config-next+ règles type-aware + linter des frontières + Prettier. Next 16 a suppriménext lintet ne lint pas lors d'unnext build— le linter est exécuté via un script séparé et dans le CI. - Configuration des variables d'environnement (section 4).
- Modèle d'erreurs et client API dans
shared/api(section 5). - Providers dans la couche
_app: QueryClient, thème, i18n, services globaux (toast, confirm) comme port dansshared+ implémentation dans_app. - Base UI (section 6).
- Tests : Vitest + Testing Library + MSW, Playwright pour l'e2e (section 7).
- Git hooks : husky + lint-staged (eslint --fix, prettier) + commitlint.
- CI : lint, typecheck, test, build pour chaque push et PR (section 8).
- Sécurité par défaut (section 9).
- README rempli avec ce qui est vrai (section 10), et Definition of Done validée (section 11).
Les étapes 1 à 3 forment un commit séparé « scaffold » : ainsi l'historique montre ce que le générateur a fait et ce que tu as fait.
3. FSD au-dessus du routage Next
D'après le guide officiel FSD pour App Router : le dossier réservé Next app/ est à la racine du projet, les couches FSD sont dans src/, et les couches app et pages reçoivent le préfixe _ pour éviter toute confusion avec les dossiers réservés Next.
app/ routage Next : uniquement les fichiers de routes
├── layout.tsx layout racine → providers depuis src/_app
├── (auth)/sign-in/page.tsx export { SignInPage as default } from "@/_pages/sign-in"
└── api/auth/login/route.ts route handler (BFF), fin
proxy.ts Next 16+ (avant middleware.ts), à côté de app/
src/
├── _app/ couche app : providers, styles globaux, implémentations des services
├── _pages/ couche pages
├── widgets/ (si le projet les utilise)
├── features/
├── entities/
└── shared/{ui,api,lib,config}
MUST :
- Les fichiers dans
app/sont fins : réexport de la page depuissrc/_pages,generateMetadata, parsing deparams/searchParams(dans Next 15+ c'est unePromise, il fautawait), et passage par props. Pas de logique. - Les route handlers et Server Actions sont fins : analyser et valider l'entrée → appeler une fonction depuis
src→ retourner la réponse. Chacun d'eux est un point d'entrée public : il vérifie l'autorisation lui-même, sans compter surproxy.ts. - Ne pas créer
src/appetsrc/pages: Next cherche les dossiers réservés aussi bien à la racine que danssrc, et un dossier portant ce nom peut facilement devenir un deuxième routeur. - Le linter des frontières connaît les préfixes : motifs
src/_app,src/_pages/*au lieu desrc/app,src/pages/*(configuration —fsd-frontend,references/conventions.md, section 11). "use client"est placé le plus bas possible dans l'arbre. La page est par défaut serveur ; les pages clientes sont des feuilles avec de l'interactivité.- Les modules contenant des secrets commencent par
import "server-only", les modules avecwindowparimport "client-only".
Voir references/next-structure.md pour plus de détails et de code.
4. Configuration de l'environnement
MUST :
- Les variables sont lues dans deux modules :
shared/config/env.server.ts(import "server-only") etshared/config/env.client.ts. Le schéma est enzod, la validation a lieu à l'import : une erreur de configuration se produit au démarrage, pas lors de la première requête. Dans le reste du code,process.envest interdit (règle du linter). - Les variables
NEXT_PUBLIC_*sont intégrées dans le bundle lors de la compilation et uniquement lors d'un accès directprocess.env.NEXT_PUBLIC_X: pas de déstructuration ni deprocess.env[name]. Il est impossible de construire une seule image pour plusieurs environnements avec des valeurs publiques différentes — ces valeurs sont lues sur le serveur et transmises vers le bas. - Les secrets sont uniquement sans le préfixe
NEXT_PUBLIC_. .env.examplecontient chaque variable avec un commentaire et une valeur sécurisée ;.env*est dans.gitignore, sauf.env.example. La compilation dans le CI se fait avec les valeurs de.env.example.
5. Modèle d'erreurs et client API
MUST :
- Les erreurs sont normalisées en
AppErroravec unkindune seule fois, à la frontière deshared/api(modèle —fsd-frontend,references/conventions.md, section 1). Au-dessus deshared/api, personne ne parse leResponse, les statuts ni les textes d'erreur. - Un seul
request()au-dessus defetch(ou un client généré, enveloppé de la même manière) : il transmet lesignal, convertit une erreur réseau ennetwork, le statut HTTP enkind, et valide le corps de la réponse avec un schéma. Exemple dansreferences/next-structure.md. - Le type d'erreur est déterminé par le code : statut,
extensions.code(GraphQL répond avec 200 même en cas d'erreur),codedans le corps. Si le backend ne renvoie que du texte, la correspondance est faite par le texte dans un seul mapper, avec un commentaire de justification et des tests. - La réponse du backend est mappée vers un type domaine à la frontière ; les DTO ne remontent pas dans l'UI.
- Les données utilisateur ne sont pas mises en cache entre les utilisateurs : pas de
"use cache"/unstable_cachepour les réponses dépendant de la session. - Le client est généré à partir du schéma (OpenAPI, GraphQL) si celui-ci existe. Le schéma ou le code généré est dans le dépôt : le CI ne doit pas contacter le backend en direct.
SHOULD : une interface sur le transport (AuthApi + factory) — uniquement quand elle a déjà un deuxième consommateur (fsd-frontend, references/state-and-data.md, 2.1). On ne crée pas de valeur de configuration de type "mock" qui lance une exception "not implemented".
6. Base UI
- Tokens de design — variables CSS dans
:root(et pour le thème sombre), reliées à Tailwind v4 via@theme. Dans les composants, uniquement des tokens ; les valeurs brutes HEX et arbitraires (text-[#3a3a3a]) sont une exception ponctuelle avec commentaire. - Les classes existent. Les composants copiés depuis shadcn/ui et les exemples arrivent avec leurs propres tokens (
bg-card,text-muted-foreground). Soit tu ajoutes ces tokens, soit tu remplaces les classes — sinon le composant reste silencieusement sans style. - Les primitives
shared/uisont construites sur une bibliothèque headless (Radix, React Aria) pour l'accessibilité ; les variantes sont gérées aveccva, la fusion de classes aveccn(clsx+tailwind-merge). - Polices —
next/font, images —next/imageavec dimensions,images.remotePatternslisté explicitement. - Storybook — seulement quand le kit UI est réutilisé par plusieurs applications ou équipes, pas avant.
7. Tests
- Vitest + Testing Library + jsdom + MSW avec
onUnhandledFrame: "error"(MSW 3 ; dans MSW 2,onUnhandledRequest). Configuration dansreferences/configs.md. - Vitest ne peut pas rendre les Server Components
async. Extrais leur logique dans des fonctions et teste-les via des tests unitaires ; la page elle-même est testée en e2e. - Playwright pour 3 à 10 chemins critiques. Dans le CI, les e2e s'exécutent contre un backend prévisible (serveur mock, environnement avec base de données de test), pas contre un environnement partagé où les tests modifient les données des autres.
- Le premier test est écrit le jour zéro : il vérifie que l'infrastructure de test fonctionne et définit un exemple.
8. CI
MUST : pour chaque push sur la branche principale et pour chaque PR : lint, typecheck, test, build. Le modèle GitHub Actions est dans references/configs.md. Pour les dépôts publics, Actions est gratuit ; pour les dépôts privés, un quota mensuel de minutes s'applique.
- La compilation dans le CI se fait avec
.env.example, sans secrets. - e2e — dans un job séparé, quand il existe un backend isolé ; sinon, mention explicite dans le README de la raison pour laquelle ils ne sont pas dans le CI.
- Le badge CI dans le README apparaît en même temps que le workflow.
9. Sécurité par défaut
poweredByHeader: false; en-têtes viaheaders()dansnext.config.ts:X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin,X-Frame-Options: DENY(ouframe-ancestorsdans CSP). CSP avec nonce est une tâche distincte, mais la décision « présent / absent et pourquoi » est consignée immédiatement.- Server Actions et route handlers sont des points de terminaison publics : validation de l'entrée avec un schéma et vérification des droits dans chacun.
dangerouslySetInnerHTML— uniquement avec un assainisseur et un commentaire de justification.- Les données personnelles et les jetons ne sont pas journalisés.
- Autorisation — selon
frontend-auth-session.
10. README
Le squelette est dans references/configs.md. Une seule règle : dans le README, seulement ce qui est vrai pour le code de ce commit. Chaque section est mise à jour dans le même commit que le code qu'elle décrit. Les plans sont dans une section séparée « Suite », avec la mention qu'ils ne sont pas encore réalisés.
11. Definition of Done pour le démarrage
- Le modèle du générateur est supprimé ; le README est un squelette rempli avec des informations vraies.
-
packageManager,engines.node,.nvmrcsont figés. - TS strict ;
typecheck,lint,test,buildpassent en local (exécutés, pas supposés). - Structure FSD avec préfixes
_app/_pages; le linter des frontières détecte les imports vers le haut. - Variables d'environnement via deux modules avec schéma ;
process.enven dehors de ceux-ci est interdit par le linter ;.env.examplecomplet. -
AppErroretrequest()dansshared/api, couverts par des tests (réseau, statuts, schéma incorrect). - Providers dans
_app; QueryClient non partagé entre les utilisateurs sur le serveur. - Tokens de design en variables CSS ; toutes les classes utilisées existent.
- Vitest + MSW + Playwright configurés, au moins un test vivant chacun.
- Git hooks et commitlint fonctionnent.
- CI vert.
- En-têtes de sécurité et
poweredByHeader: false. - Décisions de la section 1 consignées.
12. Quand tu hésites
- Entre « ajouter maintenant » et « ajouter quand ce sera nécessaire » — quand ce sera nécessaire.
- Entre « configurer manuellement » et « laisser comme
create-next-appl'a généré » — laisse, tant qu'il n'y a pas de raison de changer, et note la raison quand tu changes. - Entre « une seule application » et « mono-repo » — une seule application.
- Entre « configuration de mémoire » et « configuration de la documentation de la version actuelle » — de la documentation.