CORS Explained: Why Your API Requests Get Blocked and How to Fix It
CORS Explained: Why Your API Requests Get Blocked#
Every web developer has seen this error:
Access to fetch at 'https://api.example.com/users' from origin 'https://myapp.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present.
CORS isn't a bug — it's a security feature. Here's how it works and how to configure it correctly.
Same-Origin Policy#
Browsers enforce the same-origin policy: JavaScript on one origin can't make requests to a different origin.
An origin = scheme + host + port:
https://myapp.com ← origin
https://myapp.com:3000 ← different origin (different port)
http://myapp.com ← different origin (different scheme)
https://api.myapp.com ← different origin (different host)
Without this policy, any website could read your bank account data by making fetch requests to your bank's API using your cookies.
How CORS Works#
CORS (Cross-Origin Resource Sharing) is an opt-in mechanism. The server tells the browser which origins are allowed.
Simple Requests#
For GET/POST with standard headers:
Browser: GET https://api.example.com/users
Origin: https://myapp.com
Server response:
Access-Control-Allow-Origin: https://myapp.com
← Browser allows the response
Server response (no header):
← Browser BLOCKS the response
Preflight Requests#
For PUT/DELETE/PATCH or custom headers, the browser sends an OPTIONS request first:
1. Browser: OPTIONS https://api.example.com/users
Origin: https://myapp.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
2. Server: 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
3. Browser: PUT https://api.example.com/users/123
(actual request, now allowed)
Access-Control-Max-Age caches the preflight result — avoids re-checking for 24 hours.
The Headers#
| Header | Direction | Purpose |
|---|---|---|
Origin | Request | Browser sends the requesting origin |
Access-Control-Allow-Origin | Response | Which origins can access |
Access-Control-Allow-Methods | Response | Which HTTP methods are allowed |
Access-Control-Allow-Headers | Response | Which request headers are allowed |
Access-Control-Allow-Credentials | Response | Whether cookies/auth can be sent |
Access-Control-Max-Age | Response | How long to cache preflight |
Access-Control-Expose-Headers | Response | Which response headers JS can read |
Common Configurations#
Allow Specific Origin#
Access-Control-Allow-Origin: https://myapp.com
Allow Multiple Origins (Dynamic)#
You can't list multiple origins in one header. Instead, check the request Origin and reflect it:
// Express
app.use((req, res, next) => {
const allowed = ["https://myapp.com", "https://admin.myapp.com"];
const origin = req.headers.origin;
if (allowed.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
}
next();
});
Allow All Origins (Dangerous)#
Access-Control-Allow-Origin: *
Never use * with credentials. It allows any website to make requests.
With Credentials (Cookies/Auth)#
Access-Control-Allow-Origin: https://myapp.com // Must be specific, not *
Access-Control-Allow-Credentials: true
// Client must also opt in:
fetch("https://api.example.com/me", {
credentials: "include", // sends cookies
});
Framework Examples#
Express.js#
import cors from "cors";
app.use(cors({
origin: ["https://myapp.com", "https://admin.myapp.com"],
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
maxAge: 86400,
}));
Next.js API Routes#
// next.config.js
module.exports = {
async headers() {
return [{
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Origin", value: "https://myapp.com" },
{ key: "Access-Control-Allow-Methods", value: "GET,POST,PUT,DELETE" },
{ key: "Access-Control-Allow-Headers", value: "Content-Type,Authorization" },
],
}];
},
};
Nginx#
location /api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://myapp.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
add_header 'Access-Control-Allow-Origin' 'https://myapp.com';
proxy_pass http://backend;
}
Common Mistakes#
1. Using * with Credentials#
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
← Browser rejects this! Can't use * with credentials.
Fix: Use the specific origin, not *.
2. Missing Preflight Handling#
Server returns CORS headers on GET/POST but not on OPTIONS. Fix: Handle OPTIONS requests explicitly or use a CORS middleware.
3. Forgetting Exposed Headers#
By default, JS can only read these response headers: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma.
Custom headers like X-Total-Count need:
Access-Control-Expose-Headers: X-Total-Count
4. CORS on the Wrong Layer#
CORS headers must be on the response from the server, not on the proxy or CDN (unless configured there too). If your API is behind Nginx → API server, both might need CORS headers.
When CORS Doesn't Apply#
- Server-to-server requests — no browser, no CORS
- Same-origin requests —
myapp.com→myapp.com/api - Proxy through your own server — browser → your backend → their API
- JSONP — legacy workaround (don't use)
Architecture: Avoiding CORS#
The cleanest solution is often to avoid cross-origin requests entirely:
Option 1: Same origin
myapp.com/ → frontend (Next.js)
myapp.com/api/ → backend (API routes)
→ No CORS needed!
Option 2: Proxy
myapp.com/api/* → Nginx proxies to api.internal:3000
→ Browser sees same origin, no CORS
Option 3: API Gateway
myapp.com → CDN → API Gateway → backend services
→ Gateway handles CORS once for all services
Design API architectures at codelit.io — security audits catch missing CORS and auth gaps automatically.
Summary#
- CORS is a browser security feature, not a server bug
- Same-origin policy prevents cross-site data theft
- Set specific origins, never
*with credentials - Handle OPTIONS preflight or use CORS middleware
- Cache preflight with
Max-Ageto reduce overhead - Best fix: avoid CORS by using same-origin or a proxy
116 articles on system design at codelit.io/blog.
Try it on Codelit
GitHub Integration
Paste any repo URL to generate an interactive architecture diagram from real code
Related articles
Try these templates
OpenAI API Request Pipeline
7-stage pipeline from API call to token generation, handling millions of requests per minute.
8 componentsDistributed Rate Limiter
API rate limiting with sliding window, token bucket, and per-user quotas.
7 componentsAPI Gateway Platform
Kong/AWS API Gateway-like platform with routing, auth, rate limiting, transformation, and developer portal.
8 componentsBuild this architecture
Generate an interactive architecture for CORS Explained in seconds.
Try it in Codelit →
Comments