What is CORS?
CORS is a security feature built into browsers that restricts web pages from making requests to domains different from the one serving the page. It's the reason your frontend on localhost:3000 can't just fetch data from api.example.com without permission.
The browser enforces this by checking specific HTTP headers on the server's response. If the headers don't explicitly allow your origin, the request fails.
Access to fetch at 'https://api.example.com/data' from origin
'http://localhost:3000' has been blocked by CORS policy
Sound familiar? Welcome to web development.
How It Works
- Browser makes a request from
origin-a.comtoorigin-b.com - Browser checks if
origin-b.comallows requests fromorigin-a.com - Server responds with CORS headers (or doesn't)
- Browser either allows or blocks the response
The key insight: CORS is enforced by the browser, not the server. The request actually reaches the server. The browser just won't let your JavaScript see the response if the headers are missing.
The Headers
| Header | Purpose | Example |
|---|---|---|
Access-Control-Allow-Origin | Which origins can access | * or https://mysite.com |
Access-Control-Allow-Methods | Allowed HTTP methods | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers | Allowed request headers | Content-Type, Authorization |
Access-Control-Allow-Credentials | Allow cookies/auth | true |
Access-Control-Max-Age | Cache preflight (seconds) | 86400 |
Access-Control-Expose-Headers | Headers JS can read | X-Custom-Header |
Simple vs Preflight Requests
Not all cross-origin requests are created equal:
Simple requests go directly to the server:
GET,HEAD, orPOSTmethod- Only "safe" headers (
Accept,Content-Typewith simple values, etc.) - No custom headers
Preflight requests require an OPTIONS check first:
PUT,DELETE,PATCHmethods- Custom headers like
Authorization Content-Type: application/json
OPTIONS /api/data HTTP/1.1
Origin: https://mysite.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://mysite.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Server Configuration Examples
Express.js:
const cors = require('cors');
// Allow all origins (development only!)
app.use(cors());
// Allow specific origin
app.use(cors({
origin: 'https://mysite.com',
credentials: true
}));
Next.js API Route:
export async function GET(request) {
return Response.json({ data: 'hello' }, {
headers: {
'Access-Control-Allow-Origin': 'https://mysite.com',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
}
});
}
Nginx:
location /api/ {
add_header 'Access-Control-Allow-Origin' 'https://mysite.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
}
Where You'll Hit This
- Frontend calling a backend API - The classic scenario
- Third-party API integrations - Stripe, Twilio, etc.
- Microservices architectures - Services on different subdomains
- CDN resources - Fonts, images, scripts from other domains
- Local development - Different ports count as different origins!
What Counts as "Cross-Origin"?
Two URLs have the same origin only if they match on:
- Protocol (
httpvshttps) - Host (
api.site.comvssite.com) - Port (
:3000vs:8080)
| From | To | Same Origin? |
|---|---|---|
https://site.com | https://site.com/api | Yes |
https://site.com | http://site.com | No (protocol) |
https://site.com | https://api.site.com | No (subdomain) |
http://localhost:3000 | http://localhost:8080 | No (port) |
Common Gotchas
You cannot use Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. The browser will reject it. You must specify the exact origin.
- Forgetting the preflight - Your
OPTIONSendpoint must also return CORS headers - Caching preflights - Set
Access-Control-Max-Ageto avoid repeated OPTIONS requests - Port matters in development -
localhost:3000andlocalhost:3001are different origins - The request still reaches your server - CORS doesn't block the request, just the response. Don't rely on it for security.
CORS protects users, not your API. Anyone can bypass CORS using curl, Postman, or a backend proxy. Always authenticate and authorize requests server-side.
Debugging Tips
- Check the Network tab - Look for the OPTIONS preflight request
- Look at response headers - Are the CORS headers actually present?
- Check the console error - It usually tells you exactly what's missing
- Test with curl - If curl works but browser doesn't, it's CORS
# This bypasses CORS (no browser)
curl -X POST https://api.example.com/data \
-H "Content-Type: application/json" \
-d '{"key": "value"}'
Try It
Encode URLs for Cross-Origin Requests"CORS: Teaching developers that 'No' is a complete sentence since 2009."