Blog
4 min read

Nginx location Blocks and try_files Explained (With SPA and API Examples)

How Nginx chooses a location block — exact, prefix, regex and ^~ matches — and how try_files serves a file, a directory or a fallback. Working configs for single-page apps (fixing 404 on refresh), a frontend plus /api backend, static asset caching, and the trailing-slash proxy_pass trap.

Most Nginx confusion comes down to two things: which location block handles a request, and what try_files does. Get those right and you can fix "404 on refresh", route /api to a backend, and cache assets properly. (What Is Nginx?)

How Nginx picks a location

For each request, Nginx compares the URL path against your location blocks in this order:

Syntax Type Priority
location = /path Exact match Used immediately if it matches
location ^~ /path/ Prefix, "stop here" Longest such prefix wins and regexes are skipped
location ~ \.php$ / ~* Regex (~* = case-insensitive) First matching regex in file order
location /path/ Plain prefix Longest match, used only if no regex matched

In plain English:

  1. An exact = match wins outright.
  2. Otherwise Nginx finds the longest prefix match and remembers it. If that prefix has ^~, it's used.
  3. Otherwise it tries regex locations top to bottom; the first match wins.
  4. If no regex matches, the remembered longest prefix is used.

The surprise for most people: a regex like location ~* \.(js|css)$ can steal requests from a longer plain prefix like location /api/. Use ^~ on prefixes that must not be overridden.

What try_files does

try_files $uri $uri/ /index.html;

Nginx checks each argument in order:

  1. $uri — is there a file matching the path? Serve it.
  2. $uri/ — is there a directory? Serve its index.
  3. Otherwise, the last argument is the fallback: an internal redirect to /index.html (or =404 to return an error).

Only the last argument can be a fallback URI or status code.

Example 1: single-page app (fixes 404 on refresh)

React, Vue and Vite apps do routing in the browser. Refreshing /dashboard/settings asks the server for a file that doesn't exist → 404. Send every unknown path to index.html:

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

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

(404 on Page Refresh)

Example 2: frontend plus an API backend

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

    location ^~ /api/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

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

^~ guarantees /api/... always goes to the backend, even if you add regex locations later. Without a backend location, POST /api/... hits the static files and returns 405. (405 Method Not Allowed, Nginx Reverse Proxy Configuration)

Example 3: cache hashed assets forever

Build tools put a content hash in asset file names (index-3f9a1c.js), so they can be cached for a year. index.html must not be, or users get stale apps:

location ^~ /assets/ {
    try_files $uri =404;
    expires 1y;
    add_header Cache-Control "public, immutable";
}

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

=404 here means a missing asset returns a real 404 instead of falling back to index.html (which would serve HTML with a .js URL and break the page). (HTTP Caching Headers)

The proxy_pass trailing-slash trap

location /api/ {
    proxy_pass http://127.0.0.1:3000;    # /api/users → backend gets /api/users
}

location /api/ {
    proxy_pass http://127.0.0.1:3000/;   # /api/users → backend gets /users
}

With a URI part on proxy_pass (even just /), Nginx replaces the matched prefix. Decide whether your backend expects the /api prefix and choose accordingly.

Test before you reload

sudo nginx -t && sudo systemctl reload nginx

nginx -t catches syntax errors before they take your site down.

The summary

  • Exact = → longest prefix (with ^~ stopping there) → first regex → longest prefix.
  • try_files tries each path in order; the last argument is the fallback.
  • SPAs need try_files $uri $uri/ /index.html; protect /api/ with ^~; mind the proxy_pass slash.

EasySpawn configures routing, HTTPS and caching in front of your app automatically, and Claude Code can adjust the rules when your app needs something custom. See how it works or join the waitlist.

Related: Reverse Proxy Explained · SPA vs MPA · Deploy a React App · Caddy vs Nginx vs Traefik

Keep reading