localhost always means the device running the code. Fix: Find every hardcoded local URL; Same app serves the API? Use a relative path; Separate backend? Put its public URL in a build-time variable; Or proxy /api through your frontend host; Allow your site's origin on the API (CORS); Call the API over HTTPS; Make the backend listen on 0.0.0.0 and the host's PORT; Redeploy and test from another device.
- Browser console:
TypeError: Failed to fetchornet::ERR_CONNECTION_REFUSEDfor ahttp://localhost:…URL - Sign-up, login or data loading works on your computer but fails on your phone or anyone else's device
- After you fix the URL:
has been blocked by CORS policyor a Mixed Content warning in the console - The backend host shows
Application failed to respondor a 502 instead of your API
Why does this happen?
localhost always means the device running the code. AI tools write http://localhost:3001/api/... into the frontend because that's where the backend ran while you built the app, so on your computer it works. In production every visitor's browser tries to reach a server on their own device, which doesn't exist. Fixing the address often uncovers three follow-ups: the API must allow your site's origin (CORS), an HTTPS page can't call an http:// API (mixed content), and the backend has to listen on the address and port the host gives it.
How do you fix it?
-
Find every hardcoded local URL
Search the frontend for
localhostand127.0.0.1. Check fetch calls, axios instances, API client files and.envfiles that were committed with development values.grep -rnE "localhost|127\.0\.0\.1" src .env* 2>/dev/null -
Same app serves the API? Use a relative path
If the API lives in the same deployment (for example Next.js API routes or TanStack Start server functions), call
/api/...with no host. The browser sends it to whatever domain the page came from, so it works locally, on preview URLs and on your custom domain.// before const res = await fetch("http://localhost:3001/api/notes"); // after (API in the same app) const res = await fetch("/api/notes"); -
Separate backend? Put its public URL in a build-time variable
In a Vite app (Lovable, Bolt and most React builds) only variables prefixed
VITE_reach browser code, read throughimport.meta.env. In Next.js the prefix isNEXT_PUBLIC_. Both are written into the bundle at build time, so set the variable on your host, then redeploy: Next.js says a built app no longer responds to changes in these variables. They are visible to anyone, so never put secrets in them.// .env.production (or the host's environment settings) // VITE_API_URL=https://api.example.com const API_URL = import.meta.env.VITE_API_URL ?? ""; const res = await fetch(`${API_URL}/api/notes`); -
Or proxy /api through your frontend host
A rewrite on the frontend host forwards
/api/*to the real backend, so the browser only talks to your own domain and the frontend keeps using relative paths. Netlify documents this as a proxy rule in_redirects; Vercel supports rewrites to external origins invercel.json.# Netlify: public/_redirects /api/* https://api.example.com/:splat 200 // Vercel: vercel.json { "rewrites": [ { "source": "/api/:path*", "destination": "https://api.example.com/:path*" } ] } -
Allow your site's origin on the API (CORS)
When the frontend and API are on different domains, the browser only lets your code read the response if the API sends
Access-Control-Allow-Originfor your site. With Express, thecorsmiddleware sets it. List your production domain (and preview domains you use); MDN notes that requests sent with credentials such as cookies need an explicit origin rather than*.import cors from "cors"; app.use( cors({ origin: ["https://yourapp.com", "https://www.yourapp.com"], credentials: true, }), ); -
Call the API over HTTPS
Your deployed site is served over HTTPS, and browsers block insecure
http://requests made from a secure page (mixed content). Use the backend'shttps://URL from its host. -
Make the backend listen on 0.0.0.0 and the host's PORT
A backend that only listens on
localhost, or on a port you picked, can't be reached through the host's router. Railway says to listen on0.0.0.0and thePORTvariable it injects, otherwise you getApplication failed to respond; Render says web services must bind to host0.0.0.0and that the default expected port is 10000.const port = Number(process.env.PORT) || 3001; app.listen(port, "0.0.0.0", () => console.log(`API listening on ${port}`)); -
Redeploy and test from another device
Rebuild the frontend so the new value is baked in, then open the live site on your phone with Wi-Fi off, or in a private window. In the Network tab, every API request should now go to your real domain.
Common questions
Why does it work on my computer and nowhere else?
Your backend is running on your computer, so localhost finds it there. Any other device looks for a server on itself and finds nothing.
I changed the environment variable on the host. Why does the app still call the old URL?
VITE_ and NEXT_PUBLIC_ values are copied into the JavaScript when the app is built. Changing them has no effect until you trigger a new build and deploy.
Is it safe to put my API URL in a `VITE_` variable?
A public URL is fine. Anything with that prefix ends up in code every visitor can read, so keep API keys, database passwords and service-role keys in server-only variables without the prefix.
Can I just set `Access-Control-Allow-Origin: *`?
Only for a public API that doesn't use cookies or other credentials. MDN says that for credentialed requests the server must name the origin instead of using the * wildcard.