← Retour au blog
nginxsparoutingproductiondevops

Nginx et les SPA : au-delà du simple try_files, maîtriser le fallback routing en production

Le mythe du try_files unique persiste : beaucoup de développeurs croient qu'une seule directive try_files suffit pour router toutes les requêtes vers index.html. En réalité, cette approche naive crée des problèmes subtils en production. Lorsqu'une SPA (Single Page Application) est servie via Nginx, chaque requête vers une route inexistante au niveau du système de fichiers doit être interceptée et redirigée vers index.html, où le routeur client-side (Angular, React, Vue) prend le relais. Mais cette logique simple se complique rapidement avec les assets statiques, les API proxies, les redirections HTTPS, et surtout la stratégie de cache. Sans compréhension fine du comportement de Nginx, on se retrouve à debuguer des erreurs 404 sporadiques, des assets non servis, ou pire, des pages blanches en production.

Le problème fondamental : distinguer assets et routes

Le piège classique consiste à appliquer try_files à *tout* sans discrimination. Une SPA doit servir ses assets statiques (JS, CSS, images) directement depuis le disque, mais router les requêtes vers des URLs inexistantes vers index.html. Si vous ne faites pas cette distinction, deux scénarios catastrophiques surviennent : soit vous servez index.html quand une image manque (ce qui charge la SPA au lieu de retourner un 404 correct), soit vous cassez le fallback pour les vraies routes. La solution réside dans une configuration Nginx granulaire qui teste d'abord l'existence du fichier, puis des répertoires, et seulement ensuite bascule vers index.html. Voici une configuration robuste : location / { try_files $uri $uri/ /index.html; } fonctionne pour les routes, mais elle échoue pour les assets avec une extension inexistante. Une approche plus nuancée consiste à définir des blocs location spécifiques pour les types de fichiers statiques (js, css, png, svg, etc.) avec leurs propres règles de cache et fallback désactivé.

Imaginez une Angular app déployée en production. L'utilisateur navigue vers /dashboard/settings , une route valide gérée par le routeur. Nginx ne trouve pas ce chemin sur le disque, donc try_files bascule vers index.html, qui charge la SPA, et Angular Router affiche la bonne page. Parfait. Mais quelques secondes plus tard, un script de monitoring tente de charger /api/health (une route API). Si votre try_files est trop permissif, cette requête bascule aussi vers index.html, ce qui retourne du HTML au lieu du JSON. L'API échoue silencieusement. La solution : utiliser une structure de location qui exclut explicitement les chemins API. location ^~ /api/ { proxy_pass http://backend; } avant la location générique garantit que les requêtes API ne sont jamais interceptées par le fallback.

Cache, ETag et les déploiements qui cassent

Un deuxième piège, souvent oublié, concerne le cache des assets. Les fichiers statiques générés par le bundler Angular (main.xyz123.js) incluent un hash dans le nom : c'est excellent pour le cache-busting. Mais si vous ne configurez pas les headers de cache correctement, le navigateur de l'utilisateur continuera à charger une ancienne version après un déploiement. Inversement, index.html lui-même ne doit *jamais* être mis en cache, car c'est le point d'entrée qui charge la nouvelle SPA. Une configuration Nginx appropriée définit une stratégie de cache différente pour index.html (pas de cache) et les assets versionnés (cache agressif). Exemple : location ~* \.js$|css$|png$|jpg$ { expires 1y; add_header Cache-Control "public, immutable"; } pour les assets, et location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; } pour le point d'entrée.

Lors d'un déploiement, le serveur Nginx génère une nouvelle version d'index.html (ou le même fichier avec un contenu mis à jour). Si les en-têtes Cache-Control sont mal configurés, le navigateur de l'utilisateur continue à servir la version en cache, qui charge les anciens bundles JavaScript. L'utilisateur voit alors une application partiellement mise à jour ou complètement cassée. Pour l'éviter, assurez-vous que index.html n'a jamais d'en-têtes Expires ou Cache-Control permissifs. Les services workers compliquent la situation : si un service worker a mis en cache index.html, même Nginx ne peut pas forcer un refresh. D'où l'importance de configurer le service worker pour revalider index.html à chaque chargement.

Gestion des erreurs et fallback intelligent

Une SPA bien configurée retourne index.html pour les routes inconnues, ce qui permet au routeur Angular de gérer les 404 côté client. Mais Nginx doit aussi gérer les cas où index.html lui-même est absent (scénario rare mais catastrophique en production). Utilisez error_page 404 /index.html; pour s'assurer qu'en cas d'erreur réelle, le navigateur reçoit au moins la SPA plutôt qu'une page d'erreur générique. Cependant, cette directive change le code HTTP retourné au navigateur : sans configuration supplémentaire, Angular Router reçoit un 200 au lieu d'un 404, ce qui peut tromper les outils de monitoring. Pour conserver la sémantique HTTP correcte, utilisez try_files $uri $uri/ /index.html =404; : le =404 final indique que si le fallback échoue, retourner un vrai 404.

Configuration complète pour la production

Voici une configuration Nginx de référence pour une SPA Angular en production : server { listen 80; server_name example.com; root /var/www/app/dist/browser; # Assets versionnés : cache long terme location ~* \.(js|css|png|jpg|svg|woff2)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } # index.html : pas de cache location = /index.html { add_header Cache-Control "no-cache, no-store, must-revalidate"; } # API proxy location ^~ /api/ { proxy_pass http://backend:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # Fallback pour les routes SPA location / { try_files $uri $uri/ /index.html =404; } } . Cette configuration distingue clairement les assets (cache long terme), les requêtes API (proxy direct), et les routes SPA (fallback vers index.html). Elle évite de servir index.html pour les assets manquants, et préserve la sémantique HTTP.

Pièges courants et antipatterns

Le piège le plus dangereux consiste à activer le fallback pour *tous* les types de requêtes, y compris les HEAD et OPTIONS. Certains outils de monitoring ou des bots d'indexation envoient des requêtes HEAD vers les assets statiques. Si Nginx répond à une HEAD vers /api/health avec index.html, le monitoring croira que le serveur fonctionne, alors qu'il est peut-être cassé. Un autre antipattern : utiliser try_files sans tester d'abord l'existence du fichier. try_files $uri =404; suivi d'un fallback implicite via error_page est moins clair que d'écrire explicitement try_files $uri $uri/ /index.html =404; . Enfin, beaucoup de développeurs oublient que Nginx lit les fichiers depuis root , pas depuis le répertoire courant. Si votre dist Angular est en /var/www/app/dist/browser mais que vous avez oublié le root , Nginx cherchera les fichiers ailleurs, et *tout* basculera vers le fallback.

Conclusion

Servir une SPA sans 404 en production requiert bien plus qu'une ligne try_files. Il faut une stratégie claire : distinguer les assets des routes, configurer le cache correctement, proxifier les APIs, et tester le comportement réel avec des outils comme curl ou Postman. Testez aussi les scénarios de déploiement : déployer une nouvelle version sans vider le cache du navigateur, accéder à des routes inexistantes, vérifier que les assets versionnés se chargent bien. Une configuration Nginx bien pensée est invisible en production : l'utilisateur navigue sans friction, les déploiements sont transparents, et les erreurs HTTP sont sémantiquement correctes. Investir 30 minutes pour affiner cette configuration vous évite des heures de debugging en 3 du matin.

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️