STORM

SOFTWARE  DEVELOPER

SYSTEM  INITIALIZING

[ VIEW PORTFOLIO ]

01
Back to Blog
Deployment

Why Direct URLs Return 404 on a React SPA (And How to Fix It)

April 15, 2026 5 min read

You deployed your React app. The homepage loads fine. Navigation works perfectly. Then you share a link like /about or /privacy-policy with someone - and they get a 404.

This is one of the most common deployment mistakes with Single Page Applications. Here's exactly what's happening and how to fix it.


What's a SPA and Why Does This Happen?

A React app (or any SPA built with Vue, Angular, etc.) ships one single HTML file - index.html. Every "page" you see - home, about, contact - is not a real file on the server. They are virtual routes, rendered in the browser by JavaScript.

This creates two completely different types of navigation:

Clicking a link inside the app

JavaScript intercepts the click, swaps out the component, and updates the URL - all without touching the server. Everything works fine.

Typing a URL directly or hitting refresh

The browser sends a real HTTP request to the server asking for that path. The server looks for a file called /privacy-policy. It doesn't exist. So it returns 404 - before React even loads.

That's the core of the problem. The server doesn't know about your client-side routes.


Diagnosing It

A quick way to confirm this is the issue: if even /index.html returns 404, the server can't find your build files at all - which means your output directory is misconfigured on top of the routing problem.

Vite builds into a dist/ folder. If your hosting platform is looking somewhere else, nothing will be served.


The Fix

Tell the server: "For any URL that doesn't match a real file, serve index.html and let JavaScript handle it."

Here's how to do it on the most common platforms:

Digital Ocean App Platform

Add catchall_document to your app spec, and make sure output_dir matches your build tool's output folder (dist for Vite, build for Create React App):

yaml
static_sites:
  - name: my-app
    build_command: npm run build
    output_dir: dist
    catchall_document: index.html

Netlify

Create a _redirects file inside your public/ folder (so it gets copied into dist on build):

/*    /index.html   200

Vercel

Add a vercel.json to your project root:

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

Nginx

In your server block config:

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

Apache

In your .htaccess file:

apache
RewriteEngine On
RewriteRule ^ /index.html [L]

Why It Works in Development but Not Production

Vite's dev server (npm run dev) handles this automatically - it serves index.html for every unknown route out of the box. So the problem never shows up locally, and catches many developers off guard on first deployment.


Quick Checklist

  • Build output directory is correctly set (dist for Vite, build for CRA)
  • A catchall/fallback is configured to serve index.html for unknown routes
  • The _redirects or config file is inside public/ so it's included in the build output

This is a one-time configuration fix. Once it's in place, all your routes will work correctly - whether someone clicks a link, types a URL directly, or hits refresh.

Back to Blog
PLAYING