Module 0 · Le modèle

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 ?

🔗 Pont — Expo est à React Native ce que Next.js est à 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.

💡 Bibliothèque ou framework ?

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.

IndicePages Router (ancien)App Router (actuel)
Dossier racine des routespages/app/
Nom du fichier d'une pagen'importe lequel : pages/about.tsxtoujours page.tsx : app/about/page.tsx
Emballage partagéun unique pages/_app.tsx + _document.tsxlayout.tsx, imbriquables à chaque niveau
Chargement des donnéesgetServerSideProps, getStaticProps, getStaticPaths — exportés à côté du composant, puis passés en propsawait directement dans le composant serveur
API HTTPpages/api/hello.tsapp/api/hello/route.ts
Modèle par défauttout est composant clienttout 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.

🧠 Quiz éclair

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.

🔗 Pont — la garde d'authentification, deux mondes

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.

FichierRôle, en une ligne
layoutEnveloppe 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).
pageLe contenu propre à cette URL. Sa présence rend la route publiquement accessible.
loadingL'écran d'attente affiché automatiquement pendant que la page charge ; c'est un raccourci vers une frontière Suspense.
errorLe 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-errorLe 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-foundLa page 404 du segment — affichée quand aucune route ne correspond, ou quand le code la demande explicitement.
routeUn 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.
templateComme 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.
defaultLe 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.tsx répond à /tarifs, pas à /marketing/tarifs. Intérêt : un layout commun 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à.

✍️ Exercice de lecture

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-propos et /tarifs — le groupe (marketing) n'apparaît pas dans l'adresse ; il ne sert qu'à donner à ces deux pages le layout.tsx commun 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 — un route.ts rend le segment public tout autant qu'un page.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) et loading.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'appeler page.
  • admin/stats.tsx : le piège. Le dossier et le fichier existent, et pourtant /admin comme /admin/stats renvoient un 404 : il n'y a pas de page.tsx, et le nom stats.tsx n'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.
🧠 Quiz éclair

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.

📖 La formule

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.

🔗 Pont — le 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.

🧠 Quiz éclair

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.

Arborescence du projet RendezVous
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.

📅 Dans RendezVous

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.

✍️ Exercice de lecture

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.

CommandeQuandCe 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.

⚠️ Piège fréquent

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.

🧠 Quiz éclair

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 metadata depuis un layout ou une page, 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ères Suspense) — et le cache, la partie la plus subtile du framework : Next.js peut cacher le résultat d'un fetch, 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.
🧭 Un détail d'API qui vaut mieux que dix débogages

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.

🧠 Quiz éclair

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.

🎤 En entrevue

« 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.

À retenir

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 comportementlayout, 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.

🗂️ L'aide-mémoire
Le test le plus rapide pour dater un dépôt Next.js
un dossier app/ ou pages/ ? Puis cherche getServerSideProps : 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 page ou routejamais le dossier seul. C'est ce qui autorise à ranger n'importe quoi d'autre dans app/ 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.html sur le disque
Ce que l'export d'un page.tsx doit ê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
layout contre template
le layout persiste entre les navigations internes ; le template est recréé à chaque fois — pour relancer une animation ou remettre un état à zéro
route contre page
route rend des données (on y exporte GET, POST…) et il remplace page dans 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 build fait 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