What CORS is and why it exists
CORS (Cross-Origin Resource Sharing) is the browser security policy that blocks JavaScript requests between different domains. It exists to prevent evil.com from making requests to yourbank.com using your active cookies. Without CORS, any site could read your Gmail session or make bank transfers in your name.
The browser sends a preflight request (OPTIONS) before the real request if you use non-simple methods (PUT, DELETE) or custom headers. The server responds with Access-Control-* headers indicating what's allowed. If the server doesn't respond with correct headers, the browser blocks the response even though the server processed it. It's client-side policy, not server-side.
CORS doesn't protect your API from direct requests (curl, Postman, backend-to-backend). It only applies in browser contexts. If your API is public, CORS doesn't add real security—you need authentication, rate limiting, validation. CORS is to prevent compromised browser attacks, not to protect sensitive endpoints. That's the responsibility of tokens, OAuth, API keys.
Common mistakes that break CORS
Access-Control-Allow-Origin: * with credentials: true doesn't work. The browser rejects the combination. If you need credentials (cookies, Authorization header), you must specify exact origin: https://app.example.com. You can't use wildcard. This forces you to implement dynamic logic: check the Origin header from the request against a whitelist and respond with that specific origin.
Forgetting OPTIONS: the preflight OPTIONS must return 200 with correct CORS headers, without authentication. Many backends require auth on all routes, including OPTIONS. This breaks preflight: the browser never sends credentials in OPTIONS, so preflight fails with 401 before attempting the real request. Exclude OPTIONS from auth middleware.
Incomplete exposed headers: if your API returns custom headers (X-Total-Count, X-RateLimit-Remaining), the frontend can't read them without Access-Control-Expose-Headers. By default it only exposes simple headers (Content-Type, Content-Length). If your frontend does response.headers.get('X-Total-Count') and it's null, you forgot to expose it. List all custom headers your frontend needs to read.
Configurations by environment
Development: Access-Control-Allow-Origin: * is fine locally. Facilitates testing with different ports (frontend on 3000, backend on 8000). But never commit this config to production. Use environment variables: ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 in dev, real origins in prod.
Staging: whitelist of staging and preview domains. If you use Vercel/Netlify with preview URLs, you need to allow https://*.vercel.app or similar. This is more permissive than production but controlled. Consider regex for subdomain matching: /^https:\/\/.*\.example\.com$/. Test that it works before merging to main.
Production: exact list of allowed origins. https://app.example.com, https://www.example.com. No wildcards if you can avoid it. If you have dynamic subdomains (tenant-based SaaS), validate against strict pattern: /^https:\/\/[a-z0-9-]+\.example\.com$/. Log rejected origins to detect DNS typos or attack attempts. Long MaxAge (7200+) reduces preflight overhead in production.
Implementation in different frameworks
Express.js: use cors package. app.use(cors({ origin: process.env.ALLOWED_ORIGINS.split(','), credentials: true })). Configure before routes. For fine control, pass function: origin: (origin, callback) => { if (whitelist.includes(origin)) callback(null, true); else callback(new Error('Not allowed')); }. Handle OPTIONS explicitly if you have custom middleware.
Next.js: in API routes, set headers manually: res.setHeader('Access-Control-Allow-Origin', allowedOrigin); res.setHeader('Access-Control-Allow-Methods', 'GET,POST'); if (req.method === 'OPTIONS') { res.status(200).end(); return; }. Or use middleware in next.config.js with global headers. Edge runtime has limitations; test thoroughly.
Django: django-cors-headers package. In settings: CORS_ALLOWED_ORIGINS = ['https://app.example.com'], CORS_ALLOW_CREDENTIALS = True. Add corsheaders.middleware.CorsMiddleware at the beginning of MIDDLEWARE. For regex: CORS_ALLOWED_ORIGIN_REGEXES = [r'^https://\w+\.example\.com$']. Careful with middleware order: CORS must process before auth.