Configuración Definitiva de CORS en Node.js/Express para Producción
Cómo configurar CORS correctamente en Express y Node.js para producción. Los errores más comunes, por qué tu wildcard no funciona con cookies, y la config que uso en mis APIs reales.
Tabla de contenidos
Si estás leyendo esto, probablemente estás viendo este error en la consola:
Access to fetch at 'http://localhost:4000/api/users' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
Lo he visto cientos de veces. Y la “solución” que encuentra todo el mundo en Stack Overflow es:
// ❌ La "solución" de Stack Overflow que NO debes usar en producción
app.use(cors());
Esto permite que cualquier web del mundo haga peticiones a tu API. Para desarrollo local, vale. Para producción con autenticación, es un agujero de seguridad.
Cómo funciona CORS (la versión corta)
- Tu frontend en
https://miapp.comhacefetch('https://api.miapp.com/users') - El navegador ve que son dominios diferentes → activa CORS
- El navegador envía una petición preflight (OPTIONS) preguntando: “¿puedo hacer esta petición?”
- El servidor responde con headers diciendo: “sí,
miapp.compuede hacer GET y POST” - Solo entonces el navegador permite la petición real
Si el servidor no responde con los headers correctos → el navegador bloquea la respuesta. Tu API procesó la petición, pero el navegador no te deja ver el resultado.
La configuración que uso en producción
// src/middleware/cors.js
import cors from 'cors';
const allowedOrigins = [
'https://miapp.com',
'https://www.miapp.com',
'https://admin.miapp.com',
];
// En desarrollo, añadir localhost
if (process.env.NODE_ENV !== 'production') {
allowedOrigins.push(
'http://localhost:3000',
'http://localhost:5173', // Vite
'http://localhost:4321', // Astro
);
}
export const corsMiddleware = cors({
origin: function (origin, callback) {
// Permitir peticiones sin origin (Postman, curl, server-to-server)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`Origin ${origin} not allowed by CORS`));
}
},
credentials: true, // Permite enviar cookies
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Total-Count'], // Headers custom que el frontend puede leer
maxAge: 86400, // Cache preflight 24h (reduce peticiones OPTIONS)
});
// src/app.js
import express from 'express';
import { corsMiddleware } from './middleware/cors.js';
const app = express();
// CORS debe ir ANTES de todas las rutas
app.use(corsMiddleware);
app.use(express.json());
// Tus rutas aquí
app.use('/api/users', usersRouter);
app.use('/api/tasks', tasksRouter);
Los 7 errores de CORS más comunes (y cómo solucionarlos)
Error 1: cors() sin configurar (wildcard implícito)
// ❌ Permite TODO. No usar en producción con auth.
app.use(cors());
Esto equivale a Access-Control-Allow-Origin: *. Si tu API usa cookies o tokens de sesión, el navegador rechazará las peticiones con credentials: 'include' cuando el origin es wildcard.
// ✅ Especificar dominios
app.use(cors({ origin: 'https://miapp.com', credentials: true }));
Error 2: Wildcard + credentials = error
// ❌ Esto NO funciona
app.use(cors({ origin: '*', credentials: true }));
Access to fetch at '...' has been blocked by CORS policy:
The value of the 'Access-Control-Allow-Origin' header must not be '*'
when the request's credentials mode is 'include'.
El navegador prohíbe wildcard con credentials. Es una protección de seguridad: si permitieras cualquier origen con cookies, una web maliciosa podría hacer peticiones autenticadas a tu API.
// ✅ Origin específico con credentials
app.use(cors({
origin: 'https://miapp.com',
credentials: true
}));
Error 3: El frontend no envía credentials: 'include'
Tu servidor está bien configurado, pero el frontend no envía las cookies:
// ❌ Fetch sin credentials — no envía cookies
fetch('https://api.miapp.com/users');
// ✅ Fetch con credentials — envía cookies del dominio
fetch('https://api.miapp.com/users', {
credentials: 'include',
});
// ✅ Con axios
axios.get('https://api.miapp.com/users', {
withCredentials: true,
});
Error 4: Preflight bloqueado porque OPTIONS no responde
// ❌ Si tienes rutas que bloquean OPTIONS
app.use('/api', authMiddleware, apiRoutes);
// El middleware de auth bloquea la petición OPTIONS (no tiene token)
// ✅ CORS middleware ANTES del auth middleware
app.use(corsMiddleware); // Responde a OPTIONS sin auth
app.use('/api', authMiddleware, apiRoutes);
El preflight (OPTIONS) no lleva cookies ni tokens. Si tu middleware de autenticación está antes que CORS, bloqueará el preflight y ninguna petición pasará.
Error 5: No incluir Content-Type en allowedHeaders
// ❌ El browser envía Content-Type: application/json
// pero el servidor no lo permite
app.use(cors({ allowedHeaders: ['Authorization'] }));
// ✅ Incluir Content-Type
app.use(cors({ allowedHeaders: ['Content-Type', 'Authorization'] }));
Los “simple headers” (Accept, Content-Language, Content-Type con ciertos valores) no necesitan declararse. Pero si envías Content-Type: application/json, el navegador lo trata como un header “no simple” y requiere que esté en allowedHeaders.
Error 6: Headers personalizados no visibles en el frontend
// En el backend
res.setHeader('X-Total-Count', '142');
// En el frontend
const count = response.headers.get('X-Total-Count'); // null
Los navegadores solo exponen los “CORS-safelisted headers” al frontend. Para headers custom, necesitas exposedHeaders:
// ✅ Exponer headers custom
app.use(cors({ exposedHeaders: ['X-Total-Count', 'X-Request-Id'] }));
Error 7: CORS en producción con reverse proxy (Nginx)
Si tu API está detrás de Nginx, CORS puede configurarse en Nginx O en Express, pero no en ambos. Si ambos añaden Access-Control-Allow-Origin, el navegador recibe el header duplicado y lo rechaza:
The 'Access-Control-Allow-Origin' header contains multiple values
'https://miapp.com, https://miapp.com', but only one is allowed.
# ✅ Opción 1: CORS en Nginx (y desactivar en Express)
location /api/ {
proxy_pass http://localhost:4000;
add_header Access-Control-Allow-Origin "https://miapp.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;
if ($request_method = OPTIONS) {
return 204;
}
}
// ✅ Opción 2: CORS en Express (recomendado — más control)
// No tocar Nginx, dejar que Express maneje CORS
Yo prefiero manejar CORS en Express porque es más fácil de debuggear y mantener que editando configs de Nginx.
Configuración para múltiples dominios dinámicos
Si tienes un SaaS multi-tenant donde cada cliente tiene su subdominio:
const corsMiddleware = cors({
origin: function (origin, callback) {
if (!origin) return callback(null, true);
// Permitir cualquier subdominio de miapp.com
const allowedPattern = /^https:\/\/[\w-]+\.miapp\.com$/;
if (allowedPattern.test(origin) || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
});
Cuidado: El regex debe ser estricto. Un regex laxo como /miapp\.com/ permitiría evil-miapp.com (spoofing).
Config para APIs públicas (sin auth)
Si tu API es pública y no usa cookies:
// ✅ Para APIs públicas — wildcard está bien
app.use(cors());
// Equivale a: Access-Control-Allow-Origin: *
Ejemplos donde wildcard es correcto:
- API de precios de criptomonedas
- API de datos abiertos del gobierno
- CDN de recursos estáticos
- API de herramientas públicas (conversor de moneda, clima, etc.)
Mi checklist de CORS para producción
-
originespecifica los dominios exactos (no wildcard con auth) -
credentials: truesi usas cookies de sesión -
allowedHeadersincluyeContent-TypeyAuthorization - CORS middleware va ANTES del auth middleware
- Preflight (OPTIONS) responde con 204 sin pasar por auth
-
maxAge: 86400para cachear preflight y reducir latencia - No duplicar headers CORS entre Nginx y Express
- Los origenes de desarrollo (localhost) solo se añaden en
NODE_ENV !== 'production' -
exposedHeadersdeclarados si el frontend lee headers custom - Logs de errores CORS para detectar intentos sospechosos
Artículos relacionados
- Error: CORS Access-Control-Allow-Origin — la guía de error rápida
- Proteger tu API Node.js con JWT — auth que necesita CORS bien configurado
- Banner de Cookies RGPD paso a paso — otra pieza de seguridad web