Works locally, but "Failed to fetch" after deploying: your app still calls localhost

Short answer

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.

You'll see:
  • Browser console: TypeError: Failed to fetch or net::ERR_CONNECTION_REFUSED for a http://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 policy or a Mixed Content warning in the console
  • The backend host shows Application failed to respond or 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?

  1. Find every hardcoded local URL

    Search the frontend for localhost and 127.0.0.1. Check fetch calls, axios instances, API client files and .env files that were committed with development values.

    grep -rnE "localhost|127\.0\.0\.1" src .env* 2>/dev/null
  2. 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");
  3. 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 through import.meta.env. In Next.js the prefix is NEXT_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`);
  4. 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 in vercel.json.

    # Netlify: public/_redirects
    /api/*  https://api.example.com/:splat  200
    
    // Vercel: vercel.json
    {
      "rewrites": [
        { "source": "/api/:path*", "destination": "https://api.example.com/:path*" }
      ]
    }
  5. 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-Origin for your site. With Express, the cors middleware 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,
      }),
    );
  6. 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's https:// URL from its host.

  7. 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 on 0.0.0.0 and the PORT variable it injects, otherwise you get Application failed to respond; Render says web services must bind to host 0.0.0.0 and 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}`));
  8. 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.

Common with:

Related fixes