Few error messages have frustrated more developers than the dreaded “blocked by CORS policy.” You wrote perfectly good code, your API works when you test it directly, and yet the browser flatly refuses to let your frontend talk to it. Understanding CORS — Cross-Origin Resource Sharing — turns that baffling error into a five-minute fix. Here’s what’s really going on and how to solve it properly.
What CORS actually is
CORS is a browser security feature that controls whether a web page running on one origin is allowed to make requests to a different origin. An “origin” is the combination of scheme, domain, and port — so https://app.example.com and https://api.example.com are different origins, and even http vs https on the same domain counts as different.
By default, browsers enforce the same-origin policy: a page can freely talk to its own origin, but requests to other origins are restricted. CORS is the standardized way for a server to say “it’s okay, I permit requests from that other origin.” When people say CORS “blocked” their request, what actually happened is the browser asked the server for permission and didn’t get it.
Why the same-origin policy exists
This isn’t the browser being difficult — it’s protecting your users. Imagine you’re logged into your bank in one tab, then visit a malicious site in another. Without the same-origin policy, that malicious page’s JavaScript could quietly fire requests to your bank’s API using your logged-in session. The same-origin policy stops that by default, and CORS provides a controlled, explicit way to open specific doors when you genuinely need cross-origin access.
The key insight: CORS is enforced by the browser, configured on the server
This trips up almost everyone. The error appears in your browser console, so it feels like a frontend problem. But the fix is almost always on the server. The browser is only enforcing rules the server hands it via HTTP headers. Your frontend code can’t “turn off” CORS — the server has to grant permission.
The header that matters most is Access-Control-Allow-Origin. When your API responds with something like Access-Control-Allow-Origin: https://app.example.com, the browser sees that its page’s origin is on the guest list and lets the response through.
Preflight requests: the part that confuses people
For “simple” requests (a basic GET or POST), the browser just sends the request and checks the response headers. But for anything more involved — a PUT or DELETE, a custom header, or a JSON content type — the browser first sends a preflight request using the OPTIONS method. It’s essentially the browser asking, “Hey server, am I allowed to send this?” before sending the real thing.
The server must answer that OPTIONS request with the right headers:
Access-Control-Allow-Origin— which origins are permitted.Access-Control-Allow-Methods— which HTTP methods are allowed.Access-Control-Allow-Headers— which custom headers the request may include.
If the preflight doesn’t get satisfactory answers, the browser blocks the real request before it’s ever sent. Many “random” CORS failures are really a preflight the server didn’t handle.
How to actually fix a CORS error
The right fix depends on your setup, but the pattern is consistent — configure the server to allow your frontend’s origin:
- Node/Express: use the
corsmiddleware and specify your allowed origin. - A framework or API gateway: most have a CORS configuration section — set the allowed origins, methods, and headers there.
- Behind nginx or a proxy: you can add the
Access-Control-*headers at the proxy layer.
Crucially, name the specific origins you trust. It’s tempting to slap Access-Control-Allow-Origin: * on everything to make the error disappear, and while that works, it means any website can call your API. For public, read-only endpoints that may be fine; for anything involving authentication or user data, list your real origins instead.
Common mistakes to avoid
A few traps catch people repeatedly. Using the wildcard * together with credentials (cookies) doesn’t work — the spec forbids it, so you must name the exact origin when sending credentials. Forgetting to handle the OPTIONS preflight leaves you with requests that fail only for certain methods. And remember that trailing slashes and ports matter: https://example.com and https://example.com:8080 are different origins entirely.
A worked example: fixing CORS in Express
Let’s make this concrete. Say your React app runs at http://localhost:3000 and your Express API at http://localhost:5000. Different ports, so different origins — and your fetch calls fail with the CORS error. The fix takes two minutes:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors({
origin: ['http://localhost:3000', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true
}));
That middleware answers preflight OPTIONS requests automatically and adds the right headers to every response. Notice we listed the exact origins we trust rather than using a wildcard — and because credentials: true is set for cookie-based auth, a wildcard wouldn’t even be allowed. When you deploy, remember to add your production frontend’s origin to the list; “works locally, breaks in production” is very often just a missing origin entry.
Debugging CORS errors like a pro
When a CORS error strikes, resist the urge to randomly copy-paste fixes. Open your browser’s network tab and look at what actually happened. Find the failed request and check: did the browser send a preflight OPTIONS request first? What status did it get? What Access-Control-Allow-Origin value came back — was it missing entirely, or did it name a different origin than yours?
The error message itself usually tells you which rule failed. “No ‘Access-Control-Allow-Origin’ header is present” means the server sent nothing — it isn’t configured for CORS at all. “The ‘Access-Control-Allow-Origin’ header has a value that is not equal to the supplied origin” means CORS is configured, just not for your origin — check for typos, http-vs-https mismatches, or a missing port. And if only PUT/DELETE requests fail while GET works fine, your server almost certainly isn’t handling the preflight. Each message maps to a specific, fixable cause.
One more trap: server errors can masquerade as CORS errors. If your API crashes with a 500, the error response often lacks CORS headers — so the browser reports a CORS failure when the real problem is the crash. Always check the server logs before assuming the CORS config is wrong.
Frequently asked questions
Can I fix CORS from the frontend? No — that’s the whole point. CORS is the server declaring who it trusts. If frontend code could override it, the protection would be meaningless. Browser extensions that “disable CORS” only affect your own browser and are for local experimentation only.
Why does my request work in Postman but fail in the browser? Because CORS is enforced by browsers, not servers. Postman, curl, and server-to-server calls don’t apply the same-origin policy, so they sail straight through. That difference is actually a useful diagnostic: if a request works in Postman but not the browser, it’s almost certainly CORS.
Is it safe to use a CORS proxy? For quick experiments, maybe — but routing your production traffic (and users’ data) through a third-party proxy is a security risk. The proper fix is configuring the API server, or proxying through your own backend.
The takeaway
CORS isn’t your enemy — it’s a browser safety mechanism that stops malicious sites from abusing your logged-in sessions. The error shows up in the frontend, but the fix lives on the server: respond with the correct Access-Control-Allow-Origin (and, for preflighted requests, the matching methods and headers). Once you internalize that CORS is enforced by the browser but configured by the server, those “blocked by CORS policy” messages stop being mysterious and start being a quick, well-understood fix.

