In this guide
CORS Explained: Why Your Frontend API Calls Get Blocked (and How to Fix It)
Your API works perfectly in Postman. It works when you open the endpoint URL directly. But the moment your React app calls fetch() on it, the browser refuses with a red console error about CORS. Nothing is wrong with your endpoint — your browser is deliberately blocking the response because the server did not say it was allowed to share it. This guide explains what CORS actually is, why the browser enforces it, how the preflight check works step by step, and exactly which headers to set on your backend so your frontend and API can talk — whether you run Express, Flask, Django, or a plain static server.
The problem: same app, two origins, blocked requests
A typical student setup looks like this: the React dev server runs on http://localhost:3000 and the API runs on http://localhost:5000. Different port means a different origin. When the frontend's JavaScript asks for data, the browser sends the request, receives the response — and then throws it away, because the API never included the header that permits localhost:3000 to read it. That is the whole mystery in one sentence: CORS is not the browser refusing to send your request; it is the browser refusing to hand you the response.
This protection exists because your browser carries your session cookies for every site. Without it, any random website you visit could fire requests to your bank's API and read the answers using your logged-in session. The same-origin policy — the rule CORS relaxes — is what stops that.
What CORS actually is (and what it is not)
CORS stands for Cross-Origin Resource Sharing. It is a mechanism by which a server can opt in to letting other origins read its responses. Three points that clear up most confusion:
- It is a server-side setting enforced by the browser. Your frontend code cannot grant itself permission. The fix always lives on the API.
- It only applies to browser-based JavaScript. curl, Postman, Python scripts and mobile apps ignore CORS entirely. "Works in Postman, fails in the browser" is the classic signature of a CORS problem, not a broken API.
- It never protects the API from being called. Anyone can send requests to your endpoint with curl. CORS protects the user's browser from being tricked into reading responses on a malicious site's behalf.
How a CORS check happens, step by step
1. Simple requests: check the response header
For "simple" requests — typically GET or POST with standard headers like Content-Type: text/plain — the browser just sends the request and looks at the response. If the response contains a matching header:
Access-Control-Allow-Origin: http://localhost:3000
the browser hands the data to your JavaScript. If the header is missing or names a different origin, the browser raises the CORS error and your fetch() promise rejects. The response reached the browser; your code simply never saw it.
2. Preflighted requests: ask permission first
Requests that could change server state get an extra round trip. If you send JSON (Content-Type: application/json) or custom headers like Authorization, the browser first sends an OPTIONS request — the preflight — asking: "Would you allow a POST with these headers from this origin?" The server must answer with the right headers:
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Only then does the browser send the real request. Open the Network tab in DevTools and you will see these OPTIONS requests; when the preflight answer is wrong, the real request is never even attempted.
The headers that matter
| Header | Who sends it | What it means |
|---|---|---|
Origin |
Browser (request) | Which page the request came from. You cannot set this from JavaScript — the browser controls it. |
Access-Control-Allow-Origin |
Server (response) | Which origin may read this response. Must match the request's origin, or be * for public data. |
Access-Control-Allow-Methods |
Server (response to preflight) | Which HTTP methods the preflighted request may use. |
Access-Control-Allow-Headers |
Server (response to preflight) | Which custom headers the request may carry (e.g. Content-Type, Authorization). |
Access-Control-Allow-Credentials |
Server (response) | Whether cookies/credentials may be included. If true, the allow-origin must be a specific origin — never *. |
Access-Control-Max-Age |
Server (response to preflight) | How long (seconds) the browser may cache this preflight answer, so it stops asking on every request. |
Fixing it on the server: the three patterns that cover most projects
Pattern 1: your own backend (recommended)
Set the header in the backend itself. A minimal Flask example using the flask-cors package:
from flask import Flask, jsonify
from flask_cors import CORS
app = Flask(__name__)
# Allow only your frontend origin; change for production
CORS(app, resources={r"/api/*": {"origins": "http://localhost:3000"}})
@app.route("/api/items")
def items():
return jsonify([{"id": 1, "name": "sensor-A"}])
The same idea in Express with the cors middleware:
const cors = require("cors");
app.use(cors({ origin: "http://localhost:3000" }));
Both libraries answer preflight OPTIONS automatically. Keep the allowed origin as tight as you can: a wildcard (*) is fine for a public read-only API, but the moment you need login cookies it stops working and becomes a security hole.
Pattern 2: static files with a plain server
If your frontend is plain HTML/JS served by Python's http.server and your API is separate, the fix is identical — it still belongs on the API. Serve the frontend from any static server you like; the CORS configuration only matters where the JSON comes from.
Pattern 3: same-origin in production
The cleanest production fix is to make the problem disappear: serve the built frontend and the API from the same origin. A Flask app can serve the React build folder, or a reverse proxy (nginx) can route / to the frontend and /api to the backend. No cross-origin request means no CORS at all — and this is how most deployed student projects should be set up.
The mistakes students make (in order of frequency)
- Trying to fix it in the frontend. Setting
Access-Control-Allow-Originas a request header does nothing; the server decides. The only frontend-side option is a dev-server proxy (e.g. the React dev server proxying/apito the backend), which works because the browser then sees a same-origin request. *together with credentials. If you setAccess-Control-Allow-Credentials: true, the allow-origin cannot be*— browsers reject the combination. Name the exact origin.- Forgetting preflight headers on the real route. The OPTIONS answer can be perfect, but if the actual response lacks
Access-Control-Allow-Origin, the browser still blocks. Both must agree. - Testing from
file://. Opening an HTML file directly gives it anullorigin, which behaves oddly with CORS rules. Serve it overhttp://localhost— evenpython -m http.serveris enough. - Hard-coding the production URL in dev. Allow-list both
http://localhost:3000and your deployed domain, or read the allowed origins from an environment variable per deployment.
Debugging a CORS error in two minutes
- Open DevTools → Network tab, trigger the failing request, and click it.
- Read the console message first — it names the missing piece, e.g. "No 'Access-Control-Allow-Origin' header is present".
- If you see an OPTIONS request that failed, the preflight is the problem: fix the allow-methods/allow-headers answer on the server.
- Then verify with curl, which ignores CORS, to confirm the endpoint itself is healthy:
curl -i http://localhost:5000/api/items
If curl gets data, your API is fine — only the CORS headers are missing. If curl also fails, you have an ordinary backend bug and CORS is a distraction.
Frequently asked questions
Is CORS a security feature of my API?
No. It protects browser users from malicious sites reading responses with their credentials. Your API must still authenticate requests itself — anyone can call it with curl regardless of CORS headers.
Why does my API work on the deployed site but not locally?
Because in production the frontend and API share one origin (same domain, same port), so no cross-origin request happens. Locally they are on different ports, which counts as different origins.
Can I just disable CORS in the browser for development?
You can launch Chrome with cross-origin checks off, but do not build your workflow on it — your deployed app and your examiners' browsers will enforce CORS. Fix the server once and the problem is gone everywhere.
Does HTTPS change anything about CORS?
The rules are the same, but http:// and https:// versions of a domain are different origins. If your API is HTTPS and your page is HTTP (or vice versa), you need headers covering the actual origin the browser sends.
What about WebSockets?
WebSocket handshakes are not subject to CORS preflight in the same way; the server validates the Origin header itself during the upgrade. If you use Socket.IO, it handles this through its own origin configuration.
Limitations
This guide covers the browser-enforced CORS model for typical student full-stack projects. It does not replace a security review: allowing extra origins widens what browser-based attackers can read, so public APIs and cookie-authenticated APIs need different policies. Native mobile apps and server-to-server calls are unaffected by CORS — if your Android app calls the same API, no headers are needed. Finally, CORS behaviour can vary slightly between browsers; always verify in the browser your users will actually run.