CORS Explained for Developers: Why Your API Calls Are Being Blocked
Every web developer has hit a CORS error. The browser console says "blocked by CORS policy" and nothing works. Here is exactly what CORS is, why it exists, and how to fix it properly — without just slapping Access-Control-Allow-Origin: * on everything.
What Is CORS and Why Does It Exist?
Cross-Origin Resource Sharing (CORS) is a browser security mechanism that restricts web pages from making HTTP requests to a different origin than the one that served the page. An "origin" is the combination of protocol, domain, and port:
https://app.example.com:443 ← origin
│ │ │
└protocol └domain └port
Same origin: https://app.example.com/page1 → https://app.example.com/api
Different origin: https://app.example.com → https://api.example.com (different subdomain)
Different origin: http://example.com → https://example.com (different protocol)
Different origin: https://example.com:3000 → https://example.com:8080 (different port)
Without CORS, any website could make requests to your bank's API using your cookies. CORS prevents this by requiring the server to explicitly opt in to cross-origin requests.
CORS is a browser security feature. It does not affect server-to-server requests, curl, Postman, or mobile apps. If your API works in Postman but not in the browser, CORS is the reason.
How CORS Works: The Headers
Simple Requests
For simple requests (GET, HEAD, POST with standard content types), the browser sends the request directly and checks the response headers:
Request:
GET /api/data HTTP/1.1
Origin: https://app.example.com
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
If the Access-Control-Allow-Origin header matches the requesting origin, the browser allows the response. If it is missing or does not match, the browser blocks it.
Preflight Requests (OPTIONS)
For "non-simple" requests (PUT, DELETE, custom headers, JSON content type), the browser first sends an OPTIONS preflight request to check if the server allows it:
Preflight Request:
OPTIONS /api/data HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
Preflight Response:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Only if the preflight succeeds does the browser send the actual PUT request. The Max-Age header caches the preflight result to avoid repeating it on every request.
CORS with Credentials (Cookies)
By default, cross-origin requests do not include cookies. To send cookies:
// Client: must set credentials
fetch('https://api.example.com/data', {
credentials: 'include' // send cookies
});
// Server: must respond with ALL of these
Access-Control-Allow-Origin: https://app.example.com // NOT * (wildcard prohibited with credentials)
Access-Control-Allow-Credentials: true
Critical rule: When using credentials, Access-Control-Allow-Origin cannot be *. The browser will reject it. You must specify the exact origin.
Correct CORS Configuration
Node.js / Express
const cors = require('cors');
// Production: explicit allowlist
app.use(cors({
origin: ['https://app.example.com', 'https://admin.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}));
// Dynamic origin validation
app.use(cors({
origin: function(origin, callback) {
const allowlist = ['https://app.example.com', 'https://admin.example.com'];
if (!origin || allowlist.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}));
Nginx
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
add_header Access-Control-Max-Age 86400;
return 204;
}
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials true always;
proxy_pass http://backend;
}
Debugging CORS Errors
The browser console error is often vague. Here is how to diagnose:
# Check what headers the server returns
curl -I -X OPTIONS -H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
https://api.example.com/endpoint
Look for these issues:
- Missing
Access-Control-Allow-Origin: Server is not sending CORS headers at all. - Origin mismatch: Header says
https://example.combut you are onhttps://www.example.com. - Wildcard with credentials: Using
*withcredentials: include. - Missing allowed headers:
Access-Control-Allow-Headersdoes not includeAuthorizationorContent-Type. - Preflight not handled: Server returns 405 or 404 for OPTIONS requests.
Check HTTP response codes with our HTTP Status Codes reference. Inspect request headers with our User Agent Parser.
Debug Your API Requests
Use our developer tools to inspect headers, decode JWTs, check SSL certificates, and look up DNS records.
Browse All 50+ ToolsThe Bottom Line
CORS errors mean the server is not explicitly allowing your origin. Fix it on the server, not the client. Use an explicit allowlist of origins, handle OPTIONS preflight requests, never use Access-Control-Allow-Origin: * with credentials, and cache preflight responses with Max-Age.
Related: API Security Best Practices, HTTP Status Codes, DNS Lookup, and 50+ more free tools.