Quick Answer

CORS is a browser rule that stops JavaScript on one origin from reading responses from a different origin unless that server explicitly allows it. The error comes from your browser, not your code, which is why the same request succeeds in Postman or curl. The fix is always on the server being called: it must send an Access-Control-Allow-Origin header naming your origin. CORS protects browser users only — it does nothing to stop a script or server calling your API directly.

What Is Actually Happening

You call an API from your front end and get something like this:

Access to fetch at 'https://api.example.com/users' from origin
'http://localhost:3000' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.

Three things are worth noticing immediately.

The request usually succeeded. It reached the server, the server processed it and replied. The browser then refused to hand the response to your JavaScript. Check your server logs — you will often see a perfectly normal 200.

The browser blocked it, not your code. No amount of rewriting fetch will help. That is why it works in Postman and curl: they are not browsers and do not enforce this rule.

An "origin" is scheme plus host plus port. All three must match. http://localhost:3000 and http://localhost:5000 are different origins. So are http and https versions of the same host, and example.com versus www.example.com.

The underlying rule is the same-origin policy, which exists so that a malicious page cannot silently read your bank's API using cookies your browser would helpfully attach. CORS is the controlled way for a server to opt out of that restriction for specific origins.

Preflight: The Request You Did Not Send

For anything beyond a simple request, the browser sends an OPTIONS request first to ask permission. This is the preflight, and it confuses people because it appears in the network tab without being in their code.

OPTIONS /users HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

--- the server must answer ---

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

Only if that succeeds does the real request go out. A request avoids preflight only if it is a GET, HEAD or POST, with no custom headers, and a content type of text/plain, multipart/form-data or application/x-www-form-urlencoded.

That list explains a puzzle beginners hit constantly: a plain GET works, and the moment you add Content-Type: application/json or an Authorization header, CORS breaks. You did not change the origin — you crossed the line into requiring preflight.

It also means your server must actually handle OPTIONS. Frameworks that only route GET and POST will return 404 or 405 to the preflight, and the browser reports it as a CORS failure rather than a routing one.

Fixing It on the Server

The fix belongs on the server being called. In Express, use the cors middleware rather than setting headers by hand.

const cors = require('cors');

// Development: reflect any origin
app.use(cors());

// Production: name the origins you trust
app.use(cors({
  origin: ['https://yourapp.com', 'https://www.yourapp.com'],
  credentials: true,          // only if you send cookies
}));

Register it before your routes. Express runs middleware in order, so a cors() call placed after the route handlers never runs for those routes — a frequent cause of "I installed cors and it still fails".

For PHP on shared hosting, send the headers early and answer preflight explicitly:

<?php
header('Access-Control-Allow-Origin: https://yourapp.com');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;                      // preflight ends here
}

If you do not control the API — a third-party service — you cannot fix CORS from the front end. Call it from your own backend instead, which is not a browser and is not subject to the rule.

The Credentials Trap

Sending cookies or auth headers cross-origin adds a rule that catches almost everyone.

When the request includes credentials, Access-Control-Allow-Origin: * is rejected. The wildcard and credentials cannot be combined — the server must name the exact origin.

// FAILS when credentials are sent
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

// Works
Access-Control-Allow-Origin: https://yourapp.com
Access-Control-Allow-Credentials: true

Both sides must opt in. The browser must send credentials: 'include' on the fetch, and the server must return Access-Control-Allow-Credentials: true. Miss either and cookies are silently dropped, producing a request that arrives looking unauthenticated for no visible reason.

The usual production pattern is to read the Origin header, check it against an allow-list, and echo it back if it matches. Never reflect the Origin header unconditionally while allowing credentials — that is equivalent to allowing everyone, with authentication attached.

There is also a reading limit: JavaScript can only see a handful of response headers by default. To expose a custom one such as X-Total-Count, list it in Access-Control-Expose-Headers, or your client will see it missing even though it is on the wire.

CORS Is Not Server Security

This is the most important point in the article, and it is regularly misunderstood.

CORS protects people using browsers. It does not protect your API. It stops a malicious website from reading your API's responses using a victim's cookies. It does nothing whatsoever to stop curl, Postman, a Python script or any server anywhere from calling your endpoint and reading every byte.

So an endpoint is not private because a browser blocked you during testing. If data must be restricted, that is authentication and authorisation on the server — tokens, sessions, permission checks. Every year somebody ships an admin API believing it is safe because a CORS error once appeared in their console.

Two related habits worth avoiding. Disabling web security in your browser to make an error disappear hides a problem that your users will still hit. And using a public CORS proxy in production routes your traffic, including credentials, through a stranger's server.

Treat CORS as a browser-side convention to be configured correctly, and treat access control as a completely separate job that the server must do on its own.

Frequently Asked Questions

Why does my API work in Postman but fail in the browser? Because CORS is enforced only by browsers. Postman and curl are not browsers, so they ignore the rule entirely. The request itself is fine — the browser is refusing to hand the response to your JavaScript because the server did not allow your origin.
Can I fix a CORS error from the front end? No. The permission must come from the server being called, in its response headers. If you do not control that server, route the call through your own backend, which is not subject to browser CORS rules.
What is a preflight request? An automatic OPTIONS request the browser sends before the real one, asking whether the method and headers are permitted. It is triggered by custom headers, an Authorization header, a JSON content type, or methods like PUT and DELETE. Your server must handle OPTIONS or the preflight fails.
Why does Access-Control-Allow-Origin: * not work with cookies? The specification forbids combining the wildcard with credentials, because it would let any site make authenticated requests on a user's behalf. When sending cookies you must name the exact origin and also return Access-Control-Allow-Credentials: true.
Does CORS make my API secure? No. It only stops other websites reading your responses inside a browser. Any script, server or command-line tool can call your API and read everything. Restricting access requires authentication and authorisation on the server.