DEV Community

Cover image for React App Works Locally but Not on Vercel? A Production Checklist
Blogs World
Blogs World

Posted on Originally published at blogs-world.in

React App Works Locally but Not on Vercel? A Production Checklist

npm run dev works. The production build finishes. Vercel shows a reassuring green Ready label.

Then somebody refreshes /dashboard and gets a 404.

Or the page opens, but login does nothing. The API request is still going to localhost. An image that looked fine on your laptop has disappeared. None of these failures feels related to deployment because, technically, the deployment succeeded.

That green label proves Vercel built and published something. It does not prove that every route, environment variable, browser request, and authentication assumption survived the move to production.

The short answer

If a React app works locally but fails on Vercel, check these in this order:

  1. Run the production build locally.
  2. Confirm the project root, build command, and output directory.
  3. Test a direct visit and refresh on every client-side route.
  4. Check environment-variable names, values, and deployment scope.
  5. Inspect the real API URL, HTTPS, CORS, and cookie settings.
  6. Look for filename-case and asset-path differences.
  7. Read the browser's first Console or Network error before changing configuration.

The order matters. Changing the framework preset will not repair a CORS policy, and adding a rewrite will not install a missing package.

First, separate “deployed” from “working”

A deployment pipeline can answer a few useful questions:

  • Could it install the dependencies?
  • Did the build command exit successfully?
  • Was there an output directory to publish?

It cannot automatically confirm that a user can sign in, that the API accepts the production origin, or that React Router can recover after a hard refresh.

Before debugging in the dashboard, I would reproduce the production build locally:

npm ci
npm run build
Enter fullscreen mode Exit fullscreen mode

For a Vite project, inspect that build with:

npm run preview
Enter fullscreen mode Exit fullscreen mode

For an older Create React App project, you can serve the generated folder locally:

npx serve -s build
Enter fullscreen mode Exit fullscreen mode

This is a better test than npm run dev. Development servers are forgiving. Production builds are much less interested in forgiving a missing dependency, a bad import, or a filename whose capitalization is wrong.

If you are still at the initial setup stage, this step-by-step guide to deploying a React app on Vercel covers the complete GitHub import flow, framework settings, environment variables, SPA routing, and common deployment errors. Here, I want to stay with the awkward part that begins after Vercel says the app is ready.

Three settings quietly control the whole build

Most React deployment problems are not mysterious. Vercel is simply looking in one place while the app is building in another.

Project setup Build command Normal output directory
Vite + React npm run build dist
Create React App npm run build build
React app inside a monorepo Usually npm run build Depends on the selected app and its configuration

The monorepo case is where this becomes easy to miss. If the repository looks like this:

company-project/
├── api/
├── docs/
└── apps/
    └── web/
        ├── package.json
        ├── src/
        └── vite.config.js
Enter fullscreen mode Exit fullscreen mode

then apps/web is the project root. Pointing Vercel at company-project may leave it searching for the wrong package.json or running the correct command from the wrong directory.

There is one more wrinkle: dist is only Vite's default. If vite.config.js contains a custom build.outDir, the Vercel output directory has to match it.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  build: {
    outDir: 'public-app'
  }
})
Enter fullscreen mode Exit fullscreen mode

That app produces public-app, not dist. The setting is not wrong; the two sides just need to agree.

The home page works, but /account does not

This failure is almost a rite of passage with a client-side React app.

Clicking from / to /account works because React Router is already running in the browser. Refreshing /account is different. The browser asks the server for /account, and a static host may look for a real file at that path. There is no such file, so the server returns 404 before React gets a chance to render anything.

