Single Page Applications (React, Vue, Angular) handle routing client-side in JavaScript — but the web server still needs specific configuration to support this correctly, or users will hit 404 errors on page refresh or direct URL access.
The Problem SPA Routing Configuration Solves
When a user navigates within a React/Vue app, JavaScript handles the URL change without a real page reload. But if they refresh the page or bookmark a URL like /dashboard/settings, the browser makes a real HTTP request for that exact path — which doesn't exist as an actual file on the server, since it's a client-side route.
The Solution: Fallback to index.html
server {
listen 80;
server_name myapp.example.com;
root /var/www/myapp/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
try_files attempts to serve the exact requested file, then a directory, and finally falls back to index.html — letting the client-side JavaScript router take over and render the correct view based on the URL.
Preventing the Fallback from Applying to Real Assets
Static assets should still 404 correctly if they genuinely don't exist, rather than silently serving index.html:
location /assets/ {
try_files $uri =404;
}
location / {
try_files $uri $uri/ /index.html;
}
Caching Static Assets with Long Expiration
location ~* \.(js|css|woff2|png|jpg|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
Modern build tools (Vite, Webpack, Create React App) generate content-hashed filenames, making aggressive caching safe — a new deployment produces new filenames automatically.
Never Cache index.html Aggressively
location = /index.html {
add_header Cache-Control "no-cache";
}
Since index.html references the current build's hashed asset filenames, it must always be fetched fresh, or users could end up loading a stale HTML file referencing assets that no longer exist after a deployment.
Handling API Requests Alongside the SPA
location /api/ {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
}
location / {
try_files $uri $uri/ /index.html;
}
Nginx matches the most specific location block first, so /api/ requests correctly route to your backend while everything else falls through to the SPA.
Setting Up Client-Side Routing for React Router
No special React-specific configuration is needed beyond the Nginx try_files fallback — React Router (and equivalent libraries for Vue/Angular) handles the rest once the correct HTML is served.
Deploying an SPA Build
npm run build
rsync -avz --delete ./dist/ deploy@YOUR_SERVER_IP:/var/www/myapp/dist/
Common Errors
404 on page refresh for any route except the homepage — the try_files fallback to index.html is missing; add it as shown above.
Stale content after deployment — verify index.html specifically isn't being cached aggressively, even if other assets are.
API requests returning the SPA's HTML instead of JSON — the API location block isn't matching correctly, or is defined after (rather than before) the catch-all SPA fallback in a way that causes precedence issues.
Best Practices
- Always use
try_fileswith fallback toindex.htmlfor any client-side-routed SPA - Cache hashed assets aggressively, but never
index.htmlitself - Place API proxy rules with sufficient specificity to avoid conflicting with the SPA fallback
Related Articles
- How to Deploy a Vue.js/React Single Page Application
- How to Configure Browser Caching and Cache-Busting for Static Assets
- Nginx as a Reverse Proxy for Node.js/Docker Apps
