Blog
3 min read

Why Refreshing Your React Page Gives a 404 (and How to Fix It on Any Host)

Your single-page app works when you click around but shows 404 Not Found when you refresh or share a link. Why client-side routing causes it, and the exact fix for Nginx, Caddy, Netlify, Vercel, GitHub Pages and Express.

Your app works perfectly while you click around. Then you refresh /dashboard — or someone opens a link you shared — and get 404 Not Found. The home page still works. This affects nearly every React, Vue or Vite single-page app on its first deploy.

Why it happens

A single-page app (SPA) has one real HTML file: index.html. When you click a link inside the app, the router (React Router, for example) changes the URL and swaps the content in the browser. No new page is requested from the server.

But when you refresh /dashboard or open it directly, the browser asks the server for /dashboard. The server looks for a file or folder called dashboard, finds nothing, and returns 404. The router never gets a chance to run.

The fix, in one sentence

Configure the server to return index.html for any path that isn't a real file. The app loads, the router reads the URL, and shows the right page.

This is called a fallback or rewrite. How you set it up depends on where you host.

Nginx

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

"Try the exact file, then a folder, otherwise serve index.html." (More on Nginx: reverse proxies explained.)

Caddy

example.com {
  root * /srv/app/dist
  try_files {path} /index.html
  file_server
}

Netlify

Create public/_redirects (so it ends up in your build output):

/*    /index.html   200

The 200 makes it a rewrite (the URL stays the same) rather than a redirect.

Vercel

For a plain Vite/React app, add vercel.json:

{
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}

(Next.js apps don't need this — Next.js handles routes on the server.)

GitHub Pages

GitHub Pages has no rewrite setting. The common workarounds:

  • copy index.html to 404.html during your build, so GitHub's 404 page is your app; or
  • use HashRouter, so URLs look like /#/dashboard and the server only ever sees /.

Both work; neither is as clean as a real rewrite. If you outgrow it, move to a host that supports rewrites.

Express (Node.js)

If you serve the built app from your own Node server, add the fallback after your API routes and static files:

app.use(express.static("dist"));

app.get("/api/books", ...); // API routes first

app.get("*splat", (req, res) => {
  res.sendFile(path.resolve("dist", "index.html"));
});

(In Express 5 the catch-all is written *splat; in Express 4 it was "*".)

Don't break your API or real 404s

Two things to watch:

  1. API routes must come first. If the fallback catches /api/... too, your API calls get HTML back and fail with confusing JSON errors.
  2. Real "not found" pages. With a fallback, the server never returns 404 — /total-nonsense loads the app. Add a catch-all route in your router that shows a "Page not found" screen.

Also check the router's base path

If the app lives under a sub-path like /my-app/, both the build tool and the router need to know:

<BrowserRouter basename="/my-app">

and base: "/my-app/" in vite.config.ts. Otherwise you'll get 404s or a blank page after deploy.

The summary

  • SPAs have one real page; refreshing a deep URL asks the server for a file that doesn't exist.
  • Fix: serve index.html for any path that isn't a real file (a rewrite or fallback).
  • Keep API routes ahead of the fallback, and add a "not found" route in the app.
  • Set the base path in both the build tool and router when hosting under a sub-path.

EasySpawn serves your app through a reverse proxy on your own domain with automatic SSL — and Claude Code can configure the fallback, deploy, and refresh a deep link to check it works. See how it works or join the waitlist.

Related: Anatomy of a URL · HTTP Status Codes Explained · What Is Vite? · Static vs Dynamic Websites

Keep reading