For a frontend-only Vite single-page application using browser history, add a vercel.json file at the project root:

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "rewrites": [
    {
      "source": "/(.*)",
      "destination": "/index.html"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Commit the file and deploy again. The browser keeps the requested URL, Vercel serves index.html, and the client-side router decides which screen belongs there.

Do not paste a catch-all rewrite into every repository without looking at its architecture. A project that also contains API routes, server-rendered pages, or other separately served paths may need a more specific routing rule. Vercel's Vite deployment documentation is the right reference for the current SPA behavior.

My route test is deliberately boring: paste a nested production URL into a new private window, open it directly, and refresh it twice. Navigation from the home page is not the same test.

An environment variable can be present and still be wrong

There are four separate ways an environment variable can fail:

  • The name does not match the build tool.
  • The value belongs to local development.
  • It was added to Preview but not Production, or the reverse.
  • It was changed after the current deployment was built.

Vite exposes browser variables through import.meta.env, normally with a VITE_ prefix:

VITE_API_BASE_URL=https://api.example.com
Enter fullscreen mode Exit fullscreen mode
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL
Enter fullscreen mode Exit fullscreen mode

An existing Create React App project uses the older convention:

REACT_APP_API_BASE_URL=https://api.example.com
Enter fullscreen mode Exit fullscreen mode
const apiBaseUrl = process.env.REACT_APP_API_BASE_URL
Enter fullscreen mode Exit fullscreen mode

Those conventions are not interchangeable.

There is also a security trap hidden in the friendly phrase “environment variable.” A VITE_* value used by client-side code is bundled into JavaScript and can be inspected by anyone using the site. Vite's environment-variable guide is explicit about this. Public API base URLs and browser-safe identifiers are fine; database passwords, service-role keys, private payment credentials, and similar secrets are not.

Keep privileged values on a backend or server-side function.

Finally, changing a variable in the Vercel dashboard does not rewrite a bundle that has already been deployed. Build a new deployment after the change, and check whether you edited the Production, Preview, or Development value.

Sometimes the React app is innocent

Open the deployed site, launch DevTools, and look at the failed request in the Network panel. The actual URL usually tells a better story than the UI.

The request still points to localhost

This will never reach the backend on your laptop:

http://localhost:3000/api/products
Enter fullscreen mode Exit fullscreen mode

For every visitor, localhost means that visitor's own device. Replace it with the deployed HTTPS API URL through the correct environment variable.

The page is HTTPS but the API is HTTP

Browsers can block active mixed content. If the frontend is served over HTTPS, the production API should be available over HTTPS too.

The API rejects the Vercel origin

That is a backend CORS decision, not a React build failure. Add the exact production frontend origin to the API's allowed origins. If preview deployments also need API access, decide how those changing preview origins will be handled rather than opening the API to every origin by reflex.

Using cookies? Check Secure, SameSite, the cookie domain, credentialed CORS headers, and whether the browser actually stored the cookie. A response can return 200 while the next authenticated request still fails because the browser rejected the session cookie.

This is why “the API works in Postman” is useful but incomplete. Postman is not enforcing the browser's CORS and cookie rules.

The capitalization bug that waits for production

Some local filesystems treat Header.jsx and header.jsx as though they are the same filename. A Linux build environment does not have to be so generous.

import Header from './components/header'
Enter fullscreen mode Exit fullscreen mode

If the real file is Header.jsx, make the import match exactly:

import Header from './components/Header'
Enter fullscreen mode Exit fullscreen mode

The same issue appears with public assets. /Logo.png and /logo.png are different paths on a case-sensitive system. When only one image or module fails, compare the requested path with the repository filename character by character.

Also confirm that the missing file was committed. Git cannot deploy an image that only exists in your local working directory.

Preview deployments are more than disposable links

Once Git is connected, a branch push can create a Preview deployment while the production branch continues serving the current release. That is useful only if the preview is tested as a real environment.

A sensible flow is:

  1. Push the change to a feature branch.
  2. Let Vercel create the Preview deployment.
  3. Test direct routes, authentication, API calls, and browser errors there.
  4. Merge only after those checks pass.
  5. Verify the new Production deployment once more.

Vercel documents the branch behavior in its Git deployment guide. For applications that can charge cards, send messages, modify customer data, or perform admin actions, point Preview at a staging backend or test account. A hard-to-guess preview URL is not a security boundary.

A five-minute production check

After every meaningful deployment, I would test these before sharing the link:

  • Open the home page in a private window.
  • Paste a nested route directly into the address bar and refresh it.
  • Complete one real sign-in and sign-out cycle.
  • Trigger one representative API read and, where safe, one write.
  • Look for the first red Console error.
  • Inspect failed Network requests and their full URLs.
  • Check one narrow mobile viewport.
  • Confirm that Production is using the intended environment values.

It is a small checklist, but it catches more than staring at a green deployment card ever will.

Diagnose by symptom, not by guesswork

What you see Likely area First place to look
Build fails Dependency, import, Node version, or build script Earliest relevant build-log error and local npm run build
Deployment is ready but the page is blank Runtime JavaScript or missing configuration Browser Console
/ works but refreshing /dashboard returns 404 SPA fallback routing vercel.json and direct-route test
Requests go to localhost Production API variable Network request URL
Browser reports a CORS error Backend origin policy API CORS configuration
Images fail only in production Case mismatch or asset path Requested URL and repository filename
Preview works but Production fails Environment scope or branch configuration Production variables and production branch

The first error is normally more valuable than the tenth. Later errors are often just consequences of the first failed import, request, or initialization step.

Frequently asked questions

Do I need to upload the dist folder to Vercel?

Not for a normal Git-based deployment. Commit the source code, package.json, and the correct lock file. Vercel installs dependencies, runs the build command, and publishes the generated output. Generated folders such as dist, build, and node_modules are normally ignored.

Why did changing a Vercel environment variable do nothing?

Client-side Vite variables are inserted when the app is built. Redeploy after changing the value, and make sure the variable was added to the environment—Preview or Production—used by that deployment.

Why does React Router work when I click a link but fail after refresh?

Client-side navigation happens after React has loaded. A refresh sends the nested path to the server first. A static SPA needs a fallback rewrite so the server returns index.html and React Router can handle the URL.

Can the React frontend and API use different hosting providers?

Yes. Use the API's deployed HTTPS address and configure the backend to accept the frontend's production origin. Cross-site authentication may also require deliberate cookie and CORS settings.

Is Vercel a good host for every React project?

It is a strong fit for many Vite SPAs, frontend dashboards, prototypes, and applications built for supported serverless or framework patterns. A traditional long-running Express server, background worker, persistent WebSocket process, or content-heavy site needing server rendering has different requirements and should be evaluated separately.

“Ready” is the start of verification

A reliable React deployment is a small contract: the repository supplies a repeatable build, Vercel publishes the correct output, the router can recover on direct requests, the frontend receives the right public configuration, and the backend accepts requests from the deployed origin.

When one part of that contract breaks, start with evidence. Build locally. Open the first browser error. Read the failed request URL. Change one verified cause at a time.

That approach is slower than random dashboard edits for about thirty seconds. After that, it is much faster.

_Disclosure: This article was prepared with AI assistance. Its technical examples were checked against the linked Vercel and Vite documentation.

Top comments (0)