Serve a single-page application with an Nginx HTML fallback

Use try_files for frontend routes while preserving real asset errors, API responses, and predictable caching.

Best practices
Illustration of server operations and request logs

For a single-page application using history-based routes, let real files resolve normally and fall back to index.html for frontend paths. Keep API and asset paths separate so an error does not accidentally return HTML with a successful status.

Start with the frontend fallback

server {
    listen 80;
    server_name example.com;
    root /var/www/myspa;
    index index.html;

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

A path such as /user/42 may exist only in the browser router. The fallback internally redirects that request to the HTML entry point, allowing JavaScript to choose the view. Existing files and directories are checked first, so this does not literally force every request to one file.

An existing directory without an index can still produce a directory-related error with the $uri/ check. If every non-file frontend path should use the SPA, omit that directory check as in the next example.

Separate assets, APIs, and the entry document

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

    location ^~ /assets/ {
        try_files $uri =404;
        add_header Cache-Control "public, max-age=2592000, immutable";
    }

    location ^~ /api/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location = /index.html {
        try_files $uri =404;
        add_header Cache-Control "no-cache";
    }

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

This preserves /api/ in the upstream URI because proxy_pass has no URI suffix. Add separate handling for /api without a slash if your application uses that endpoint.

Only use the long immutable asset policy for filenames that change when their contents change, such as hashed build artifacts. no-cache allows the entry document to be stored but requires validation before reuse; it does not mean “never store.” Use no-store if preventing storage is the actual requirement. Neither setting alone guarantees an immediate update in the presence of a service worker or independently configured CDN.

The exact entry-document location returns 404 if index.html is absent, preventing an endless fallback cycle. Publish the entry document and its referenced assets coherently so clients do not receive HTML pointing to missing files.

Verify the route boundaries

Test a real frontend deep link, an existing asset, a missing asset, and an API route. A missing asset should stay a 404; an API error should stay an API response rather than become the SPA shell.

DataKit log collection can help inspect asset failures around a release. Access logs cannot establish whether JavaScript ran successfully or whether the browser displayed a blank page; inspect browser errors separately.

FAQ

Why do deep links still fail?

Check the selected server and location, the root path, the deployed entry document, and worker permissions. Also confirm the browser router's base path matches the URL where the application is hosted.

Does hash routing need a fallback?

The fragment in /#/user/42 is not sent to the server, so that route reaches /. The server still needs to serve the entry page and assets correctly.

Why does my API client receive HTML?

Its request likely missed the API location or was redirected into the frontend handler. Compare the actual request path, including any deployment prefix, with the configured API boundary.

Reference: Nginx try_files.

Get a tailored plan

Contact us

Join the community

Scan with WeChat
to join the community

Try Guance

Start online and pay only for what you use.

Get started

Choose a Guance plan

Code hosting