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
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 :
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 :
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 :
| Fichier | Chargé |
|---|---|
.env | Toujours |
.env.local | Toujours, ignoré par Git (pour tes réglages personnels) |
.env.development | Avec npm run dev |
.env.production | Avec npm run build |
# .env.production
VITE_API_URL=/v1
VITE_NOM_ENVIRONNEMENT=production
Dans le code, on les lit avec import.meta.env :
const BASE = import.meta.env.VITE_API_URL ?? '/v1';
if (import.meta.env.DEV) console.log('Mode développement');
| Variable | Valeur |
|---|---|
import.meta.env.VITE_… | Tes variables, toujours des chaînes |
import.meta.env.MODE | 'development' ou 'production' |
import.meta.env.DEV, .PROD | Des booléens |
import.meta.env.BASE_URL | Le 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) :
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.
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 :
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.htmlest servi avecno-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:3000sans chemin à la fin transmet l'adresse complète (/v1/fiches) à l'API. Derrière ce proxy, l'API activetrust 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ébergeur | Redirection de repli | Remarque |
|---|---|---|
| Netlify | Un fichier public/_redirects contenant /* /index.html 200 | Vite copie public/ dans dist/ |
| Vercel | Un fichier vercel.json : { "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] } | Détecte Vite automatiquement |
| Cloudflare Pages | Automatique pour une SPA sans fichier 404.html | |
| GitHub Pages | Pas de redirection : copier index.html en 404.html après le build | Le 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.
# É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).
# .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
| Point | Pourquoi | Comment |
|---|---|---|
| HTTPS partout | Le jeton d'accès et le mot de passe ne doivent jamais circuler en clair | Certificat sur le serveur web ou l'hébergeur |
| En-têtes de sécurité | Une CSP limite les dégâts d'une faille XSS | Content-Security-Policy, X-Content-Type-Options sur le serveur web (la CSP) |
| Pas de source maps publiques | Elles donnent ton code source lisible | Valeur par défaut de Vite (build.sourcemap: false) |
| Poids du JavaScript | Temps de chargement sur mobile, écoconception | Surveiller 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épendances | Une faille dans une bibliothèque est une faille dans ton application | npm 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.
- L'accueil s'affiche, la navigation fonctionne, mais recharger la page
/fiches/12donne une erreur 404 du serveur. - Page blanche ; la console affiche des erreurs 404 sur
/assets/index-CGeL-Yyg.js. L'application est publiée surhttps://exemple.github.io/appli-frais/. - Les appels à l'API partent vers
http://localhost:3000depuis le site en ligne. - Tu as modifié
VITE_API_URLdans le fichier.env.productiondu serveur, mais l'application appelle toujours l'ancienne adresse.
Voir la réponseMasquer la réponse de l'exercice 1
- Pas de redirection de repli : le serveur cherche un fichier
fiches/12. Ajoutertry_files $uri $uri/ /index.html(Nginx),FallbackResource /index.html(Apache) ou la règle de l'hébergeur. - L'application est dans un sous-dossier, mais elle cherche ses fichiers à la racine. Fixer
base: '/appli-frais/'dansvite.config.ts(et lebasenamedu routeur), puis reconstruire. - L'adresse de l'API a été écrite en dur dans le code, ou fixée dans un
.envde développement utilisé au build. Utiliserimport.meta.env.VITE_API_URL, une adresse relative (/v1) ou la bonne valeur dans.env.production, puis reconstruire. - Les variables
VITE_sont remplacées au moment du build : modifier le fichier après coup ne change rien. Il faut relancernpm run buildet redéployerdist/.
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.
- L'adresse publique de l'API.
- La clé secrète qui signe les JWT.
- Le mot de passe de la base MySQL.
- La clé publique d'un service de cartographie, restreinte au domaine du site par le fournisseur.
- Le nom de l'environnement (« recette », « production »), affiché dans un bandeau.
Voir la réponseMasquer la réponse de l'exercice 2
- Oui : elle est visible de toute façon dans l'onglet Réseau.
- 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.
- Non : seule l'API se connecte à la base ; le front ne doit même pas savoir qu'elle existe.
- 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.
- 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
# .env.production
VITE_API_URL=https://api.gsb.example/v1
# 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é ?