Meta Description: Understand CORS (Cross-Origin Resource Sharing) for REST APIs. Learn preflight requests, credentials, and how to configure CORS securely.
Keywords: cors, cross-origin requests, cors headers, preflight requests, api cors, cors configuration
Word Count: ~2,200 words
Your API works perfectly in Postman. But when you call it from a browser, you get:
Access to fetch at 'https://api.petstoreapi.com/v1/pets' from origin
'https://myapp.com' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.
This is CORS (Cross-Origin Resource Sharing). It's a browser security feature that blocks requests to different domains.
Here's how to fix it correctly.
What Is CORS?
CORS is a browser security mechanism. It prevents malicious websites from making unauthorized requests to your API.
Same-Origin Policy
Browsers enforce the same-origin policy. JavaScript can only make requests to the same origin (protocol + domain + port).
Same origin (allowed):
Page: https://myapp.com/dashboard
API: https://myapp.com/api/pets ✓
Different origin (blocked):
Page: https://myapp.com/dashboard
API: https://api.petstoreapi.com/v1/pets ✗
Without CORS, the second request fails.
Why Same-Origin Policy Exists
Imagine you're logged into your bank at bank.com. You visit a malicious site evil.com. Without same-origin policy:
// On evil.com
fetch('https://bank.com/api/transfer', {
method: 'POST',
credentials: 'include', // Sends your bank.com cookies
body: JSON.stringify({
to: 'attacker-account',
amount: 10000
})
});
The browser would send your authentication cookies to bank.com, and the attacker could steal your money.
Same-origin policy prevents this. Requests from evil.com to bank.com are blocked.
How CORS Works
CORS lets servers explicitly allow cross-origin requests.
Simple Requests
For simple requests (GET, POST with simple headers), the browser:
- Sends the request with an
Originheader - Server responds with
Access-Control-Allow-Origin - Browser allows or blocks based on the header
Request:
GET /v1/pets HTTP/1.1
Host: api.petstoreapi.com
Origin: https://myapp.com
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Content-Type: application/json
[{"id":"123","name":"Max"}]
The browser sees Access-Control-Allow-Origin: https://myapp.com and allows the response.
Preflight Requests
For complex requests (PUT, DELETE, custom headers), the browser sends a preflight request first.
Preflight (OPTIONS request):
OPTIONS /v1/pets/123 HTTP/1.1
Host: api.petstoreapi.com
Origin: https://myapp.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization
Preflight response:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Max-Age: 86400
If the preflight succeeds, the browser sends the actual request.
Configuring CORS
Allow All Origins (Development Only)
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});
Warning: * allows any website to call your API. Only use this for public APIs without authentication.
Allow Specific Origins (Production)
const allowedOrigins = [
'https://myapp.com',
'https://staging.myapp.com',
'http://localhost:3000' // Development
];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});
Handle Preflight Requests
app.options('*', (req, res) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '86400'); // Cache preflight for 24 hours
res.status(204).send();
});
Using CORS Middleware
const cors = require('cors');
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // Allow cookies
maxAge: 86400 // Cache preflight for 24 hours
}));
CORS with Credentials
To send cookies or authentication headers, you need special configuration.
Client Side
fetch('https://api.petstoreapi.com/v1/pets', {
credentials: 'include' // Send cookies
});
Server Side
app.use(cors({
origin: 'https://myapp.com', // Must be specific, not '*'
credentials: true
}));
Important: When credentials: true, you cannot use Access-Control-Allow-Origin: *. You must specify exact origins.
Common CORS Issues
Issue 1: Wildcard with Credentials
Error:
The value of the 'Access-Control-Allow-Origin' header in the response
must not be the wildcard '*' when the request's credentials mode is 'include'.
Fix: Use specific origins, not *:
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');
Issue 2: Missing Preflight Headers
Error:
Request header field authorization is not allowed by
Access-Control-Allow-Headers in preflight response.
Fix: Add the header to allowed headers:
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
Issue 3: Preflight Cache Not Working
Browsers cache preflight responses. If you change CORS config, clients might use stale cache.
Fix: Set Access-Control-Max-Age appropriately:
res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours
For development, use 0 to disable caching:
res.setHeader('Access-Control-Max-Age', '0');
Issue 4: Custom Headers Not Exposed
By default, browsers only expose safe response headers. Custom headers are hidden.
Fix: Use Access-Control-Expose-Headers:
res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count, X-Page-Number');
Now JavaScript can read these headers:
const response = await fetch('/v1/pets');
const totalCount = response.headers.get('X-Total-Count');
CORS Security Best Practices
1. Validate Origins Strictly
Don't use regex or substring matching:
Bad:
// Allows evil-myapp.com!
if (origin.includes('myapp.com')) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
Good:
const allowedOrigins = ['https://myapp.com', 'https://staging.myapp.com'];
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
2. Don't Reflect Origin Blindly
Bad:
// Allows any origin!
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
Good:
if (allowedOrigins.includes(req.headers.origin)) {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
}
3. Use HTTPS
CORS doesn't protect against man-in-the-middle attacks. Always use HTTPS for both your app and API.
4. Limit Allowed Methods
Only allow methods you actually use:
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
// Don't add TRACE, CONNECT, or other methods you don't use
5. Limit Allowed Headers
Only allow headers you need:
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Don't allow all headers
CORS Alternatives
1. Proxy Server
Run a proxy on your domain that forwards requests to the API:
Browser → https://myapp.com/api/pets → Proxy → https://api.petstoreapi.com/v1/pets
No CORS issues because requests stay on the same origin.
2. JSONP (Deprecated)
JSONP bypasses CORS by using <script> tags. Don't use it—it's insecure and deprecated.
3. Server-Side Requests
Make API calls from your backend, not the browser:
Browser → Your Backend → API
No CORS because browsers aren't involved.
Testing CORS
Using cURL
# Simple request
curl -H "Origin: https://myapp.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: authorization" \
-X OPTIONS \
https://api.petstoreapi.com/v1/pets
Check for Access-Control-Allow-Origin in the response.
Using Browser DevTools
- Open DevTools → Network tab
- Make a cross-origin request
- Check the request headers (should include
Origin) - Check the response headers (should include
Access-Control-Allow-Origin)
Using Online Tools
- test-cors.org
- Chrome extension: CORS Unblock (for testing only)
Summary
CORS is a browser security feature that blocks cross-origin requests by default.
To allow cross-origin requests: 1. Set Access-Control-Allow-Origin header 2. Handle OPTIONS preflight requests 3. Set Access-Control-Allow-Methods and Access-Control-Allow-Headers
For credentials (cookies, auth headers): 1. Set Access-Control-Allow-Credentials: true2. Use specific origins, not *3. Set credentials: 'include' on the client
Security: - Validate origins strictly - Use HTTPS - Limit allowed methods and headers - Don't reflect origins blindly
CORS protects users from malicious websites. Configure it correctly to balance security and functionality.