← Retour au blog
NginxSPAProductionAngularConfiguration

Nginx: Serving a SPA without 404 Errors in Production

A single-page application (SPA) relies on a client-side router that intercepts URL changes and updates the DOM without reloading. But Nginx, by default, treats every HTTP request as a request for a static file. When a user visits /dashboard/settings directly or refreshes the page, the server looks for a physical file at that path, doesn't find it, and returns a 404 error. This is the classic pitfall: your SPA works perfectly on localhost , but breaks the moment you deploy to production.

The Problem: Nginx Looks for Files, Not Routes

The difference between a traditional server and an SPA is fundamental. Apache or Nginx are accustomed to mapping HTTP requests to physical files: /api/users → /var/www/api/users.php , /images/logo.png → /var/www/images/logo.png . With an SPA, only one HTML file exists—usually index.html —which contains your compiled application (JavaScript bundle). All internal routes are handled by the router in the browser, not by the server. When Nginx receives a request for /products/123 , it searches for a folder or file named products/123 on disk, doesn't find it, and returns 404. Worse, if the user refreshes the page, they lose access to their content entirely, creating a frustrating experience and harming SEO (though modern crawlers execute JavaScript, the latency penalty remains significant).

The Solution: try_files with Fallback to index.html

The standard Nginx configuration for serving an SPA uses the try_files directive. It tests for a resource in this order: first the requested file, then the folder, then finally a default file. Here's the minimal working configuration:

server {
    listen 80;
    server_name example.com;
    root /var/www/dist;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

This directive means: "If the file $uri exists, serve it. Otherwise, try the folder $uri/ . Otherwise, serve /index.html without changing the browser's URL." That last step is crucial. Without it, the browser would see the URL change, which breaks SPA navigation. With try_files , the URL stays /products/123 , but the server silently sends index.html content. Your Angular, React, or Vue router receives this HTML, loads the JavaScript, and rebuilds the correct interface based on the current URL.

Real-World Use Case: Angular with Ionic

Consider an Ionic/Angular application with these routes: /home , /products , /products/:id , /settings . After ng build --prod or ionic build --prod , you get a dist/ folder containing index.html , compiled CSS and JS files, and assets. With the try_files configuration above, all scenarios work: visiting /products directly, refreshing the page, sharing a link to /products/42 , or navigating via the Angular router. No 404s. Every request falls back to index.html , which loads your JS bundle, parses the URL, initializes the right components, and renders the content.

Common Pitfalls and Solutions

A very frequent pitfall: forgetting to exclude actual API routes from the fallback to index.html . If your backend runs on the same server or is routed by Nginx (for example, /api/ points to an upstream), you must explicitly exclude those paths. For instance:

location / {
    try_files $uri $uri/ /index.html;
}
location /api/ {
    proxy_pass http://backend:3000;
}

Another pitfall: serving versioned static files (like main.abc123def.js ) with long cache headers, then redeploying without updating the hash. Nginx itself isn't responsible, but configure appropriate cache headers so browsers don't get stuck on an old version. Adding Cache-Control: no-cache to index.html and Cache-Control: immutable, max-age=31536000 to versioned assets solves this problem.

Production-Ready Optimizations

In production, a few simple tweaks boost robustness. Compress static files with gzip to reduce bandwidth: gzip on; gzip_types text/plain text/css application/json application/javascript; . Set aggressive caching on versioned static assets and minimal or no caching on index.html . Use add_header Cache-Control "public, max-age=31536000, immutable"; in a location ~ block for assets, and add_header Cache-Control "no-cache, no-store, must-revalidate"; for index.html . You can also add security headers like X-Frame-Options: SAMEORIGIN and X-Content-Type-Options: nosniff to prevent certain attacks.

Operational Conclusion

Serving an SPA without 404 errors on Nginx requires just one intelligent line of configuration: try_files $uri $uri/ /index.html; . This simple directive silently redirects all unknown routes to your main HTML file, freeing your client-side router to work as designed. Deploy this configuration, test page reloads on different routes, and verify your API calls remain intact. It's one of the most impactful and least invasive changes to stabilize an SPA in production. Once in place, you can focus on real optimization: compression, caching, and JavaScript bundle performance.

Développeur Angular & Mobile freelance — Strasbourg.

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