Proxy it or fix CORS? Two answers to the same cross-origin wall
You hit the cross-origin wall: your frontend on one origin cannot read a response from an API on another, because the API does not permit your origin. There are exactly two real fixes, and picking between them comes down to a single question — do you control the API server? If you do, add the CORS headers. If you do not, proxy the request through your own origin so, as far as the browser is concerned, there is no cross-origin request at all. Everything else (“disable CORS in the browser,” “add a header in my fetch”) is either a local hack or impossible, because CORS is granted by the API’s response, not by the client.
Fix CORS when you own the API
If the API is yours, the correct fix is to send the permission headers for the
origins you trust. Echo the specific origin (do not reflexively use *, which also
disables credentialed requests), and answer the preflight:
// express: allow your app's origin to read responses, and handle preflight
app.use((req, res, next) => {
res.set("Access-Control-Allow-Origin", "https://app.example.com");
res.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
res.set("Access-Control-Allow-Headers", "content-type, authorization");
if (req.method === "OPTIONS") return res.sendStatus(204); // preflight
next();
});
This is the clean fix because it addresses the actual thing — the API declaring who may read it.
Proxy when you don’t
If the API belongs to someone else, you cannot make it send headers. So you remove the cross-origin-ness instead: the browser calls your server (same origin, no CORS), and your server calls the third-party API server-to-server, where CORS does not exist:
// your own origin: the browser sees a same-origin call; you relay it server-side
app.get("/api/weather", async (req, res) => {
const upstream = await fetch("https://third-party.com/v1/weather?city=" + req.query.city, {
headers: { Authorization: `Bearer ${process.env.WEATHER_KEY}` }, // secret stays server-side
});
res.json(await upstream.json());
});
In dev, the framework’s dev server usually does this for you with a proxy config, which is why a call works locally and then breaks in production — the dev proxy was quietly making it same-origin.
The proxy has a bonus: secrets stay server-side
There is a reason to prefer the proxy even for some APIs you could CORS-enable: it keeps credentials off the client. A third-party API key shipped to the browser is public; relayed through your proxy, it lives in a server env var and never reaches the user. That makes the proxy the right answer whenever a secret is involved, cross-origin or not. So the decision tree is: own the API and no secret → send CORS headers; don’t own it, or a secret is involved → proxy. What you never do is disable browser security or pretend the client can grant itself permission — CORS is the server’s to give, and the proxy is how you route around a server that won’t. The proxy-and-cors exercise builds both fixes so the choice becomes reflexive.