
Mon premier projet Next.js était un bordel.
Des composants partout. Des utils mélangés avec des hooks. Des routes API qui importaient depuis des dossiers random. Chaque fois que je devais trouver quelque chose, je passais 5 minutes à chercher.
Trois ans plus tard, j'ai travaillé sur des dizaines de projets Next.js. Certains en solo, certains avec des équipes de 10+. J'ai essayé toutes les structures que j'ai trouvées sur internet. La plupart ont échoué quand le projet a grandi.
Voici ce qui marche vraiment.
La structure que j'utilise aujourd'hui
src/
├── app/ # App Router (routes uniquement)
│ ├── (auth)/
│ │ ├── login/
│ │ │ └── page.tsx
│ │ └── register/
│ │ └── page.tsx
│ ├── (main)/
│ │ ├── dashboard/
│ │ │ ├── page.tsx
│ │ │ └── loading.tsx
│ │ └── settings/
│ │ └── page.tsx
│ ├── api/
│ │ └── webhooks/
│ │ └── route.ts
│ ├── layout.tsx
│ └── page.tsx
│
├── components/
│ ├── ui/ # Design system (boutons, inputs, cards)
│ │ ├── button.tsx
│ │ ├── input.tsx
│ │ └── card.tsx
│ └── features/ # Composants spécifiques aux features
│ ├── auth/
│ │ ├── login-form.tsx
│ │ └── register-form.tsx
│ └── dashboard/
│ ├── stats-card.tsx
│ └── recent-activity.tsx
│
├── lib/ # Utilitaires core
│ ├── db.ts # Client base de données
│ ├── auth.ts # Configuration auth
│ └── utils.ts # Helpers génériques
│
├── hooks/ # Hooks React custom
│ ├── use-user.ts
│ └── use-media-query.ts
│
├── services/ # Logique métier & appels API
│ ├── user.service.ts
│ └── payment.service.ts
│
├── types/ # Types TypeScript
│ └── index.ts
│
└── config/ # Configuration app
└── site.ts
Laisse-moi t'expliquer pourquoi chaque décision compte.
Leçon 1 : garde app/ pour le routing uniquement
Le dossier app/ est pour le routing. C'est tout.
Avant, je mettais des composants, des utils, et toutes sortes de trucs dans app/. Grosse erreur. Le dossier devenait impossible à naviguer.
# ❌ Ce que je faisais avant
app/
├── dashboard/
│ ├── page.tsx
│ ├── DashboardHeader.tsx
│ ├── DashboardSidebar.tsx
│ ├── useDashboardData.ts
│ └── dashboard.utils.ts
# ✅ Ce que je fais maintenant
app/
├── dashboard/
│ ├── page.tsx
│ └── loading.tsx
components/features/dashboard/
├── dashboard-header.tsx
├── dashboard-sidebar.tsx
└── stats-card.tsx
hooks/
└── use-dashboard-data.ts
Le fichier page importe ce dont il a besoin. La structure de routing reste propre.
Leçon 2 : les route groups changent tout
Les route groups (dossiers avec parenthèses) permettent d'organiser les routes sans affecter les URLs.
app/
├── (auth)/ # URL: /login, /register
│ ├── login/
│ └── register/
├── (main)/ # URL: /dashboard, /settings
│ ├── dashboard/
│ └── settings/
└── (marketing)/ # URL: /, /pricing, /about
├── page.tsx
├── pricing/
└── about/
Chaque groupe peut avoir son propre layout :
// app/(auth)/layout.tsx
export default function AuthLayout({ children }) {
return (
<div className="min-h-screen flex items-center justify-center">
{children}
</div>
);
}
// app/(main)/layout.tsx
export default function MainLayout({ children }) {
return (
<div className="flex">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
);
}
Plus de layouts conditionnels dans un seul fichier. Chaque section a sa propre structure.
Leçon 3 : sépare les composants UI des composants feature
Ça m'a pris des années à comprendre.
Les composants UI sont génériques, réutilisables. Boutons, inputs, cards, modals. Ils ne connaissent pas ta logique métier.
Les composants feature sont spécifiques à ton app. Ils connaissent les utilisateurs, les paiements, les dashboards. Ils utilisent les composants UI.
components/
├── ui/ # Générique, réutilisable
│ ├── button.tsx # <Button variant="primary">
│ ├── input.tsx # <Input label="Email">
│ ├── card.tsx # <Card title="Stats">
│ └── modal.tsx # <Modal open={true}>
│
└── features/ # Spécifique à l'app
├── auth/
│ ├── login-form.tsx # Utilise <Button>, <Input>
│ └── user-avatar.tsx
└── billing/
├── pricing-card.tsx
└── payment-form.tsx
Quand j'ai besoin d'un bouton, je vais dans ui/. Quand j'ai besoin du formulaire de login, je vais dans features/auth/.
Plus besoin de chercher parmi 50 composants.
Leçon 4 : les services gèrent la logique métier
Avant, je mettais des appels API partout. Dans les composants. Dans les hooks. Dans les utils.
Maintenant tout passe par les services :
// services/user.service.ts
export const userService = {
async getById(id: string) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('Failed to fetch user');
return response.json();
},
async update(id: string, data: UpdateUserData) {
const response = await fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(data),
});
if (!response.ok) throw new Error('Failed to update user');
return response.json();
},
async delete(id: string) {
const response = await fetch(`/api/users/${id}`, {
method: 'DELETE',
});
if (!response.ok) throw new Error('Failed to delete user');
},
};
Les composants utilisent les services, ils ne connaissent pas fetch :
// components/features/settings/profile-form.tsx
import { userService } from '@/services/user.service';
function ProfileForm({ userId }) {
async function handleSubmit(data) {
await userService.update(userId, data);
}
// ...
}
Avantages :
- La logique API est à un seul endroit
- Facile à tester (mock le service)
- Facile à changer (remplace fetch par axios, ajoute du cache)
Leçon 5 : le dossier lib/ est pour l'infrastructure
lib/ contient ce dont ton app a besoin pour tourner. Connexions base de données. Setup auth. Clients de services externes.
// lib/db.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const db = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
// lib/auth.ts
import NextAuth from 'next-auth';
import { authConfig } from '@/config/auth';
export const { auth, signIn, signOut } = NextAuth(authConfig);
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
lib/ est bas niveau. Les services et composants importent depuis lib/, jamais l'inverse.
Leçon 6 : colocalise quand ça a du sens
Parfois un composant n'est utilisé qu'à un seul endroit. Ne crée pas un dossier pour lui.
app/
├── dashboard/
│ ├── page.tsx
│ └── _components/ # Utilisé uniquement dans dashboard
│ └── welcome-banner.tsx
Le préfixe underscore (_components) dit à Next.js de l'ignorer pour le routing. Il reste proche de là où il est utilisé.
J'utilise ça pour :
- Les composants utilisés par une seule page
- Les hooks spécifiques à une page
- Les types locaux
Si j'en ai besoin ailleurs plus tard, je le déplace dans components/features/.
Leçon 7 : les imports absolus sauvent la santé mentale
Plus de ../../../../components/ui/button.
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
Maintenant :
import { Button } from '@/components/ui/button';
import { userService } from '@/services/user.service';
import { useUser } from '@/hooks/use-user';
Propre. Clair. Pas de calcul mental pour trouver le chemin.
Ce que j'ai arrêté de faire
Organiser par type (la structure "classique")
# ❌ Ne fais pas ça
components/
├── Header.tsx
├── Footer.tsx
├── Button.tsx
├── LoginForm.tsx
├── DashboardStats.tsx
├── UserAvatar.tsx
└── ... 50 autres fichiers
Ça ne scale pas. À 50 composants, t'es perdu.
Tout mettre dans utils/
# ❌ Ne fais pas ça
utils/
├── api.ts
├── auth.ts
├── db.ts
├── format.ts
├── validation.ts
└── ... tout le reste
utils/ devient un tiroir fourre-tout. Sépare-le en lib/, services/, et helpers/.
Over-engineer dès le jour 1
J'ai vu des projets avec 20 dossiers et 5 fichiers. Ne fais pas ça.
Commence simple. Ajoute de la structure quand tu ressens la douleur. Si tu cherches des fichiers, tu as besoin de mieux organiser. Sinon, t'es bien.
L'arbre de décision
La cheat sheet
| Quoi | Où | Exemple |
|---|---|---|
| Pages & routes | app/ | app/dashboard/page.tsx |
| UI générique | components/ui/ | button.tsx, input.tsx |
| Composants feature | components/features/ | login-form.tsx |
| Hooks custom | hooks/ | use-user.ts |
| Appels API & logique | services/ | user.service.ts |
| DB, auth, utils | lib/ | db.ts, auth.ts |
| Types TypeScript | types/ | index.ts |
| Config app | config/ | site.ts |
La leçon
La structure c'est pas suivre des règles. C'est trouver les choses vite.
Quand j'ouvre un projet après 6 mois, je dois savoir où tout est. Quand un nouveau dev rejoint l'équipe, il devrait pas avoir besoin d'une visite guidée.
La meilleure structure est celle qui te fait moins réfléchir.
C'est la partie 3 de ma série "Ce que j'ai appris à mes dépens". Prochain article : ce que 3 ans de code reviews m'ont appris.
Lire la partie 4 : Ce que 3 ans de code reviews m'ont appris →
Des questions ? Contacte-moi sur LinkedIn ou découvre plus sur mon blog.