If you have seen a red CORS error in your browser console, it means your frontend is sending a request to an API but the browser refuses to hand the response back to you. The classic message reads: Access to fetch at '...' from origin '...' has been blocked by CORS policy. At first glance it looks like the server crashed, but the request usually reaches the server and a response does come back — the browser simply won't pass that response to your JavaScript because of a security rule. In this article I explain what the error really is, the concepts of origin and preflight, and how to fix the problem permanently with the right response headers.
What exactly is a CORS error?
CORS stands for Cross-Origin Resource Sharing. By default, browsers enforce the same-origin policy: JavaScript on a page may only read responses that belong to the same origin. An origin is made of three parts: protocol, domain and port. So https://site.com, http://site.com, https://api.site.com and https://site.com:8080 are all different origins.
If your frontend runs on http://localhost:3000 and your API on http://localhost:8000, those are two different origins, and the browser blocks the response unless the server explicitly grants permission. CORS is the standardized way of granting that permission: the server adds special headers to its response saying "I allow this origin to read data from me".
The key distinction: the fix lives on the server, not the client
This is the most common misunderstanding. CORS is a browser mechanism, but the permission comes from the server. You cannot solve the error by tweaking fetch options on the JavaScript side; the fix is making the API send the correct response headers. The key header is:
Access-Control-Allow-Origin: https://site.com
If this header is missing from the response, or does not match the origin making the request, the browser blocks it. When you fire the same request from Postman or curl you have no problem — because they are not browsers and do not enforce the same-origin policy. That is exactly why the error only appears in the browser.
What is a preflight (OPTIONS) request?
Before some requests, the browser sends a preflight request before the actual one. This is a "permission check" made with the OPTIONS method. The browser asks: "May I send a request from this origin, with this method, with these headers?"
A preflight is triggered because the request is not "simple". A request needs a preflight if it uses a method other than GET, POST or HEAD (e.g. PUT, DELETE, PATCH), or if it carries custom headers (e.g. Authorization, Content-Type: application/json). The server must return the allowed methods and headers in its OPTIONS response:
Access-Control-Allow-Origin: https://site.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Access-Control-Max-Age tells the browser how many seconds to cache the preflight response, preventing an OPTIONS call on every request. If the server returns a 404 or 405 to the OPTIONS request, that is the first thing you need to fix.
Fixing the CORS error on the server side
The solution varies by framework, but the logic is the same: add the right headers. In an Express-based Node.js API, the official cors package is the cleanest path:
const cors = require('cors');
app.use(cors({
origin: 'https://site.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
}));
In Laravel, the CORS configuration lives in config/cors.php and the HandleCors middleware applies it automatically. You define the allowed origins there:
'paths' => ['api/*'],
'allowed_methods' => ['*'],
'allowed_origins' => ['https://site.com'],
'allowed_headers' => ['*'],
'supports_credentials' => true,
On a web server such as Nginx or Apache you can add the headers directly too, but managing them at the application layer is usually more flexible because you can validate the origin conditionally.
Working with credentials (cookies and tokens)
If your requests carry cookies or authentication data, two rules kick in. First: you must enable credentials: 'include' on the client side:
fetch('https://api.site.com/data', {
credentials: 'include',
});
Second and most critical: when credentials are used, the Access-Control-Allow-Origin header cannot be * (wildcard). The browser rejects it for security reasons; you must spell out the exact origin. The server must also send Access-Control-Allow-Credentials: true. Skipping these two rules is one of the most common causes of the confusing "cannot use wildcard with credentials" error.
Common mistakes and a quick checklist
- Wildcard + credentials conflict: if you send cookies, use the exact origin instead of
*. - OPTIONS left unanswered: make sure your server returns a 2xx response to the preflight.
- Trailing slash difference:
https://site.comversushttps://site.com/can break origin matching; compare protocol + host + port precisely. - Proxy workaround: in development you can proxy the frontend dev server (e.g. Vite) to the API so requests appear to come from the same origin.
Frequently Asked Questions
Can I fix the CORS error by changing only the frontend code?
No. The correct response headers must come from the server. The only things you can do on the frontend are using a proxy in development or fixing your credentials setting; the actual permission is always granted on the API side.
Why does it work in Postman but I get a CORS error in the browser?
Because CORS is a security policy enforced only by the browser. Postman and curl do not enforce the same-origin policy, so the same request works fine there. This tells you the server is running but the CORS headers are missing.
Is it safe to use Access-Control-Allow-Origin: *?
It is acceptable for a public API that carries no credentials. But for APIs that work with cookies/tokens or serve sensitive data, do not use the wildcard; list the allowed origins explicitly.
Want to fix the CORS configuration on your API permanently and securely? Let's review the origin, preflight and credentials settings between your frontend and backend together and build a solution that fits your project — get in touch with me.