Un error CORS ("No 'Access-Control-Allow-Origin' header is present") significa que una página web de un origen, como http://localhost:5173, llamó a tu API en otro origen, y el navegador no dejó que la página leyera la respuesta porque la respuesta no decía que ese origen estuviera permitido. La solución está en el servidor: envía Access-Control-Allow-Origin con el origen de la página, y responde a la petición preflight OPTIONS del navegador con los métodos y headers que aceptas. En Express son unas pocas líneas de middleware, o app.use(cors()) del paquete cors.
La respuesta es la lista de productos, y su pestaña Headers muestra access-control-allow-origin: https://app.example.com: una página de ese origen puede leerla. Ahora envía OPTIONS /api/products/1 con los headers Origin: https://app.example.com y Access-Control-Request-Method: PUT. Ese es el preflight que envía un navegador antes de un PUT, y la respuesta es 204 con los métodos y headers permitidos y sin body.
Qué comprueba el navegador
Las páginas del mismo origen no necesitan nada. Para una petición entre orígenes, el navegador lee estos headers de la respuesta:
| Header | Significado |
|---|---|
Access-Control-Allow-Origin | El único origen que puede leer la respuesta, o * para cualquiera |
Access-Control-Allow-Methods | Métodos permitidos en la petición real (solo en el preflight) |
Access-Control-Allow-Headers | Headers que la página puede enviar, como Authorization (solo en el preflight) |
Access-Control-Allow-Credentials | true si la página puede enviar cookies y leer la respuesta |
Access-Control-Max-Age | Segundos que el navegador puede cachear la respuesta del preflight |
Vary: Origin | Indica a las cachés que la respuesta depende del header Origin de la petición |
Un GET simple o un POST tipo formulario va directo al servidor, y el navegador comprueba Access-Control-Allow-Origin en la respuesta. El servidor ejecuta la petición de todas formas; CORS solo decide si la página ve el resultado. Una petición con un body JSON, un header Authorization o un método como PUT o DELETE va precedida de una petición preflight OPTIONS, y la petición real solo se envía si la respuesta del preflight lo permite. Por eso un middleware CORS tiene que responder a OPTIONS antes de que se ejecute cualquier ruta o comprobación de auth.
Permitir varios orígenes, con credenciales
Access-Control-Allow-Origin contiene un origen o *, nunca una lista. Para permitir varios, compara el header Origin de la petición con una lista de permitidos y devuélvelo cuando coincida:
La primera petición no tiene header Origin, como una llamada desde curl o desde una página del mismo origen, así que recibe {"origin":null} y ningún header CORS. Envía GET /api/me con el header Origin: http://localhost:5173 y la pestaña Headers muestra ese origen devuelto más access-control-allow-credentials: true. Cambia el header a Origin: https://evil.example y los dos headers CORS desaparecen: el servidor sigue respondiendo, pero un navegador impediría que la página lo leyera. Access-Control-Allow-Credentials: true permite que la página envíe cookies (con fetch(url, { credentials: 'include' })), y los navegadores lo rechazan junto con *, así que las credenciales siempre necesitan el origen exacto.
El paquete cors
El paquete cors hace el mismo trabajo con opciones. Instálalo con npm install cors:
const cors = require('cors');
// Any origin, no cookies: fine for a public read-only API.
app.use(cors());
// Only your frontend, with cookies.
app.use(cors({
origin: ['https://app.example.com', 'http://localhost:5173'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
// One route only.
app.get('/api/public', cors(), handler);
Llamado sin opciones, cors() envía Access-Control-Allow-Origin: * y responde a todos los preflight. Usa * solo para datos públicos que no necesitan login.
Errores comunes
Añadir CORS después de las rutas o después de un middleware de auth. La petición preflight OPTIONS no lleva header Authorization, así que una comprobación de auth puesta antes la responde con 401 y el navegador muestra un error CORS. Registra primero el middleware CORS.
Poner * con credenciales. Los navegadores rechazan la respuesta. Devuelve el origen concreto.
Intentar arreglar CORS en el frontend. Opciones de la petición como mode: 'no-cors' no quitan la restricción; te dan una respuesta opaca que no puedes leer. Solo lo arreglan los headers del servidor.
Para ver cómo funcionan app.use() y el orden del middleware, consulta middleware en Express.
Preguntas frecuentes
¿Por qué mi API funciona en Postman o curl pero no en el navegador?
CORS solo lo aplican los navegadores. Postman, curl y otros servidores ignoran los headers CORS, así que leen la respuesta igualmente. El navegador se niega a entregar la respuesta a los scripts de una página de otro origen salvo que el servidor permita ese origen.
¿CORS es una medida de seguridad para mi servidor?
Protege a los usuarios, no a tu servidor. Impide que una página maliciosa lea las respuestas de tu API con las cookies del usuario. Cualquiera puede seguir llamando a tu API desde un script o un servidor, así que la autenticación y la autorización siguen siendo cosa tuya.
¿Qué cuenta como un origen distinto?
Un origen es el esquema, el host y el puerto juntos. http://localhost:5173 y http://localhost:3000 son orígenes distintos porque cambia el puerto, y https://example.com y https://api.example.com se diferencian en el host.
¿Puedo evitar CORS en desarrollo?
Sí, haciendo que la petición sea del mismo origen: configura el servidor de desarrollo del frontend (server.proxy de Vite, por ejemplo) para que reenvíe /api al servidor de Express. Así el navegador habla con un solo origen y no hacen falta headers CORS.