
Je suis Mathieu Ponton, Co-Founder & ingénieur logiciel chez Apogée Consult à Lyon. Ingénieur diplômé de Polytech Lyon (Informatique), j'ai fait trois ans en apprentissage, partagés entre la Métropole de Lyon (inclusion numérique avec Res'in et sobriété énergétique avec Écolyo) et Superwyze, une startup medtech (POCs, dont certains aujourd'hui industrialisés, et travail sur des codebases existantes). J'ai livré plus de 10 projets en production (web, mobile et IA / RAG) pour des PME, startups et organisations publiques.
Next.js 15 App Router : les patterns qui tiennent en production après 10 mois
- nextjs
Dix mois après avoir basculé nos projets sur App Router, voici ce qui tient vraiment en production, et les erreurs qu'on ne répétera plus.
Next.js 15 App Router : les patterns qui tiennent en production après 10 mois
Dix mois de production sur plusieurs projets avec App Router, c'est suffisant pour séparer les patterns solides des idées qui semblaient bonnes sur un README. Ce qui suit n'est pas un tutoriel. C'est un bilan d'exploitation.
Ce qu'on avait sous-estimé au départ
L'App Router change le modèle mental, pas seulement la structure de fichiers. On a mis plusieurs semaines avant de vraiment comprendre la frontière entre Server Components et Client Components, pas en théorie, mais dans la pratique d'un projet avec des dizaines de composants existants.
Le piège le plus fréquent : ajouter "use client" dès qu'on croise un useState, sans mesurer l'impact sur le rendu. Chaque directive "use client" coupe le graph côté serveur à partir de ce point. Si un composant profond dans l'arbre a besoin d'interactivité, l'encapsuler proprement dans un sous-composant client change tout.
// Mauvais : toute la section devient client
"use client"
export function ProductSection({ items }) {
const [selected, setSelected] = useState(null)
return (
<div>
<ProductList items={items} /> {/* rendu côté client alors qu'il n'en a pas besoin */}
<ProductDetail item={selected} />
</div>
)
}
// Correct : on isole l'interactivité
// ProductSection.tsx (Server Component)
export function ProductSection({ items }) {
return (
<div>
<ProductList items={items} />
<ProductSelector /> {/* Client Component isolé */}
</div>
)
}On a appliqué ce refactor sur une page dense, 23 composants, et le JS envoyé au client a diminué de 38 % sans toucher au comportement visible.
Les patterns qui ont résisté
Colocalisé par feature, pas par type
La structure app/(marketing)/landing/page.tsx avec un dossier _components/ local a tenu sur tous nos projets. On avait testé la structure par type (components/ui/, components/layout/, etc.) sur un projet précédent, elle devient illisible dès que le projet dépasse 40 routes.
La règle qu'on applique : un composant qui n'est utilisé que dans une route vit à côté de cette route. Un composant partagé entre deux features monte d'un niveau. Un composant partagé partout va dans src/components/.
Data fetching au plus près de la donnée
Dans Pages Router, on concentrait le fetching dans getServerSideProps. Avec App Router, on fetch dans chaque Server Component qui a besoin de la donnée. Ça paraît contre-intuitif au départ, plusieurs fetch() dans le même rendu. En pratique, Next.js déduplication les requêtes identiques dans le même cycle de rendu, et on gagne en lisibilité.
// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
const product = await getProduct(params.id) // fetch dédupliqué automatiquement
return (
<>
<ProductHeader product={product} />
<RelatedProducts productId={params.id} /> {/* peut aussi fetch getProduct sans doublon réseau */}
</>
)
}Route groups pour la logique d'auth
Les route groups (auth) et (public) avec des layouts distincts ont résolu proprement la gestion des sessions. On évite ainsi de dispatcher la logique d'authentification dans chaque page.tsx. Le layout (auth)/layout.tsx vérifie la session une fois, redirige si besoin, et les pages enfants ne s'en préoccupent plus.
Parallel routes pour les modales
Les parallel routes (@modal) pour les modales d'édition inline sont restées stables sur un dashboard complexe. C'est verbeux à mettre en place, mais le résultat est propre : la modale s'ouvre sans quitter la page, le state URL est préservé, et le rechargement direct fonctionne.
Ce qu'on a abandonné
generateStaticParams sur des données volatiles
On a tenté de statifier des pages produit qui changent plusieurs fois par jour. Le ISR avec revalidate court (60 secondes) donnait l'illusion du dynamique, mais on se retrouvait avec des caches incohérents entre les instances. On est passé au rendu dynamique pour ces pages et on a arrêté de chercher à les statifier.
Les Server Actions pour tout
On avait commencé à mettre des Server Actions partout, y compris pour des opérations de lecture. Erreur. Les Server Actions sont faites pour les mutations. Les utiliser pour fetcher des données donne un code plus lent et moins prévisible qu'un simple fetch() dans un Server Component. On en parle en détail dans un article séparé.
useEffect pour synchroniser des données serveur
Quelques développeurs de l'équipe avaient le réflexe Pages Router : charger des données manquantes côté client via useEffect. Avec App Router, c'est presque toujours un anti-pattern. Si une donnée doit être fraîche, elle doit venir d'un Server Component, pas d'un effet client.
Les décisions d'architecture qui ont payé
Typescript strict dès le début. Les types des params et searchParams dans les page.tsx ont changé en Next.js 15, ils sont désormais des Promise. Activer strict mode et typer correctement dès le départ nous a évité des régressions silencieuses lors des upgrades.
// Next.js 15, params et searchParams sont des Promise
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ id: string }>
searchParams: Promise<{ q?: string }>
}) {
const { id } = await params
const { q } = await searchParams
// ...
}Un seul point d'entrée pour les appels API. On a centralisé tous les fetch() dans des fonctions dans src/lib/api/. Chaque fonction accepte les options de cache explicitement. Ça rend les décisions de caching lisibles et auditables, on voit d'un coup d'œil quelles données sont statiques, lesquelles sont dynamiques, lesquelles sont revalidées.
Erreur boundaries explicites. On a ajouté des fichiers error.tsx par section de l'application plutôt qu'un seul global. Un crash dans le bloc de recommandations ne plante plus toute la page produit.
Ce qui reste inconfortable
L'outillage de debug côté serveur reste en retrait par rapport à ce qu'on avait avec Pages Router. Quand un Server Component génère une erreur en production, les stack traces sont parfois tronquées ou peu informatives. On passe plus de temps à instrumenter les logs manuellement qu'avec l'ancienne approche.
Le cache de Next.js 15 est puissant, mais ses règles d'invalidation sont encore complexes à raisonner. On a eu deux incidents en production liés à des données servies depuis le cache alors qu'on pensait la route dynamique. La règle qu'on a adoptée : documenter explicitement la stratégie de cache de chaque route dans un commentaire en tête de fichier.
La migration depuis Pages Router sur un projet de 80+ routes est un sujet à part entière, on le couvre dans un article dédié.