Qué es CORS y por qué existe
CORS (Cross-Origin Resource Sharing) es la política de seguridad del navegador que bloquea requests JavaScript entre dominios diferentes. Existe para prevenir que evil.com haga requests a yourbank.com usando tus cookies activas. Sin CORS, cualquier sitio podría leer tu sesión de Gmail o hacer transferencias bancarias en tu nombre.
El navegador envía un preflight request (OPTIONS) antes de la request real si usás métodos no-simples (PUT, DELETE) o headers custom. El servidor responde con headers Access-Control-* indicando qué está permitido. Si el servidor no responde con headers correctos, el navegador bloquea la response aunque el servidor la haya procesado. Es policy client-side, no server-side.
CORS no protege tu API de requests directos (curl, Postman, backend-to-backend). Solo protege contextos de navegador. Si tu API es pública, CORS no agrega seguridad real—necesitás autenticación, rate limiting, validación. CORS es para prevenir ataques de navegador comprometido, no para proteger endpoints sensibles. Esa es responsabilidad de tokens, OAuth, API keys.
Errores comunes que rompen CORS
Access-Control-Allow-Origin: * con credentials: true no funciona. El navegador rechaza la combinación. Si necesitás credentials (cookies, Authorization header), debés especificar origin exacto: https://app.example.com. No podés usar wildcard. Esto obliga a implementar lógica dinámica: chequear el header Origin del request contra whitelist y responder con ese origin específico.
Olvidar OPTIONS: el preflight OPTIONS debe retornar 200 con headers CORS correctos, sin autenticación. Muchos backends requieren auth en todas las rutas, incluyendo OPTIONS. Esto rompe el preflight: el navegador nunca envía credentials en OPTIONS, entonces el preflight falla con 401 antes de intentar la request real. Excluí OPTIONS de middleware de auth.
Headers expuestos incompletos: si tu API retorna headers custom (X-Total-Count, X-RateLimit-Remaining), el frontend no puede leerlos sin Access-Control-Expose-Headers. Por default solo expone headers simples (Content-Type, Content-Length). Si tu frontend hace response.headers.get('X-Total-Count') y es null, olvidaste exponerlo. Listá todos los headers custom que tu frontend necesita leer.
Configuraciones por ambiente
Desarrollo: Access-Control-Allow-Origin: * está bien localmente. Facilita testing con distintos puertos (frontend en 3000, backend en 8000). Pero nunca commitees esta config a producción. Usá variables de entorno: ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 en dev, origins reales en prod.
Staging: whitelist de dominios de staging y preview. Si usás Vercel/Netlify con preview URLs, necesitás permitir https://*.vercel.app o similar. Esto es más permisivo que producción pero controlado. Considerá regex para match de subdominios: /^https:\/\/.*\.example\.com$/. Testeá que funcione antes de mergear a main.
Producción: lista exacta de origins permitidos. https://app.example.com, https://www.example.com. Nada de wildcards si podés evitarlo. Si tenés subdominios dinámicos (tenant-based SaaS), validá contra patrón estricto: /^https:\/\/[a-z0-9-]+\.example\.com$/. Loggeá origins rechazados para detectar typos en DNS o intentos de ataque. MaxAge largo (7200+) reduce preflight overhead en producción.
Implementación en distintos frameworks
Express.js: usá cors package. app.use(cors({ origin: process.env.ALLOWED_ORIGINS.split(','), credentials: true })). Configurá antes de rutas. Para control fino, pasá función: origin: (origin, callback) => { if (whitelist.includes(origin)) callback(null, true); else callback(new Error('Not allowed')); }. Manejá OPTIONS explícitamente si tenés middleware custom.
Next.js: en API routes, setteá headers manualmente: res.setHeader('Access-Control-Allow-Origin', allowedOrigin); res.setHeader('Access-Control-Allow-Methods', 'GET,POST'); if (req.method === 'OPTIONS') { res.status(200).end(); return; }. O usá middleware en next.config.js con headers globales. Edge runtime tiene limitaciones; testeá bien.
Django: django-cors-headers package. En settings: CORS_ALLOWED_ORIGINS = ['https://app.example.com'], CORS_ALLOW_CREDENTIALS = True. Agregá corsheaders.middleware.CorsMiddleware al principio de MIDDLEWARE. Para regex: CORS_ALLOWED_ORIGIN_REGEXES = [r'^https://\w+\.example\.com$']. Cuidado con orden de middleware: CORS debe procesar antes que auth.