7 Things I Check When a React App Works Locally but Breaks in Production
One of the most annoying frontend problems is this:
Your app works perfectly on localhost.
Then you deploy it.
And suddenly:
- API requests fail
- routes return 404
- environment variables are missing
- images disappear
- authentication redirects break
- everything works on your machine, but nowhere else
This happens often enough that I now go through roughly the same checklist every time.
Here are the things I usually check first.
1. Environment variables
Local development makes environment variables feel simple because .env files are always nearby.
Production is different.
A variable may:
- not exist on the server
- use a different name
- contain an outdated URL
- not be exposed to the browser
- have been changed without rebuilding the app
For example, in Vite:
VITE_API_URL=https://api.example.com
and:
const apiUrl = import.meta.env.VITE_API_URL;
If you accidentally use:
API_URL=https://api.example.com
the variable will not automatically be exposed to client-side code.
Next.js has a similar distinction:
NEXT_PUBLIC_API_URL=https://api.example.com
is available in the browser, while:
API_URL=https://api.example.com
is normally server-side only.
When something works locally but fails after deployment, I always inspect the actual production environment before changing application code.
2. Hardcoded localhost URLs
This one is extremely common.
During development, code slowly accumulates things like:
fetch("http://localhost:3001/api/users");
or:
const socket = new WebSocket("ws://localhost:4000");
Everything works locally because all services are running on the same machine.
In production, localhost means the user's own machine or the production server, depending on where the code executes.
Neither is usually what you want.
A better approach is to keep URLs in configuration:
const apiUrl = import.meta.env.VITE_API_URL;
fetch(`${apiUrl}/api/users`);
I also search the whole project for:
localhost
127.0.0.1
http://
before deploying.
It catches more issues than you might expect.
3. CORS
A request that succeeds locally can fail immediately when the frontend and backend are hosted on different origins.
Example:
Frontend:
https://app.example.com
API:
https://api.example.com
The backend needs to allow requests from the frontend origin.
A typical Express configuration might look like:
import cors from "cors";
app.use(
cors({
origin: "https://app.example.com",
credentials: true,
})
);
During debugging, I open the browser network panel and inspect the request directly.
If the browser says something like:
blocked by CORS policy
then changing React code usually will not fix the problem.
The fix belongs on the API side.
4. Client-side routing
Single-page applications often have routes like:
/
/dashboard
/settings
/users/123
Navigation inside the app works because React Router handles it.
But refreshing:
https://example.com/dashboard
may return:
404 Not Found
Why?
Because the web server tries to find an actual /dashboard file.
For a client-side SPA, the server usually needs to return index.html for unknown application routes.
For Nginx, that may look like:
location / {
try_files $uri $uri/ /index.html;
}
Platforms like Vercel, Netlify, Cloudflare Pages, and others have their own rewrite rules.
If navigation works but refreshing a route does not, routing configuration is one of the first things I check.
5. HTTP vs HTTPS
Browsers are much stricter once your site is served over HTTPS.
For example:
https://example.com
calling:
http://api.example.com
can be blocked as mixed content.
WebSockets have the same issue.
Instead of:
new WebSocket("ws://api.example.com");
you may need:
new WebSocket("wss://api.example.com");
A page can appear completely normal while some background functionality silently fails because the browser blocks insecure requests.
The console usually reveals this quickly.
6. File name case sensitivity
This bug can be surprisingly confusing.
On some local development systems, this may appear to work:
import Header from "./components/header";
even if the real file is:
Header.jsx
But a Linux production server may treat:
Header.jsx
and:
header.jsx
as different files.
The deployment build then fails, or the module cannot be resolved.
I try to keep import paths exactly aligned with filenames:
import Header from "./components/Header";
This problem is especially easy to introduce when renaming files only by changing capitalization.
Git may not always detect the change the way you expect.
One workaround is:
git mv Header.jsx HeaderTemp.jsx
git mv HeaderTemp.jsx header.jsx
7. Production build behavior
Development mode is not production mode.
React, Vite, Next.js, and bundlers can behave differently after optimization.
Before blaming the hosting provider, I build the application locally.
For Vite:
npm run build
npm run preview
For Next.js:
npm run build
npm start
This catches problems such as:
- missing imports
- invalid environment variables
- SSR-only errors
- browser-only APIs used on the server
- build-time data fetching failures
- TypeScript errors
- incorrect asset paths
If the production build already fails locally, deployment is not the real problem.
My debugging order
When a deployment behaves differently from localhost, I usually check things in this order:
1. Browser console
2. Network requests
3. Production environment variables
4. API URLs
5. CORS
6. Routing/rewrite rules
7. HTTPS / mixed content
8. Production build logs
This order saves me from randomly changing code.
The browser console and network panel alone often reveal the issue within a few minutes.
A small habit that helps
I try to avoid treating production configuration as something I think about only during deployment.
Instead, I keep local development reasonably close to production:
- use environment variables from the start
- avoid hardcoded URLs
- test production builds locally
- keep frontend and backend configuration separate
- use HTTPS-compatible URLs
- check routes with direct page loads
The closer the environments are, the fewer surprises appear later.
Final thought
When an app works locally but fails in production, the React code itself is often fine.
The problem is usually somewhere around it:
configuration
networking
routing
security
environment
build process
Top comments (0)