Why SPA Refresh Returns 404: Understanding Client-Side Routing
1. The Mystery of the Missing Page
You just finished building your amazing React/Vue application.
It works perfectly on localhost:3000. You can navigate from Home to About, from About to Contact. Everything is smooth.
Excited, you run npm run build, upload the files to your server (AWS S3, Nginx, or GitHub Pages), and share the link.
Your friend clicks the link, lands on the Homepage. "Cool!"
Then they navigate to /about. "Nice!"
Then, purely out of habit, they press Refresh (F5) or copy the URL example.com/about and open it in a new tab.
404 Not Found.
The screen goes white. The browser says the file does not exist.
You panic. "But it worked on my machine!"
Why does clicking a link work, but typing the same URL fail?
2. Server-Side Routing vs Client-Side Routing
To understand the fix, you must understand the architecture.
The Old Way (Multi-Page Application - MPA)
In traditional websites (like PHP, old HTML sites), the URL corresponds directly to a file on the server's hard drive.
- Request:
example.com/about.html
- Server: Looks for
about.html in the folder.
- Found? Send it. Not found? Send 404.
The New Way (Single-Page Application - SPA)
Modern frameworks like React, Vue, and Angular are Single-Page Applications.
There is literally only ONE HTML file: index.html.
The /about, /contact, /users/123 pages do not exist as files. They are virtual pages created by JavaScript.
- Initial Load: You enter
example.com. The server returns index.html. The React app starts.
- Navigation: You click "About". React intercepts the click. It prevents the browser from talking to the server. Instead, it uses the History API (
history.pushState) to change the URL bar to show /about and renders the About Component. The Server is oblivious to this change.
- Refresh: You press F5 while on
example.com/about. The browser makes a fresh network request to the server: "Give me the file located at /about".
- The Crash: The server looks for a file named
about or a folder named about. It finds nothing. It dutifully returns 404 Not Found.
3. The Solution: The Catch-All Fallback
The solution is to trick the server.
We need to tell the server:
"If a user asks for a file that doesn't exist, don't give them a 404 error. Instead, give them index.html."
When the browser receives index.html (even though it asked for /about), it loads your React app.
The React Router then wakes up, looks at the URL bar (/about), and says:
"Oh, the current URL is /about. I should render the About Page component immediately."
From the user's perspective, it looks like the page loaded correctly.
4. Configuration Guide for Popular Servers
Here are the copy-paste configurations for the most common hosting environments.
4.1. Nginx
If you manage your own VPS or Nginx container, edit your server block configuration (/etc/nginx/conf.d/default.conf usually).
server {
listen 80;
root /var/www/my-react-app;
index index.html;
location / {
# Check if file exists ($uri), if not check folder ($uri/),
# if neither, serve /index.html
try_files $uri $uri/ /index.html;
}
}
The try_files directive is standard for all SPAs.
4.2. Apache HTTP Server
If you are on a shared hosting (cPanel style) or using LAMP stack, create a .htaccess file in the root of your public folder.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
Translation: "If the requested filename is NOT a file (!-f) and NOT a directory (!-d), then serve index.html."
4.3. AWS S3 (Static Website Hosting)
If you host on an S3 Bucket directly:
- Go to Properties -> Static website hosting.
- Set Index document to
index.html.
- Set Error document to
index.html.
Wait, setting Error document to index.html? Yes.
When S3 can't find /about, it triggers an error. You tell it "On error, serve index.html". S3 serves the file with a 200 OK status (or sometimes 404 but with the content).
4.4. AWS CloudFront + S3
If you use CloudFront (CDN) in front of S3 (which you should for SSL), the S3 Error Document trick might not work perfectly.
- Go to your CloudFront Distribution.
- Click Error Pages tab.
- Click Create Custom Error Response.
- HTTP Error Code: 403 Forbidden (S3 returns 403 for missing objects mostly) or 404.
- Customize Error Response: Yes.
- Response Page Path:
/index.html.
- HTTP Response Code: 200 OK. (Crucial! Do not send 404 code to the browser).
4.5. Netlify
Create a file named _redirects inside your public (or build) folder.
/* /index.html 200
This rule tells Netlify: "For ANY path (/*), serve /index.html with a 200 status code."
4.6. Vercel
Create a vercel.json file in your root directory.
{
"rewrites": [
{ "source": "/(.*)", "destination": "/" }
]
}
5. Alternative: HashRouter
There is one way to avoid all this server configuration: Hash Routing.
Instead of using https://example.com/about, you use https://example.com/#/about.
How it works:
- The browser NEVER sends anything after the
# (the hash fragment) to the server.
- Request:
example.com
- Server sees:
example.com (ignoring #/about).
- Server returns:
index.html.
- JS loads, sees
#/about, renders About page.
It works on GitHub Pages (which doesn't support SPA rewrites natively easily) and file systems.
Downside:
- URLs look ugly.
- SEO issues: Search crawlers might ignore hash fragments.
- Anchors (
<a href="#section">) behave weirdly.
Use BrowserRouter (React) or WebHistory (Vue) whenever possible. Use HashRouter only when you absolutely cannot configure the server.
6. What about SSR (Next.js)?
If you use Server-Side Rendering (SSR) like Next.js or Nuxt.js, this problem usually doesn't exist.
Since there is a real server (Node.js) running, it receives the request for /about, renders the HTML on the fly, and sends it back.
From the browser's perspective, it looks just like a traditional file-based website.
However, if you use next export (Static Site Generation), you are back to square one and must use the Rewrite rules mentioned above (e.g., vercel.json or _redirects).
7. How to Debug 404 Errors
If you have applied the settings but still see 404 errors, check the following:
- Browser Cache: Clear your browser cache or test in Incognito mode. The 404 response might be cached.
- API Requests: Ensure your API requests (e.g.,
/api/users) are not being redirected to index.html by mistake. You might need to exclude /api/* from your rewrite rules.
- Base URL: If you often deploy to a subdirectory (e.g.,
example.com/myapp/), ensure your Router's base path is configured correctly in your code (basename="/myapp" in React Router).
- Check the Network Tab: Open Chrome DevTools -> Network. Look at the request for the page. Is it returning HTML or something else?
8. Summary
The "404 on Refresh" error is the initiation rite for every SPA developer.
It's a simple mismatch between the Client's routing logic and the Server's file-system logic.
- The Problem: Server looks for real files; App uses fake URLs.
- The Fix: Configure server to fallback to
index.html on 404.
- The Command:
try_files $uri /index.html (Nginx) or Redirect Rules.
Now that you know the secret, you never have to fear the refresh button again.
Happy deploying!