Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 1516 minIntermédiaire

Build et déploiement d'une application React (Vite)

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • construire la version de production et la tester avant de la publier ;
  • utiliser les variables d'environnement de Vite sans y mettre de secret ;
  • configurer la redirection vers index.html sur un serveur ou un hébergeur ;
  • servir le front et l'API sur le même domaine ;
  • vérifier chaque version avec l'intégration continue.

3 exercices corrigés1 schéma

Déployer une application React construite avec Vite, c'est lancer npm run build, qui produit dans dist/ des fichiers statiques (une page HTML, du JavaScript et du CSS optimisés), puis les publier sur n'importe quel serveur web ou hébergeur statique. Deux réglages font la différence entre une mise en ligne qui marche et une page blanche : les variables d'environnement fixées au moment du build, et la redirection de toutes les adresses vers index.html pour que le routeur fonctionne. Ce chapitre construit AppliFrais pour la production et la déploie de plusieurs façons.

Construire la version de production

bash
npm run build

Le script lance deux commandes : tsc -b vérifie les types de tout le projet (une erreur TypeScript arrête le build), puis vite build produit la version optimisée :

text
vite v8.3.5 building client environment for production...
✓ 163 modules transformed.
dist/index.html                   0.46 kB │ gzip:   0.29 kB
dist/assets/index-DG3HAu6L.css    0.27 kB │ gzip:   0.20 kB
dist/assets/index-CGeL-Yyg.js   360.58 kB │ gzip: 112.55 kB
✓ built in 349ms

Ce que fait le build :

  • regroupe tous les modules (ton code, React, React Router, TanStack Query) en quelques fichiers ;
  • retire ce qui ne sert pas (le code mort, les types TypeScript, les avertissements de développement de React) ;
  • minifie le code (noms courts, espaces retirés) ;
  • ajoute une empreinte au nom de chaque fichier (index-CGeL-Yyg.js) : elle change dès que le contenu change, ce qui permet de mettre ces fichiers en cache pour un an sans risque de servir une ancienne version.

Les 360 ko de JavaScript (112 ko compressés) sont surtout React et les bibliothèques : c'est le coût de départ d'une SPA, à surveiller. Pour tester le résultat avant de le publier :

bash
npm run preview    # sert dist/ sur http://localhost:4173

Les variables d'environnement

L'adresse de l'API, le nom de l'environnement ou une clé publique de service changent entre le développement et la production. Vite lit des fichiers .env à la racine du projet :

FichierChargé
.envToujours
.env.localToujours, ignoré par Git (pour tes réglages personnels)
.env.developmentAvec npm run dev
.env.productionAvec npm run build
bash
# .env.production
VITE_API_URL=/v1
VITE_NOM_ENVIRONNEMENT=production

Dans le code, on les lit avec import.meta.env :

ts
const BASE = import.meta.env.VITE_API_URL ?? '/v1';

if (import.meta.env.DEV) console.log('Mode développement');
VariableValeur
import.meta.env.VITE_…Tes variables, toujours des chaînes
import.meta.env.MODE'development' ou 'production'
import.meta.env.DEV, .PRODDes booléens
import.meta.env.BASE_URLLe chemin de base de l'application (/ par défaut)

