CORS errors are some of the most frustrating in web development. The configuration has many moving parts, and a single mistake — like using * with credentials — silently breaks your app.
I built a free tool that lets you configure CORS visually and get copy-ready code for Express.js, nginx, Apache, and Cloudflare Workers.
Tool
CORS Headers Generator
https://devnestio.pages.dev/cors-generator/
Configure origins, methods, allowed headers, exposed headers, credentials, and preflight max-age — then pick your framework.
How CORS Works
Browsers block cross-origin requests (different domain, port, or protocol) unless the server explicitly allows them via CORS headers.
# The browser sends a preflight OPTIONS request:
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
# The server responds with:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
The Most Common Mistakes
Wildcard + Credentials = broken
# This does NOT work:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true <- ignored by browsers!
When using credentials (cookies, Authorization headers), you must specify the exact origin:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
Multiple origins need dynamic responses
The Access-Control-Allow-Origin header can only contain one origin. For multiple allowed origins, check the request Origin header on the server and reflect it back:
const ALLOWED_ORIGINS = ['https://app.example.com', 'https://admin.example.com'];
const corsOptions = {
origin: (origin, callback) => {
if (!origin || ALLOWED_ORIGINS.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS not allowed'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400,
};
app.use(cors(corsOptions));
app.options('*', cors(corsOptions)); // Handle preflight
Always add Vary: Origin when the response differs by origin — this prevents CDNs from caching the wrong origin's response.
nginx Configuration
# Single origin
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Max-Age "86400" always;
# Handle OPTIONS preflight
if ($request_method = OPTIONS) {
add_header Content-Length 0;
add_header Content-Type text/plain;
return 204;
}
Cloudflare Workers
export default {
async fetch(request, env) {
const origin = request.headers.get('Origin');
const ALLOWED = ['https://app.example.com', 'https://admin.example.com'];
const allow = ALLOWED.includes(origin) ? origin : null;
const corsHeaders = {
'Access-Control-Allow-Origin': allow || '',
'Vary': 'Origin',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
};
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders });
}
const response = await fetch(request);
const newHeaders = new Headers(response.headers);
Object.entries(corsHeaders).forEach(([k, v]) => newHeaders.set(k, v));
return new Response(response.body, { ...response, headers: newHeaders });
}
};
Generate your CORS config in seconds — no more copy-pasting from StackOverflow.
Top comments (0)