Bonnes Pratiques
Les habitudes des devs Next.js qui livrent des projets solides.
9.1Architecture de projet
Un projet bien organisé se comprend en un coup d'œil. Voici la structure recommandée :
app/
├── layout.tsx # Layout racine
├── page.tsx # Homepage
├── (marketing)/ # Groupe de routes (pas dans l'URL)
│ ├── about/
│ └── pricing/
├── (app)/ # Zone authentifiée
│ ├── dashboard/
│ └── settings/
└── api/ # API Routes
components/
├── ui/ # Composants génériques (Button, Card, Modal...)
├── forms/ # Composants de formulaires
└── layout/ # Header, Footer, Sidebar
lib/
├── utils.ts # Fonctions utilitaires
├── constants.ts # Constantes
└── types.ts # Types TypeScript partagés
hooks/
└── use-media-query.ts # Custom hooks9.2Composants : la règle du "push down"
Tu te souviens de la frontière Server / Client du chapitre 4 ? La règle d'or : pousse les Client Components le plus bas possible dans l'arbre.
// ❌ MAUVAIS : tout le composant est Client
'use client'
export default function ProductPage() {
const [count, setCount] = useState(0)
return (
<div>
<h1>Super Produit</h1> {/* <- statique */}
<p>Description longue...</p> {/* <- statique */}
<img src="/product.jpg" /> {/* <- statique */}
<ReviewsList /> {/* <- statique */}
{/* SEUL élément interactif ↓ */}
<button onClick={() => setCount(c => c + 1)}>
Ajouter au panier ({count})
</button>
</div>
)
}// ✅ BON : seul le bouton est Client
// app/produit/page.tsx (Server Component)
import { AddToCartButton } from './add-to-cart-button'
export default function ProductPage() {
return (
<div>
<h1>Super Produit</h1>
<p>Description longue...</p>
<img src="/product.jpg" />
<ReviewsList />
<AddToCartButton /> {/* ← Client isolé */}
</div>
)
}
// app/produit/add-to-cart-button.tsx
'use client'
import { useState } from 'react'
export function AddToCartButton() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(c => c + 1)}>
Ajouter au panier ({count})
</button>
)
}Résultat : 90 % de ta page reste en Server Component (pas de JS envoyé au navigateur), et seul le petit bouton interactif est en Client Component. Ton site est plus léger et plus rapide.
9.3Performance : les quick wins
Ces optimisations prennent 5 minutes chacune et ont un impact énorme :
Images
Utilise toujours next/image. Ajoute priority sur l'image above-the-fold (hero). Utilise le format WebP quand possible.
Polices
Utilise next/font au lieu de charger Google Fonts via un <link>. Zero layout shift garanti.
Imports dynamiques
Pour les gros composants pas toujours visibles (modal, graphique), utilise le lazy loading :
import dynamic from 'next/dynamic'
// Le composant n'est chargé QUE quand il est affiché
const HeavyChart = dynamic(() => import('./heavy-chart'), {
loading: () => <p>Chargement du graphique...</p>,
})
// Usage normal
export default function StatsPage() {
return (
<div>
<h1>Statistiques</h1>
<HeavyChart />
</div>
)
}9.4Gestion des erreurs
Un bon site ne crash pas. Il gère les erreurs avec élégance :
// app/error.tsx - Erreur globale
'use client'
export default function Error({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div className="text-center py-20">
<h2 className="text-2xl font-bold mb-4">
Oups, quelque chose s'est mal passé !
</h2>
<p className="text-slate-600 mb-6">{error.message}</p>
<button
onClick={reset}
className="bg-blue-600 text-white px-6 py-2 rounded-lg"
>
Réessayer
</button>
</div>
)
}
// app/not-found.tsx - Page 404
export default function NotFound() {
return (
<div className="text-center py-20">
<h2 className="text-4xl font-bold mb-4">404</h2>
<p className="text-slate-600">
Cette page n'existe pas (encore ?).
</p>
</div>
)
}9.5Sécurité de base
Quelques règles simples pour éviter les galères :
| Règle | Pourquoi | Comment |
|---|---|---|
| Valide les entrées | Injection, XSS | Zod dans les Server Actions |
| Pas de secrets côté client | Vol de clés API | Pas de NEXT_PUBLIC_ pour les secrets |
| Protège les API Routes | Accès non autorisé | Vérifie l'auth dans chaque handler |
| Sanitize le HTML | XSS | Jamais de dangerouslySetInnerHTML sur du contenu user |
// Server Action sécurisée avec Zod
'use server'
import { z } from 'zod'
const ContactSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
message: z.string().min(10).max(1000),
})
export async function sendContact(formData: FormData) {
const result = ContactSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
})
if (!result.success) {
return { error: 'Données invalides' }
}
// ✅ Les données sont validées et typées
await saveToDatabase(result.data)
return { success: true }
}9.6Le checklist avant de publier
Avant chaque mise en production, passe par cette checklist :
npm run buildpasse sans erreur- Toutes les images utilisent
next/image - Chaque page a un
titleet unedescriptionuniques - Les
.env.localsont dans le.gitignore - Les Server Actions valident les entrées (Zod)
- Les pages
error.tsxetnot-found.tsxexistent - Lighthouse score 90+ sur Performance et Accessibilité
- Le site est responsive (testé sur mobile)