Pour que TypeScript connaisse tes variables, crée le fichier src/vite-env.d.ts (le modèle n'en fournit plus) :

ts
interface ImportMetaEnv {
  readonly VITE_API_URL?: string;
}

Deuxième conséquence : les valeurs sont figées au moment du build. Changer .env.production sur le serveur après coup ne change rien ; il faut reconstruire. C'est pour cela qu'une adresse d'API relative (/v1) est pratique : le même build fonctionne sur tous les serveurs qui relaient /v1 vers l'API.

Le problème des adresses profondes

Ton application est en ligne. La page d'accueil marche, tu cliques sur une fiche, l'adresse devient https://frais.gsb.example/fiches/12. Tu appuies sur F5… 404 Not Found.

Pourquoi : en cliquant, c'est React Router qui a changé l'adresse, dans le navigateur, sans requête. En rechargeant, le navigateur demande vraiment /fiches/12 au serveur, qui cherche un fichier fiches/12 dans dist/ et ne le trouve pas. Le serveur doit renvoyer index.html pour toute adresse qui ne correspond pas à un fichier : React se charge, React Router lit l'adresse et affiche la fiche. C'est la redirection de repli (SPA fallback), à configurer sur chaque hébergement.

Navigateurfrais.gsb.exampleHTTPSNginx/assets/index-CGeL-Yyg.jsun fichier existe dans dist/ :servi tel quel,en cache pour un an/fiches/12aucun fichier : index.htmltry_files … /index.htmlReact Router affiche la fiche/v1/fichesrelayé à l'API (proxy inverse)proxy_pass 127.0.0.1:3000avec l'en-tête Authorizationdist/index.html,assets/API Node.jsport 3000base de donnéesune seuleorigine :pas de CORS
Un seul domaine : les fichiers statiques, la redirection vers index.html et le proxy vers l'API

Servir le front et l'API sur le même domaine

La configuration recommandée pour AppliFrais reproduit en production le proxy de Vite : un serveur web (Nginx, Apache, Caddy) sert les fichiers de dist/ et relaie /v1/ vers l'API Node.js. Le navigateur ne voit qu'une origine : pas de CORS, et pas de requête de pré-vérification OPTIONS avant chaque appel qui porte l'en-tête Authorization (chapitre 12).

Avec Nginx :

nginx
server {
    listen 80;
    server_name frais.gsb.example;
    root /var/www/appli-frais/dist;
    index index.html;

    # L'API : ses routes commencent par /v1, relayées au serveur Node.js
    location /v1/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Les fichiers avec empreinte : en cache pour un an
    location /assets/ {
        add_header Cache-Control "public, max-age=31536000, immutable";
        try_files $uri =404;
    }

    # Tout le reste : le fichier s'il existe, sinon index.html (le routeur prend le relais)
    location / {
        add_header Cache-Control "no-cache";
        try_files $uri $uri/ /index.html;
    }
}
  • try_files $uri $uri/ /index.html : c'est la redirection de repli.
  • index.html est servi avec no-cache (le navigateur revérifie à chaque visite) : après un nouveau déploiement, il charge la nouvelle page, qui pointe vers les nouveaux fichiers à empreinte.
  • proxy_pass http://127.0.0.1:3000 sans chemin à la fin transmet l'adresse complète (/v1/fiches) à l'API. Derrière ce proxy, l'API active trust proxy (cours Node.js).
  • En production, ce serveur écoute en HTTPS (port 443) avec un certificat, par exemple de Let's Encrypt ; le port 80 ne sert qu'à rediriger vers HTTPS. Le jeton d'accès ne doit jamais circuler en clair : intercepté, il ouvrirait le compte jusqu'à son expiration.

Avec Apache, la redirection de repli tient en une ligne dans un fichier .htaccess placé dans dist/ (ou dans la configuration du site) : FallbackResource /index.html.

Les hébergeurs statiques

Si l'API est hébergée ailleurs, le front seul peut aller sur un hébergeur de sites statiques, souvent gratuit pour un projet personnel. Chacun a sa façon de déclarer la redirection de repli :

HébergeurRedirection de repliRemarque
NetlifyUn fichier public/_redirects contenant /* /index.html 200Vite copie public/ dans dist/
VercelUn fichier vercel.json : { "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }Détecte Vite automatiquement
Cloudflare PagesAutomatique pour une SPA sans fichier 404.html
GitHub PagesPas de redirection : copier index.html en 404.html après le buildLe code de retour reste 404 ; publication par une action (GitHub Pages)

Dans ce cas, l'API est sur une autre origine : elle doit autoriser celle du front avec CORS (la variable CORS_ORIGIN de l'API du cours Node.js), et le front doit fixer VITE_API_URL à l'adresse complète de l'API, par exemple https://api.gsb.example/v1 pour l'API déployée dans le cours Node.js. Comme le jeton voyage dans un en-tête et non dans un cookie, il n'y a rien d'autre à régler (CORS).

Déployer avec Docker

Pour livrer le front comme une image (le cours Docker explique les images et les conteneurs), on utilise une construction en plusieurs étapes : une image Node.js construit l'application, une image Nginx ne garde que le résultat.

dockerfile
# Étape 1 : construire
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Étape 2 : servir les fichiers statiques
FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html

Le fichier nginx.conf reprend la configuration ci-dessus, avec root /usr/share/nginx/html et proxy_pass http://api:3000, où api est le nom du service de l'API dans le fichier compose.yaml. L'image finale ne contient ni Node.js, ni node_modules, ni le code source : quelques dizaines de mégaoctets. Pense à un fichier .dockerignore qui exclut node_modules et dist.

Vérifier avant chaque déploiement : l'intégration continue

Un déploiement ne devrait jamais partir avec un test qui échoue ou une erreur de types. Une chaîne d'intégration continue le garantit : à chaque git push, elle installe les dépendances, lance le linter, les tests et le build (cours CI/CD).

yaml
# .github/workflows/front.yml
name: Front AppliFrais
on: [push, pull_request]

jobs:
  verifier:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npx vitest run
      - run: npm run build
      - uses: actions/upload-artifact@v7
        with:
          name: dist
          path: dist

npm ci installe exactement les versions du package-lock.json. L'artefact dist est le paquet prêt à déployer : l'étape de déploiement continu le récupère et le publie.

Les derniers réglages de production

PointPourquoiComment
HTTPS partoutLe jeton d'accès et le mot de passe ne doivent jamais circuler en clairCertificat sur le serveur web ou l'hébergeur
En-têtes de sécuritéUne CSP limite les dégâts d'une faille XSSContent-Security-Policy, X-Content-Type-Options sur le serveur web (la CSP)
Pas de source maps publiquesElles donnent ton code source lisibleValeur par défaut de Vite (build.sourcemap: false)
Poids du JavaScriptTemps de chargement sur mobile, écoconceptionSurveiller la taille affichée par le build ; charger les routes à la demande (chapitre 10) ; éviter les bibliothèques lourdes pour une seule fonction
Mises à jour des dépendancesUne faille dans une bibliothèque est une faille dans ton applicationnpm audit, Dependabot (chaîne d'approvisionnement)

Exercices

Exercice 1 sur 3 Diagnostiquer une mise en ligne Réussi À revoir

Pour chaque symptôme après déploiement, donne la cause la plus probable et la correction.

  1. L'accueil s'affiche, la navigation fonctionne, mais recharger la page /fiches/12 donne une erreur 404 du serveur.
  2. Page blanche ; la console affiche des erreurs 404 sur /assets/index-CGeL-Yyg.js. L'application est publiée sur https://exemple.github.io/appli-frais/.
  3. Les appels à l'API partent vers http://localhost:3000 depuis le site en ligne.
  4. Tu as modifié VITE_API_URL dans le fichier .env.production du serveur, mais l'application appelle toujours l'ancienne adresse.
Voir la réponseMasquer la réponse de l'exercice 1
  1. Pas de redirection de repli : le serveur cherche un fichier fiches/12. Ajouter try_files $uri $uri/ /index.html (Nginx), FallbackResource /index.html (Apache) ou la règle de l'hébergeur.
  2. L'application est dans un sous-dossier, mais elle cherche ses fichiers à la racine. Fixer base: '/appli-frais/' dans vite.config.ts (et le basename du routeur), puis reconstruire.
  3. L'adresse de l'API a été écrite en dur dans le code, ou fixée dans un .env de développement utilisé au build. Utiliser import.meta.env.VITE_API_URL, une adresse relative (/v1) ou la bonne valeur dans .env.production, puis reconstruire.
  4. Les variables VITE_ sont remplacées au moment du build : modifier le fichier après coup ne change rien. Il faut relancer npm run build et redéployer dist/.

Tu avais trouvé ?

Exercice 2 sur 3 Secret ou pas ? Réussi À revoir

Lesquelles de ces valeurs peuvent aller dans une variable VITE_ du front ? Pour les autres, dis où elles doivent aller.

  1. L'adresse publique de l'API.
  2. La clé secrète qui signe les JWT.
  3. Le mot de passe de la base MySQL.
  4. La clé publique d'un service de cartographie, restreinte au domaine du site par le fournisseur.
  5. Le nom de l'environnement (« recette », « production »), affiché dans un bandeau.
Voir la réponseMasquer la réponse de l'exercice 2
  1. Oui : elle est visible de toute façon dans l'onglet Réseau.
  2. Non : dans la configuration de l'API (variable d'environnement du serveur ou gestionnaire de secrets). Quiconque la connaît peut fabriquer des jetons valides.
  3. Non : seule l'API se connecte à la base ; le front ne doit même pas savoir qu'elle existe.
  4. Oui : une clé conçue pour être publique, et limitée par le fournisseur à ton domaine. C'est le cas des clés « navigateur » de nombreux services.
  5. Oui.

Tu avais trouvé ?

Exercice 3 sur 3 Écrire la configuration Réussi À revoir

Le front d'AppliFrais est hébergé sur Netlify, l'API sur https://api.gsb.example. Écris le contenu de .env.production, le fichier de redirection de repli, et liste ce que l'API doit configurer.

Voir la réponseMasquer la réponse de l'exercice 3
bash
# .env.production
VITE_API_URL=https://api.gsb.example/v1
text
# public/_redirects (copié dans dist/ au build)
/*  /index.html  200

Côté API : autoriser l'origine du front avec CORS. Dans l'API du cours Node.js, c'est la variable CORS_ORIGIN=https://appli-frais.netlify.app : le middleware cors renvoie alors Access-Control-Allow-Origin: https://appli-frais.netlify.app et répond aux requêtes de pré-vérification OPTIONS, que l'en-tête Authorization déclenche avant chaque appel. Rien d'autre : le jeton voyage dans un en-tête, pas dans un cookie, donc ni credentials: 'include', ni Access-Control-Allow-Credentials, ni cookie tiers bloqué par le navigateur.

Avec une session par cookie, ce serait plus lourd : credentials: 'include' dans chaque fetch, Access-Control-Allow-Credentials: true, un cookie Secure avec SameSite=None qui oblige à une protection CSRF, et certains navigateurs (Safari par défaut) qui bloquent les cookies tiers (chapitre 12). Le jeton Bearer rend ce déploiement sur deux domaines simple ; le domaine unique reste le plus simple de tous.

Tu avais trouvé ?

Chapitre terminé !

Fais les 3 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : React à l'E6 : bonnes pratiques et exercices corrigésRevoir le sommaire du cours