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:
- An exact
=match wins outright. - Otherwise Nginx finds the longest prefix match and remembers it. If that prefix has
^~, it's used. - Otherwise it tries regex locations top to bottom; the first match wins.
- 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:
$uri— is there a file matching the path? Serve it.$uri/— is there a directory? Serve its index.- Otherwise, the last argument is the fallback: an internal redirect to
/index.html(or=404to 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;
}
}
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_filestries each path in order; the last argument is the fallback.- SPAs need
try_files $uri $uri/ /index.html; protect/api/with^~; mind theproxy_passslash.
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
How to Self-Host n8n: Docker, Postgres, HTTPS and Backups
A production-minded guide to self-hosting n8n: Docker Compose with Postgres instead of SQLite, the environment variables that matter (encryption key, webhook URL, timezone), HTTPS behind a reverse proxy, backups of both the database and the encryption key, updates, and when queue mode is worth it.
Caddy vs Nginx vs Traefik: Which Reverse Proxy Should You Use?
Three popular reverse proxies, three philosophies: Nginx is the fast, flexible standard; Caddy gives you automatic HTTPS with a two-line config; Traefik configures itself from Docker labels. How they compare on HTTPS, configuration, Docker and Kubernetes integration, performance and debugging — with the same site configured in each.