- Next.js
- Architecture
- TypeScript
Structurer un projet Next.js App Router qui tient sur la durée
Les conventions de dossiers, de composants serveur et de récupération de données que j'applique sur tous mes projets Next.js depuis deux ans.
L'App Router de Next.js apporte une vraie liberté d'organisation, et c'est précisément le problème. Sans convention, un projet de six mois devient un labyrinthe de components/ à trois niveaux. Voici la structure que j'applique systématiquement.
Séparer la route de l'interface
Une règle simple : les fichiers dans app/ ne contiennent que de la composition et de la récupération de données. Aucun style, aucune logique d'interaction.
// app/[locale]/projets/page.tsx
export default async function ProjectsPage({ params }: Props) {
const { locale } = await params;
const projects = await getProjects(locale);
return <ProjectGrid projects={projects} locale={locale} />;
}Tout le reste vit dans components/. L'avantage se révèle au moment de réorganiser les URLs : déplacer une route ne touche plus au code d'interface.
Trois familles de composants
J'évite les arborescences profondes au profit de trois dossiers plats :
components/ui/: primitives sans logique métier :Button,Card,Badge.components/sections/: blocs de page complets :Hero,SkillsSection.components/layout/: navigation, pied de page, fournisseurs de contexte.
Si un composant ne trouve pas sa place dans l'une des trois, c'est généralement qu'il mélange deux responsabilités.
Garder les composants clients aux extrémités
Le réflexe le plus coûteux est de marquer "use client" trop haut dans l'arbre. Tout ce qui est en dessous part alors dans le bundle navigateur.
La bonne approche consiste à descendre la frontière aussi bas que possible : un bouton de thème est un composant client, la page qui le contient reste serveur.
Le cas des animations
Framer Motion impose "use client". Plutôt que de rendre toute une section cliente, j'isole un composant Reveal minimal qui ne fait qu'animer son children :
"use client";
export function Reveal({ children, delay = 0 }: Props) {
return (
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-80px" }}
transition={{ duration: 0.5, delay }}
>
{children}
</motion.div>
);
}Le contenu passé en children est rendu côté serveur : il n'entre jamais dans le bundle.
Centraliser le contenu dans data/
Les textes, listes de projets et compétences vivent dans des fichiers TypeScript sous data/. C'est typé, versionné avec le code, et modifiable sans toucher à un seul composant. Pour un portfolio ou un site vitrine, c'est largement suffisant : un CMS devient utile seulement quand une personne non technique doit publier.
Ce que je ne fais plus
Deux habitudes abandonnées en route :
- Les barrel files
index.tspartout. Ils cassent le tree-shaking et créent des dépendances circulaires silencieuses. - Les hooks génériques trop tôt. Un
useFetchmaison est presque toujours moins bon que le cache natif defetchdans les composants serveur.
La structure n'est pas une fin en soi : elle sert à ce qu'un développeur qui revient sur le projet six mois plus tard sache où chercher en moins de dix secondes.
Articles liés
Ma méthode pour un front-end Angular qui reste maintenable
Découpage par domaine, composants standalone, signaux contre RxJS, intercepteurs et détection de changement : les décisions que je prends au démarrage d'un projet Angular.
- Angular
- TypeScript
- Architecture
- Frontend
Intégrer les API de paiement des agrégateurs sans se faire piéger
Mobile Money, cartes bancaires, agrégateurs multiples : le modèle unifié, l'idempotence et la réconciliation qui évitent les doubles débits en production.
- Paiement
- FinTech
- Architecture
- API