CORS Explained: Cross-Origin Resource Sharing
CORS is a critical security mechanism. Understanding how it works is essential for building web applications that make cross-origin requests.
Quick Navigation: How CORS Works • CORS Headers Explained • Common CORS Errors • Server-Side Implementation • Best Practices
Quick Decision Guide
Quick Fix Guide:
Common Error: "CORS policy blocked" when frontend (localhost:3000) calls backend (localhost:8000).
Solution: Configure server to send CORS headers: - Access-Control-Allow-Origin: Set to your frontend domain (never * in production) - Access-Control-Allow-Methods: GET, POST, PUT, DELETE - Access-Control-Allow-Headers: Content-Type, Authorization
Preflight Requests: Browser sends OPTIONS request first for PUT/DELETE/custom headers. Server must handle OPTIONS and return CORS headers.
Express.js Example: Use cors middleware: app.use(cors({ origin: 'http://localhost:3000' }))
Security Tip: Never use Access-Control-Allow-Origin: * with credentials. Always specify exact origin in production.
How CORS Works
Request Flow
Simple Request (No Preflight)
// Frontend makes request
fetch('https://api.com/data');
// Browser checks:
// 1. Is origin different? Yes
// 2. Is it a simple request? Yes
// 3. Browser sends request with Origin header
// 4. Server responds with CORS headers
// 5. Browser checks headers, allows/deniesPreflight Request
For "non-simple" requests, browser sends OPTIONS request first:
// Frontend makes request
fetch('https://api.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token',
},
});
// Browser sends preflight:
OPTIONS https://api.com/data
Origin: https://app.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
// Server responds:
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: content-type, authorization
// Browser checks response, then sends actual requestSimple vs Preflight Requests
Simple Requests (No Preflight)
Preflight Required
CORS Headers Explained
Essential CORS Headers
Access-Control-Allow-Origin
// Allow specific origin
Access-Control-Allow-Origin: https://app.com
// Allow all origins (NOT recommended for production)
Access-Control-Allow-Origin: *
// Allow multiple origins (must check in code)
if (allowedOrigins.includes(req.headers.origin)) {
res.header('Access-Control-Allow-Origin', req.headers.origin);
}Access-Control-Allow-Methods
// Allow specific methods
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
// Allow all methods
Access-Control-Allow-Methods: *Access-Control-Allow-Headers
// Allow specific headers
Access-Control-Allow-Headers: Content-Type, Authorization
// Allow all headers
Access-Control-Allow-Headers: *Access-Control-Allow-Credentials
// Allow cookies/credentials
Access-Control-Allow-Credentials: true
// Frontend must also set:
fetch(url, {
credentials: 'include',
});Note: Cannot use * with credentials
Common CORS Errors
Error: "No 'Access-Control-Allow-Origin' header"
Problem: Server doesn't send CORS headers
Solution:
// Express.js
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
next();
});Error: "Preflight request doesn't pass"
Problem: Preflight OPTIONS request fails
Solution:
// Handle OPTIONS request
app.options('*', (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.sendStatus(200);
});Error: "Credentials flag is true but 'Access-Control-Allow-Credentials' is not 'true'"
Problem: Using credentials but server doesn't allow
Solution:
// Server
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Origin', 'https://app.com'); // Must be specific, not *
// Client
fetch(url, {
credentials: 'include',
});Error: "Request header field is not allowed"
Problem: Custom header not allowed
Solution:
// Server must include header in Allow-Headers
res.header('Access-Control-Allow-Headers', 'X-Custom-Header');Server-Side Implementation
Express.js Example
const express = require('express');
const app = express();
// CORS middleware
const allowedOrigins = [
'https://app.com',
'https://www.app.com',
'http://localhost:3000',
];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Max-Age', '86400'); // 24 hours
// Handle preflight
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
app.get('/api/data', (req, res) => {
res.json({ message: 'Hello CORS!' });
});Using cors Package
const cors = require('cors');
app.use(cors({
origin: ['https://app.com', 'http://localhost:3000'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));Next.js API Routes
// pages/api/data.js
export default function handler(req, res) {
res.setHeader('Access-Control-Allow-Origin', 'https://app.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
res.json({ data: 'Hello' });
}Best Practices
Security Considerations
1. Don't Use Wildcard (*) in Production
// ❌ BAD
Access-Control-Allow-Origin: *
// ✅ GOOD
Access-Control-Allow-Origin: https://app.com2. Whitelist Specific Origins
const allowedOrigins = [
'https://app.com',
'https://www.app.com',
];
if (allowedOrigins.includes(req.headers.origin)) {
res.header('Access-Control-Allow-Origin', req.headers.origin);
}3. Use Environment Variables
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];4. Set Appropriate Max-Age
// Cache preflight for 24 hours
Access-Control-Max-Age: 86400Common Patterns
Pattern 1: Public API
// Allow all origins, no credentials
app.use(cors({
origin: '*',
credentials: false,
}));Pattern 2: Authenticated API
// Specific origins, with credentials
app.use(cors({
origin: ['https://app.com'],
credentials: true,
}));Pattern 3: Development Only
if (process.env.NODE_ENV === 'development') {
app.use(cors({
origin: 'http://localhost:3000',
}));
}Debugging CORS
// Log CORS requests
app.use((req, res, next) => {
console.log('Origin:', req.headers.origin);
console.log('Method:', req.method);
console.log('Headers:', req.headers['access-control-request-headers']);
next();
});