La carte de Next.js
Tu sais faire des composants, gérer de l'état, parler à un serveur avec fetch. Il te
manque tout ce qu'il y a autour : le routage, le serveur, le build, le référencement — le
trou exact que Next.js vient boucher. Avant de plonger dans ses mécanismes, qui auront chacun leur
module, on dessine la carte : ce qu'il y a dans un projet, où ça vit, à quoi ça
sert. Une carte ne te dit pas comment marcher ; elle t'empêche de te perdre.
Pourquoi un framework par-dessus React ?
Tu connais déjà parfaitement cette relation, tu ne l'avais peut-être juste pas nommée. React Native aussi ne fait « que » l'interface ; il ne dit rien du build, des permissions natives, des mises à jour par-dessus l'air, ni de la navigation. C'est Expo qui apporte tout ça — revois le module sur Expo (../halterofit/expo.html) : le bundler, la configuration native, les modules prêts à l'emploi, la chaîne de build EAS. Expo t'appelle exactement comme Next.js t'appellera.
Retiens la symétrie, elle vaut de l'or en entrevue : React → Next.js sur le web, React Native → Expo sur mobile. Même couche, même rôle, même compromis liberté/convention. Tu n'apprends donc pas un concept neuf ici : tu appliques un concept que tu pratiques déjà quotidiennement, à un nouveau terrain.
Le point de départ, c'est de voir à quel point React est petit. Il ne fait qu'une chose : décrire une interface en fonction d'un état — le modèle que tu connais depuis le module sur le modèle de React, dans le guide Halterofit. Il ne dit rien sur : quelle URL affiche quel écran ; comment transformer trente fichiers TypeScript en un paquet que le navigateur sait charger ; comment fabriquer du HTML sur un serveur pour que Google et les réseaux sociaux voient autre chose qu'une page vide ; comment mettre en cache une réponse, redimensionner une image, gérer les variables d'environnement. Aucune de ces questions n'a de réponse dans React, et elles sont pourtant toutes obligatoires dès qu'on met un vrai site en ligne.
Chaque équipe assemblait donc sa propre tour de briques : un bundler (Webpack, puis Vite), un routeur tiers (React Router) dont la configuration se maintient à la main, un serveur de rendu maison pour le SEO — la brique la plus douloureuse, avec deux points d'entrée à garder synchronisés —, une bibliothèque de data fetching et de cache (React Query, SWR), une gestion des métadonnées (titre, Open Graph), un découpage du code réglé à la main pour ne pas envoyer 2 Mo de JavaScript d'un coup, plus les images, les polices, les variables d'environnement et le déploiement. Le vrai coût n'est pas d'écrire une brique : c'est de garantir que les huit s'entendent entre elles, et de continuer à le garantir pendant trois ans de mises à jour.
Un framework, c'est la réponse à ce coût. Next.js prend toutes ces décisions à ta place, les fait tenir ensemble, et te livre l'ensemble déjà câblé : le routage, le bundler (c'est Turbopack, désormais le bundler par défaut), le rendu serveur, le cache, l'optimisation d'images. Tu échanges de la liberté contre de la convention — et la convention, quand elle est bonne, est un accélérateur monumental : tu ne passes plus tes journées à assembler, tu les passes à écrire des fonctionnalités.
La distinction tient en une phrase : tu appelles une bibliothèque, un framework t'appelle. Avec React seul, c'est ton code qui décide quand monter l'application, sur quel élément du DOM, dans quel ordre. Avec Next.js, c'est lui qui a le contrôle : il reçoit la requête HTTP, décide quel fichier correspond à l'URL demandée, appelle ton composant, fait quelque chose du résultat. Tu ne pilotes plus, tu remplis des cases à des emplacements convenus — d'où l'importance de les connaître, tout l'objet de ce module.
Les deux routeurs, et pourquoi on n'apprend que l'App Router
Voici le premier piège de vocabulaire, et il est de taille parce qu'il pollue à peu près tous les tutoriels que tu trouveras en ligne. Next.js a eu deux systèmes de routage successifs, qui coexistent encore dans le même produit. Le Pages Router est le système historique : toujours supporté, il fait tourner des milliers d'applications en production, mais c'est l'ancien monde — tout y est composant client, rendu une fois sur le serveur puis « réhydraté » dans le navigateur. L'App Router est le système actuel : la structure des dossiers y décrit toute la navigation (layouts imbriqués, états de chargement, erreurs), et il est bâti sur les Server Components — par défaut un composant s'exécute sur le serveur et n'envoie jamais son code au navigateur, changement de modèle mental assez profond pour mériter son propre module (Server et Client Components). Ce guide n'enseigne que l'App Router ; de l'ancien monde, tu dois seulement savoir le reconnaître, sinon tu perdras une demi-heure devant un vieux tutoriel ou le dépôt d'un test technique.
| Indice | Pages Router (ancien) | App Router (actuel) |
|---|---|---|
| Dossier racine des routes | pages/ | app/ |
| Nom du fichier d'une page | n'importe lequel : pages/about.tsx | toujours page.tsx : app/about/page.tsx |
| Emballage partagé | un unique pages/_app.tsx + _document.tsx | layout.tsx, imbriquables à chaque niveau |
| Chargement des données | getServerSideProps, getStaticProps, getStaticPaths — exportés à côté du composant, puis passés en props | await directement dans le composant serveur |
| API HTTP | pages/api/hello.ts | app/api/hello/route.ts |
| Modèle par défaut | tout est composant client | tout est composant serveur par défaut |
Le test le plus rapide devant un dépôt inconnu : y a-t-il un dossier app/ ou un
dossier pages/ à la racine (ou dans src/) ? Puis cherche la chaîne
getServerSideProps : si elle apparaît, tu es dans l'ancien monde ; si tu vois des
layout.tsx partout, dans le nouveau. Un projet peut légitimement contenir les deux
pendant une migration progressive — Next.js l'autorise, une route à la fois. Ce changement n'était
pas un caprice de version : dans le Pages Router, les données se chargeaient au niveau de la
page entière, jamais au niveau du composant qui en avait besoin. Les Server Components
suppriment cette contrainte — le module Rendu & data fetching y revient.
Un collègue migre une vieille page : il colle ce fichier tel quel dans app/rendez-vous/[service]/page.tsx. La page plante aussitôt avec « Cannot read properties of undefined (reading 'map') ». Que s'est-il passé ?
// pages/rendez-vous/[service].tsx — l'ANCIEN monde, recopié tel quel
export async function getServerSideProps({ params }) {
const slots = await findSlots(params.service);
return { props: { slots } };
}
export default function Page({ slots }) {
return <ul>{slots.map((s) => <li key={s.id}>{s.heure}</li>)}</ul>;
}
Dans l'App Router, getServerSideProps ne veut plus rien dire : c'est un export nommé comme un autre, que personne n'appelle. Il ne s'exécute donc jamais — et rien ne t'avertit, puisque exporter une fonction est parfaitement légal. Le composant, lui, ne reçoit aucun slots : une page ne reçoit que params et searchParams. slots vaut donc undefined, d'où le plantage sur .map. La version App Router fait le travail dans le composant : export default async function Page({ params }), puis const { service } = await params; et const slots = await findSlots(service);. Au passage, c'est le test le plus rapide pour dater une ressource trouvée en ligne.
L'anatomie d'un projet Next.js
À quoi ressemble le dossier d'un projet fraîchement créé ? Rassurant : il y a très peu de choses, Next.js n'exige presque aucune configuration. Prends le temps d'associer chaque entrée à son rôle, c'est ce que tu chercheras des yeux dans chaque projet que tu ouvriras.
mon-projet/
├── app/ ← LE cœur : chaque dossier ici devient un segment d'URL
├── public/ ← fichiers servis tels quels : /logo.svg, /robots.txt, favicon…
├── next.config.js ← configuration du framework (images distantes, redirections…)
├── proxy.ts ← code exécuté AVANT la route (redirections, en-têtes, auth légère)
├── .env.local ← secrets et réglages de TA machine — jamais commité
├── eslint.config.mjs ← règles de qualité de code (ESLint, config « à plat »)
├── tsconfig.json ← réglages TypeScript, dont l'alias d'import « @/ »
├── next-env.d.ts ← types générés par Next.js — on ne le modifie JAMAIS à la main
├── package.json ← dépendances + les scripts dev / build / start
└── node_modules/ ← les dépendances installées (jamais commité)
Un projet Next.js minimal. Certains glissent app/ (et proxy.ts) dans un
dossier src/ : option officielle, purement cosmétique, ne te laisse pas surprendre.
Détaillons les entrées les moins évidentes.
public/ contient les fichiers statiques, servis tels quels
sans passer par le build : public/logo.svg est accessible à l'adresse
/logo.svg — la racine du site correspond à la racine de public/, pas à
/public/…. C'est là que vont le favicon, les images de marque, un
robots.txt. Corollaire à ne jamais oublier : tout ce que tu y mets est
téléchargeable par n'importe qui — ce n'est pas un endroit pour un fichier « interne ».
proxy.ts est le plus récent de la liste, et son nom a changé : c'est le
successeur de ce que l'écosystème appelait middleware.ts — si tu croises ce nom-là dans
un article, c'est l'ancienne appellation de la même case. La fonction qu'il exporte s'exécute
avant que Next.js ne décide quelle route rendre, sur chaque requête qui correspond à
son matcher (le filtre d'URL qu'on lui déclare) : redirections, réécritures d'URL, pose
d'un en-tête ou d'un cookie. L'équipe Next.js insiste sur un point — c'est un outil de dernier
recours, pas l'endroit où mettre ta logique métier ni ta seule vérification d'autorisation,
laquelle doit aussi vivre au plus près des données.
next.config.js, enfin, est court mais dense en indices : les domaines
d'images autorisés révèlent d'où viennent les médias, les redirections révèlent l'histoire du site
(une page qui a bougé, une ancienne URL qu'on honore), les options expérimentales révèlent le niveau
de risque que l'équipe accepte. C'est par lui, et par proxy.ts s'il existe, qu'on entre
dans un dépôt inconnu ; la méthode complète est dans le module
Lire et critiquer du code frontend.
Dans le module sur la navigation d'Halterofit
(../halterofit/navigation.html), tu as vu la garde
d'authentification : un _layout.tsx qui retourne <Redirect /> si
l'utilisateur n'est pas connecté, et qui protège d'un coup tout son sous-arbre d'écrans.
proxy.ts joue une partition voisine côté web — un point de passage unique, en amont,
qui laisse entrer ou redirige — mais à un étage bien plus bas : il s'exécute
sur le serveur, avant même que le moindre composant ne soit rendu, alors que la
garde d'Expo Router s'exécute dans l'app déjà lancée.
La leçon transversale est la même dans les deux cas : un point d'entrée unique qui autorise ou détourne vaut mieux que dix vérifications éparpillées. Seul l'étage change.
Le dossier app/ : les dossiers font les URL, les fichiers font le comportement
Voici la règle centrale, et si tu ne devais retenir qu'une phrase de ce module, ce serait
celle-ci : dans app/, les DOSSIERS définissent les URL, et les NOMS DE FICHIERS
définissent le comportement. Deux axes indépendants, qu'il faut arriver à voir séparément.
Premier axe, les dossiers. Chaque dossier imbriqué sous app/ ajoute un
segment à l'URL : app/rendez-vous/confirmation/ correspond à
/rendez-vous/confirmation. Tu retrouves là exactement le routage par fichiers d'Expo
Router — c'est Next.js qui a popularisé l'idée côté web, et Expo Router qui l'a portée sur mobile.
Deuxième axe, les fichiers : un dossier tout seul ne produit rien, ce sont des fichiers aux
noms réservés qui lui donnent un comportement. Voici les neuf que tu dois savoir
reconnaître — l'alphabet du framework.
| Fichier | Rôle, en une ligne |
|---|---|
layout | Enveloppe partagée par le segment et tout ce qu'il contient ; elle persiste entre les navigations internes (l'état d'une barre latérale n'est pas perdu). |
page | Le contenu propre à cette URL. Sa présence rend la route publiquement accessible. |
loading | L'écran d'attente affiché automatiquement pendant que la page charge ; c'est un raccourci vers une frontière Suspense. |
error | Le filet de sécurité : si un composant du segment lève une erreur, c'est ce composant qui s'affiche à la place, sans faire tomber le reste du site. |
global-error | Le filet de dernier recours, à la racine : il remplace toute la page, y compris le layout racine, quand c'est le layout racine lui-même qui a échoué. |
not-found | La page 404 du segment — affichée quand aucune route ne correspond, ou quand le code la demande explicitement. |
route | Un point d'accès HTTP (une API) au lieu d'une page HTML : on y exporte des fonctions GET, POST… Il remplace page dans le même dossier — jamais les deux. |
template | Comme layout, mais recréé à chaque navigation au lieu d'être conservé : utile quand on veut relancer une animation d'entrée ou remettre un état à zéro à chaque visite. |
default | Le contenu de repli d'un « slot » de routes parallèles quand Next.js ne sait pas quoi y afficher (typiquement après un rechargement complet). |
Ces noms s'écrivent avec l'extension de ton choix : .tsx pour tout ce qui rend du JSX,
.ts pour un route, qui ne rend que des données.
La conséquence la plus utile : la colocalisation
La conséquence pratique est énorme. Puisqu'un segment n'est public que s'il contient un
page ou un route, tous les autres fichiers que tu poses dans
app/ sont… simplement des fichiers : personne ne peut « visiter »
app/rendez-vous/creneaux.ts, c'est un module que ton code importe, point.
Cela autorise la colocalisation : ranger composants, utilitaires, tests et types
à côté de la page qui les utilise plutôt que dans un lointain components/
global. Confort de lecture considérable — tu ouvres un dossier de fonctionnalité et tu vois tout ce
qui la concerne ; tu supprimes la fonctionnalité, tu supprimes un dossier, et rien ne traîne.
Deux conventions complètent le tableau — le module Routing les détaille :
-
Les groupes
(nom)— un dossier entre parenthèses organise sans apparaître dans l'URL :app/(marketing)/tarifs/page.tsxrépond à/tarifs, pas à/marketing/tarifs. Intérêt : unlayoutcommun sans polluer les adresses. -
Les dossiers privés
_nom— le tiret bas exclut totalement le dossier et tous ses enfants du routage.app/rendez-vous/_components/ne pourra jamais, même par accident, devenir une URL.
Pour les URL variables, la syntaxe reprend celle que tu connais : [slug] capture
un segment ; [...slug] (« catch-all ») en capture
plusieurs d'un coup, donc /a/b/c ; [[...slug]] accepte en
plus aucun segment, ce qui lui fait couvrir le chemin parent. Restent les
slots @nom (routes parallèles : plusieurs pages côte à côte dans un
même layout) et l'interception (.), (..),
(...), qui ouvre une route en modale par-dessus la page courante tout en changeant
l'URL. Tout cela est détaillé dans le module
Routing : segments, layouts, navigation, dont la table de conversion
Expo Router ↔ App Router transpose ligne à ligne les conventions que tu connais déjà.
Voici l'arborescence app/ d'un projet. Question : liste toutes les
URL réellement accessibles, et dis pour chaque fichier restant pourquoi il n'en produit aucune.
app/
├── layout.tsx
├── page.tsx
├── (marketing)/
│ ├── layout.tsx
│ ├── a-propos/page.tsx
│ └── tarifs/page.tsx
├── rendez-vous/
│ ├── page.tsx
│ ├── slots.ts
│ ├── _components/CarteCreneau.tsx
│ └── [service]/
│ ├── page.tsx
│ └── loading.tsx
├── admin/
│ └── stats.tsx
└── api/
└── services/route.ts
Voir le corrigé
Les URL accessibles :
/— grâce àapp/page.tsx./a-proposet/tarifs— le groupe(marketing)n'apparaît pas dans l'adresse ; il ne sert qu'à donner à ces deux pages lelayout.tsxcommun placé à côté d'elles./rendez-vous— grâce àrendez-vous/page.tsx./rendez-vous/<service>— une infinité d'adresses en réalité :/rendez-vous/physio,/rendez-vous/dentiste… Le dossier[service]capture un segment variable./api/services— unroute.tsrend le segment public tout autant qu'unpage.tsx, mais il répond des données (du JSON, en général) plutôt qu'une page HTML.
Les fichiers qui ne produisent aucune URL :
layout.tsx(les deux) etloading.tsx: des fichiers de comportement. Ils habillent une route, ils n'en créent pas.rendez-vous/slots.ts: un simple module colocalisé près de la page qui l'importe. Rien ne l'expose au réseau._components/CarteCreneau.tsx: le tiret bas rend le dossier privé, donc exclu du routage — mais même sans lui, ce fichier ne serait pas une route, faute de s'appelerpage.admin/stats.tsx: le piège. Le dossier et le fichier existent, et pourtant/admincomme/admin/statsrenvoient un 404 : il n'y a pas depage.tsx, et le nomstats.tsxn'a aucune signification pour le routeur. C'est le réflexe d'Expo Router qui trahit ici — là-bas, ce fichier aurait donné/admin/stats.
Le dossier app/tableau-de-bord/ contient un fichier Widget.tsx et rien d'autre. Que renvoie l'adresse /tableau-de-bord ?
Un 404. Un segment n'est public que s'il contient un page ou un route ; Widget.tsx n'est qu'un module colocalisé. C'est précisément cette règle qui rend la colocalisation possible : on peut ranger composants, utilitaires et tests dans app/ sans risquer d'exposer quoi que ce soit.
La page minimale et le layout racine
Le plus petit site Next.js qui fonctionne tient en deux fichiers, et ces deux fichiers sont la formule de ce module : tout le reste du framework n'en est qu'une variation.
Le premier est le layout racine, app/layout.tsx :
obligatoire — le seul fichier dont Next.js exige l'existence — avec une
particularité qui surprend toujours au début, c'est lui qui écrit les balises
<html> et <body>. Il n'y a pas d'index.html dans
un projet Next.js : le document n'existe nulle part sur le disque, il est produit par ce
composant, à chaque requête ou au moment du build. Quant à la prop children
qu'il reçoit, c'est le contenu du segment enfant — la page, ou un layout plus profond. Tu reconnais
le patron de composition que tu utilises depuis toujours en React : un composant enveloppe qui rend
{children} au milieu de sa propre structure. Ici, la « structure » se trouve juste être
le squelette du document.
Le duo layout + page
// app/layout.tsx — OBLIGATOIRE, une seule fois, à la racine.
// C'est lui qui écrit <html> et <body> : il n'y a pas d'index.html.
export default function RootLayout({
children, // ← le segment enfant : la page, ou un layout plus profond
}: { children: React.ReactNode }) {
return (
<html lang="fr">
<body>{children}</body>
</html>
);
}
// app/page.tsx — la page de l'URL « / ». Export DEFAULT obligatoire.
export default function Page() {
return <h1>Bienvenue chez RendezVous</h1>;
}
Le layout enveloppe, la page occupe. Chaque segment de l'App Router est une
variation de ce duo : un layout optionnel qui persiste, une page qui
change. Tu la reconnaîtras à ses deux marques : l'export default et le paramètre
children du layout.
Deux détails de cette formule, sources d'erreurs très fréquentes chez les débutants.
L'export doit être default : Next.js va chercher l'export par défaut
de page.tsx pour savoir quel composant rendre, et un export function Page()
nommé, sans default, produit une erreur au build. Le nom de la fonction, lui,
n'a aucune importance. Et le layout racine ne se remonte pas : comme il contient
<html> et <body>, tout ce que tu y mets — barre de navigation,
pied de page, police, thème — s'applique à absolument toutes les pages du site, et les layouts plus
profonds (app/(marketing)/layout.tsx) ne redéclarent pas ces balises : ils
s'insèrent simplement à l'intérieur, comme des poupées russes.
Et regarde ce que ce layout racine produit, une fois rendu : <html lang="fr">,
un <body>, du contenu dedans. C'est La page web minimale du module
Du mobile au web : lire ce guide, à peine déguisée — le
squelette n'a pas disparu, il a changé de forme : il s'écrit maintenant en JSX, dans un
composant React, au lieu d'un fichier index.html. Le <head> non
plus n'a pas disparu ; simplement, tu ne le tapes plus toi-même : Next.js l'assemble à partir de
l'objet metadata que tu exportes.
C'est aussi pour cela qu'ouvrir app/layout.tsx est le premier geste devant un dépôt
inconnu : en trente secondes tu y vois la langue, les polices, le thème, l'en-tête et le pied de
page, et surtout les fournisseurs (providers) qui enveloppent l'application — un thème, une
session, un client de données. Ces fournisseurs te disent quelles grandes technologies sont en jeu
avant même que tu aies lu le package.json.
createRoot que tu n'écris plus
Dans le module React web ↔ React Native (react-web.html), tu as vu le
point d'entrée d'une application React côté navigateur : un index.html avec un
<div id="root"></div> vide, et un fichier JavaScript qui appelle
createRoot(document.getElementById('root')).render(<App />). C'est le geste qui
branche React sur le DOM.
Dans un projet Next.js, ce fichier de démarrage n'existe pas, et ce n'est pas un oubli : le framework l'écrit pour toi. Le layout racine décrit le document, Next.js s'occupe de le rendre en HTML sur le serveur puis de rebrancher React dessus dans le navigateur (ce qu'on appelle l'hydratation). C'est très exactement l'idée du « framework qui t'appelle » : tu ne fournis plus le point d'entrée, tu remplis une case convenue.
Toutes les pages du site affichent l'en-tête et le pied de page… et rien d'autre : le contenu propre à chaque page a disparu. Aucune erreur, aucun 404, les adresses sont les bonnes. Où est le problème ?
// app/layout.tsx
import { EnTete } from '@/components/EnTete';
import { PiedDePage } from '@/components/PiedDePage';
export default function RootLayout({
children,
}: { children: React.ReactNode }) {
return (
<html lang="fr">
<body>
<EnTete />
<PiedDePage />
</body>
</html>
);
}
children est bien déclaré… et jamais rendu. Le segment enfant — la page, ou un layout plus profond — arrive dans cette prop ; si le layout ne le place nulle part dans son JSX, sa sortie ne contient tout simplement pas la page. D'où l'absence de symptôme franc : la route a bien été trouvée, le composant de page a bien été exécuté sur le serveur, c'est son résultat qui est jeté. Correctif : <main>{children}</main> entre l'en-tête et le pied de page. Rien de propre à Next.js là-dedans, d'ailleurs : c'est le composant-enveloppe React qui oublie de rendre son contenu — sauf qu'ici l'enveloppe est le document tout entier.
Le squelette de RendezVous
Posons l'application qui va nous accompagner tout au long de ce guide. RendezVous est un site de prise de rendez-vous pour une clinique : on présente les services offerts (physiothérapie, dentiste, massothérapie…), on en choisit un, on voit les créneaux libres, on réserve. C'est petit, mais assez réel pour rencontrer tous les sujets du guide — données à charger, formulaire à valider, API à appeler, états de chargement et d'erreur, référencement à soigner. À ce stade, ne cherche pas à comprendre comment chaque pièce fonctionne : il s'agit seulement de rendre ce dessin familier, pour que les modules suivants aient un territoire où se poser.
app/
├── layout.tsx ← <html>/<body>, en-tête et pied de page du site entier
├── page.tsx → / (accueil : la liste des services)
├── loading.tsx ← squelette affiché pendant le chargement de l'accueil
├── not-found.tsx ← la page 404 du site
│
├── (legal)/ ← groupe : n'apparaît PAS dans les URL
│ ├── layout.tsx ← une mise en page « document » commune aux deux pages
│ ├── confidentialite/page.tsx → /confidentialite
│ └── conditions/page.tsx → /conditions
│
├── rendez-vous/
│ ├── page.tsx → /rendez-vous (choisir un service)
│ └── [service]/ ← segment dynamique
│ ├── page.tsx → /rendez-vous/physio, /rendez-vous/dentiste…
│ ├── loading.tsx ← attente pendant le chargement des créneaux
│ ├── error.tsx ← filet si le chargement des créneaux échoue
│ └── _components/ ← dossier PRIVÉ : jamais une URL
│ ├── SlotGrid.tsx
│ └── BookingForm.tsx
│
└── api/
└── services/route.ts → /api/services (répond du JSON, pas du HTML)
components/ ← composants réutilisés PARTOUT (Button, Field, Badge…)
lib/ ← logique sans interface : accès aux données, validation, dates
public/ ← logo.svg, favicon, images des services
Lis cette carte avec la règle du module : les dossiers font les URL (colonne de droite), les noms de
fichiers font le comportement. (legal) et _components ne produisent aucune
adresse.
Pourquoi _components/ sous rendez-vous/[service]/, mais aussi un
components/ à la racine ? Parce que ce ne sont pas les mêmes composants :
SlotGrid n'a de sens que sur la page d'un service, le Button sert
partout. Règle de pouce : garde une chose aussi près que possible de son utilisation, et
remonte-la quand un deuxième — mieux, un troisième — appelant apparaît.
Et lib/ ? La logique sans interface : chercher les créneaux, valider
un formulaire, formater une date. La séparer des composants a un bénéfice concret que tu connais
depuis Halterofit — une fonction pure se teste sans rendre le moindre composant —
et on y reviendra dans le module sur les tests du web.
Un collègue ajoute ces deux fichiers au projet RendezVous. Pour chacun : cette route est-elle publique ? Si oui, à quelle URL ? Si non, pourquoi ?
1. app/equipe/page.tsx
2. app/equipe/EquipeGrille.tsx
Voir le corrigé
1. Publique → /equipe. Le dossier equipe/ ajoute un
segment, et le page.tsx le rend accessible. C'est la combinaison minimale.
2. Non publique. Le fichier est bien dans app/, mais il ne
s'appelle ni page ni route : il n'a donc aucune adresse. C'est un
composant colocalisé, importé par la page voisine. Personne ne peut « visiter »
/equipe/EquipeGrille.
Les scripts et le cycle de vie du projet
Un projet Next.js s'utilise avec trois commandes, déclarées dans les scripts du
package.json. Elles correspondent à trois moments distincts de la vie du code, et
confondre ces moments est une source classique de confusion.
| Commande | Quand | Ce qu'elle fait |
|---|---|---|
next dev |
Pendant que tu développes | Lance un serveur local qui recompile à chaque sauvegarde et rafraîchit le navigateur presque instantanément. C'est Turbopack, le bundler par défaut de Next.js, qui fait ce travail. |
next build |
Une fois, avant de déployer | Compile tout le projet en une version optimisée, et pré-rend ce qui peut l'être (détaillé juste sous ce tableau). Échoue si TypeScript ou une page rencontre une erreur : c'est ton filet de sécurité. |
next start |
En production | Démarre le serveur qui sert le résultat du build. Ne compile rien : sans build préalable, il refuse de partir. |
Arrêtons-nous sur build, parce que le mot est trompeur quand on vient du mobile. Chez
Expo, « build » veut dire compiler : produire un binaire installable. Ici, il recouvre
deux opérations. La première est bien la compilation — TypeScript et JSX en
JavaScript que les navigateurs comprennent, découpage en morceaux chargés à la demande,
minification, feuilles de style. La seconde est le pré-rendu, la vraie
particularité d'un framework web : pour chaque page qui n'a pas besoin d'informations propres à un
visiteur, Next.js exécute ton composant tout de suite, pendant le build, et
enregistre le HTML sur le disque. À l'exécution il n'y a plus rien à calculer, d'où des pages quasi
instantanées et parfaitement lisibles par les moteurs de recherche.
D'où le fait qu'un build échoue parfois sur une erreur qui, en apparence, ne se produit
« qu'à l'exécution » : cette exécution a lieu maintenant. Quelles pages sont pré-rendues et
selon quels critères, c'est le sujet du module Rendu & data fetching.
Les variables d'environnement, et la ligne rouge du secret
Un site a besoin de valeurs qui changent selon l'endroit où il tourne : adresse de la base de
données, clé d'API, URL publique du site. Elles n'ont rien à faire dans le code — on les met dans un
.env.local, qui reste sur ta machine et n'est jamais commité (le
.gitignore par défaut s'en charge) ; en production, l'hébergeur offre son propre écran
pour les saisir.
# .env.local — jamais commité, jamais partagé
# Reste sur le SERVEUR : jamais envoyée au navigateur.
CLE_API_CLINIQUE=sk_live_9f3b2c...
# Le préfixe NEXT_PUBLIC_ = « recopie-moi dans le JavaScript du navigateur ».
# À réserver aux valeurs QUE TOUT LE MONDE PEUT VOIR.
NEXT_PUBLIC_URL_SITE=https://rendezvous.example.com
Deux variables, deux destins radicalement différents — et la seule chose qui les sépare est un préfixe de douze caractères.
La règle est simple au point d'être facile à sous-estimer.
Par défaut, une variable d'environnement reste sur le serveur : ton code serveur
lit process.env.CLE_API_CLINIQUE, mais cette chaîne n'est écrite nulle part dans ce qui
part vers le navigateur. C'est le comportement sûr, et c'est celui par défaut.
Le préfixe NEXT_PUBLIC_ renverse ce comportement. Il dit à Next.js :
« recopie littéralement cette valeur dans le JavaScript envoyé au navigateur ». Le mot important est
littéralement — au build, chaque occurrence de
process.env.NEXT_PUBLIC_URL_SITE est remplacée par la chaîne elle-même dans le fichier
livré, que n'importe qui peut lire dans les outils de développement de son navigateur. Ce n'est pas
une faille, c'est le but déclaré du préfixe ; la faille, c'est de le mettre devant quelque chose qui
devait rester secret. Conséquence souvent découverte à la dure : puisque la substitution a lieu
au build, changer une variable NEXT_PUBLIC_ chez ton hébergeur ne
suffit pas — il faut reconstruire. Les variables serveur, elles, sont lues à
l'exécution et prennent effet au redémarrage.
Croire qu'un dossier dans app/ crée une URL. C'est le réflexe hérité
d'Expo Router, où poser settings.tsx suffit à créer l'écran. Ici, non : sans
page.tsx (ou route.ts), le dossier existe mais l'adresse renvoie un 404.
Quand une route « ne marche pas », premier réflexe : y a-t-il bien un page.tsx
dans ce dossier, et est-il bien orthographié ainsi ?
Coller NEXT_PUBLIC_ devant un secret pour faire taire une erreur.
Presque un rite de passage : ton code client lit process.env.CLE_API, la valeur
arrive undefined, une recherche rapide suggère d'ajouter le préfixe, ça marche… et ta
clé secrète est publiée dans le JavaScript de chaque visiteur. Le undefined n'était
pas un bug, c'était la protection qui fonctionnait. Le vrai correctif est de déplacer
l'appel côté serveur (composant serveur, route.ts, Server Action), pas
d'abaisser la barrière.
Tu corriges ces deux valeurs dans le tableau de bord de ton hébergeur, puis tu redémarres le serveur. Les appels à l'API partent bien avec la nouvelle clé ; les liens de partage, eux, pointent encore vers l'ancienne adresse. Pourquoi une seule des deux a-t-elle changé ?
# Variables d'environnement de production — modifiées il y a deux minutes
CLE_API_CLINIQUE=sk_live_NOUVELLE
NEXT_PUBLIC_URL_SITE=https://rendezvous.ca
Parce que les deux valeurs sont lues à des moments différents. CLE_API_CLINIQUE est lue à l'exécution, par du code serveur qui interroge process.env à chaque requête : redémarrer suffit. NEXT_PUBLIC_URL_SITE, elle, a été recopiée littéralement dans le JavaScript au moment du build : dans les fichiers servis aux visiteurs, process.env.NEXT_PUBLIC_URL_SITE n'existe plus, il a été remplacé par l'ancienne chaîne. Ces fichiers sont ceux du dernier build, et rien ne relit la variable. Correctif : reconstruire, puis redéployer. Retiens que le préfixe ne décide pas seulement qui peut voir la valeur, mais aussi à quel moment elle est figée.
Ce que Next.js apporte encore, en survol
Le routage et le rendu sont le cœur du framework, mais Next.js embarque d'autres outils, chacun réglant un problème classique du web. On ne fait ici que les nommer, pour que tu saches qu'ils existent et où les retrouver.
-
Les images (
next/image). Un composant<Image />qui remplace<img>et automatise ce que personne ne fait à la main : format moderne, redimensionnement selon l'écran, chargement paresseux hors de vue, et place réservée d'avance pour que la page ne « saute » pas quand l'image arrive — ce dernier point est mesuré par Google et compte dans le référencement. -
Les polices (
next/font). Téléchargées et servies depuis ton propre domaine plutôt que depuis un service tiers : plus rapide, meilleur pour la vie privée, et sans clignotement de texte au chargement. -
Les métadonnées et le SEO. Titre de l'onglet, description, image de partage sur
les réseaux sociaux : tout se déclare en exportant un objet
metadatadepuis unlayoutou unepage, et Next.js génère les balises dans le vrai HTML, celui que les robots lisent sans exécuter de JavaScript. Un site React classique leur renvoie une page vide ; un site Next.js, du contenu. -
Le streaming — le serveur envoie le HTML par morceaux au lieu d'attendre que
toute la page soit prête (fichiers
loading, frontièresSuspense) — et le cache, la partie la plus subtile du framework : Next.js peut cacher le résultat d'unfetch, le HTML d'une page, la navigation côté client, avec des règles précises pour le demander et l'invalider. Sujet du module Rendu & data fetching.
Dans l'App Router, les paramètres reçus par une page — params (les segments
dynamiques) et searchParams (ce qui suit le ? dans l'URL) — sont des
promesses, qu'on lit donc avec await :
// app/rendez-vous/[service]/page.tsx
export default async function Page({
params, // ← une PROMESSE, pas un objet simple
}: { params: Promise<{ service: string }> }) {
const { service } = await params; // ← d'où le await, et l'async ci-dessus
return <h1>Créneaux pour {service}</h1>;
}
Un tutoriel qui écrit params.service directement, sans await, date
d'avant ce changement : excellent indicateur de fraîcheur pour évaluer une ressource trouvée en
ligne. Le pourquoi est expliqué en détail dans le module Routing ; retiens seulement,
pour l'instant, qu'il faut le await.
Dans le navigateur, l'onglet affiche bien « Physiothérapie — RendezVous ». Mais quand la réceptionniste colle l'adresse dans un message, l'aperçu du lien montre « RendezVous » tout court, le titre du site. Pourquoi ?
// app/rendez-vous/[service]/page.tsx
'use client';
import { useEffect } from 'react';
export default function Page() {
useEffect(() => {
document.title = 'Physiothérapie — RendezVous';
}, []);
return <h1>Physiothérapie</h1>;
}
Parce que le robot qui fabrique l'aperçu n'exécute pas de JavaScript : il lit le HTML que le serveur a renvoyé, et s'arrête là. Or useEffect ne s'exécute que dans le navigateur, après l'affichage — trop tard pour le HTML initial, et jamais pour le robot. Ce dernier voit donc encore le <title> hérité du layout racine. La bonne façon : exporter un objet metadata depuis la page ou le layout (ou la fonction generateMetadata quand le titre dépend du segment dynamique), pour que Next.js écrive les balises dans le HTML avant qu'il ne quitte le serveur. Détail qui compte : metadata ne s'exporte que depuis un composant serveur, donc le 'use client' est ici doublement le problème.
« Pourquoi utiliser Next.js plutôt que React seul ? »
Parce que React ne résout qu'un problème : décrire l'interface en fonction de l'état. Tout le reste d'une application web réelle — le routage, le bundling, le rendu côté serveur pour le SEO, le chargement de données, le cache, l'optimisation des images — reste à assembler soi-même. Next.js fournit tout ça déjà câblé et cohérent.
Je le formulerais comme un compromis : on échange de la liberté contre de la convention. Sur un produit réel, c'est presque toujours gagnant, parce que le coût n'est pas d'écrire chaque brique mais de les faire tenir ensemble dans la durée. J'ajouterais que c'est exactement la relation qu'Expo entretient avec React Native, que j'utilise au quotidien : même couche, même rôle.
« App Router ou Pages Router — lequel utilises-tu, et comment les distingues-tu ? »
J'apprends et j'utilise l'App Router, parce que c'est la direction actuelle du framework et qu'il est bâti sur les Server Components. Le Pages Router est toujours supporté et très présent dans le code existant, donc je sais le lire, mais je ne démarrerais pas un projet neuf avec.
Pour les distinguer, je regarde trois choses. Le nom du dossier : app/ ou
pages/. Le nom des fichiers de page : dans l'App Router, c'est toujours
page.tsx dans un dossier, alors que le Pages Router accepte n'importe quel nom.
Et le chargement des données : si je vois getServerSideProps ou
getStaticProps, je suis dans le Pages Router ; si je vois des composants
async qui font leur await eux-mêmes, je suis dans l'App Router.
« Où mets-tu une clé d'API secrète dans un projet Next.js ? »
Dans un fichier .env.local, qui n'est pas commité, et en production dans les
variables d'environnement de l'hébergeur. Surtout : sans le préfixe
NEXT_PUBLIC_. Ce préfixe demande explicitement à Next.js de recopier la valeur dans
le JavaScript envoyé au navigateur — donc tout ce qui le porte est public par construction.
Ensuite, je ne lis cette clé que depuis du code serveur : un Server Component, un
route.ts, une Server Action. Si un composant client en a besoin, c'est le signe que
l'appel est au mauvais endroit — je le déplace côté serveur et le client parle à mon propre point
d'accès, qui garde la clé. Et je me méfie du réflexe d'ajouter NEXT_PUBLIC_ quand une
variable arrive undefined côté client : ce undefined est la protection
qui fait son travail.
React ne fait que l'interface ; Next.js fournit tout le reste (routage, bundler
Turbopack, rendu serveur, cache, images, métadonnées) — comme Expo au-dessus de React Native. On
n'apprend que l'App Router (app/, page.tsx, composants
async) ; du Pages Router (pages/, getServerSideProps), on
sait seulement le reconnaître.
La règle centrale : les dossiers font les URL, les noms de fichiers font le
comportement — layout, page, loading,
error, global-error, not-found, route,
template, default. Un segment n'est public que s'il contient un
page ou un route, ce qui autorise la colocalisation libre ; les
groupes (nom) organisent sans apparaître dans l'URL, les dossiers _nom
sortent du routage. La formule du module est le duo layout + page : le layout
racine est obligatoire et écrit <html>/<body>, la page
s'exporte en default. Côté outillage : next dev pour développer,
next build pour compiler et pré-rendre, next start pour servir ;
et une variable d'environnement reste serveur sauf si elle porte NEXT_PUBLIC_.
Et ailleurs : le routage par fichiers n'est pas une bizarrerie de Next.js,
c'est la norme du web moderne — Remix, SvelteKit, Nuxt, Astro et, tu le sais déjà, Expo Router.
Le vocabulaire change à la marge (+page.svelte, _layout.tsx,
page.tsx), l'idée est identique : l'arborescence EST la carte, et
savoir la lire survivra à Next.js. Deuxième idée transposable : la distinction
« ce qui reste sur le serveur » / « ce qui part chez le client » est le fondement
de toute la sécurité applicative, quel que soit le langage.
- Le test le plus rapide pour dater un dépôt Next.js
- un dossier
app/oupages/? Puis cherchegetServerSideProps: s'il apparaît, c'est l'ancien monde. Vaut aussi pour dater un article trouvé en ligne - Ce qui rend une URL accessible
- la présence d'un fichier
pageouroute— jamais le dossier seul. C'est ce qui autorise à ranger n'importe quoi d'autre dansapp/sans l'exposer - Le seul fichier qu'un projet est obligé d'avoir
app/layout.tsx— et c'est lui qui écrit<html>et<body>. Il n'y a pas d'index.htmlsur le disque- Ce que l'export d'un
page.tsxdoit être export default. Un export nommé fait échouer le build ; le nom de la fonction, lui, n'a aucune importance- Ce que
NEXT_PUBLIC_fait vraiment, et à quel moment - il recopie la valeur littéralement dans le JavaScript du navigateur, au build. Donc : n'importe qui peut la lire, et la changer chez l'hébergeur ne suffit pas — il faut reconstruire
layoutcontretemplate- le
layoutpersiste entre les navigations internes ; letemplateest recréé à chaque fois — pour relancer une animation ou remettre un état à zéro routecontrepagerouterend des données (on y exporteGET,POST…) et il remplacepagedans le même dossier : jamais les deux- La gradation des segments variables
[slug]capture un segment ;[...slug]plusieurs ;[[...slug]]plusieurs ou aucun, donc il couvre aussi le chemin parent- Ce qu'un dossier entre parenthèses change à l'URL
- rien —
(marketing)groupe pour partager un layout sans apparaître dans l'adresse. Le tiret bas, lui (_composants), exclut totalement du routage - Ce que
buildfait de plus que compiler - il pré-rend : il exécute tes composants tout de suite et écrit le HTML. D'où des erreurs « d'exécution » qui tombent au build — cette exécution a lieu maintenant