↓Ir al contenido

Políticas CORS

Esta semana completaste el ciclo de vida completo de una petición en NestJS. El siguiente paso natural es conectar esa API a un frontend React — y en el momento en que el frontend está en un dominio distinto al backend, el navegador aplica CORS. Este material explica exactamente qué es, cómo funciona y cómo configurarlo sin dejar la API innecesariamente expuesta.


1. La Política de Mismo Origen #

Los navegadores implementan la Same-Origin Policy (SOP): un script de una página solo puede hacer peticiones HTTP al mismo origen del que vino. Si el origen es distinto, el navegador bloquea la petición.

Origen = esquema + host + puerto. Los tres deben coincidir:

http://localhost:3000   y   http://localhost:4000   → distinto puerto → CORS
http://localhost:3000   y   https://localhost:3000  → distinto esquema → CORS
http://miapp.com        y   http://api.miapp.com    → distinto host → CORS
http://miapp.com        y   http://miapp.com        → mismo origen → sin restricción

Por qué existe #

Sin SOP, un sitio malicioso podría hacer peticiones a tu banco desde el navegador de un usuario autenticado y leer la respuesta. La SOP garantiza que malicioso.com no puede leer respuestas de banco.com, aunque el usuario tenga sesión activa.

CORS es la excepción controlada #

CORS (Cross-Origin Resource Sharing) es el mecanismo que le permite al servidor declarar explícitamente qué orígenes distintos pueden acceder a sus recursos. El servidor no bloquea — el navegador bloquea por defecto y el servidor puede levantar esa restricción con los headers correctos.

Frontend (localhost:3000) ──petición──▶ API (localhost:4000)
                          ◀──respuesta── API incluye Access-Control-Allow-Origin
Navegador comprueba el header:
  - coincide con el origen del frontend → deja pasar la respuesta
  - no coincide o no existe             → bloquea la respuesta (CORS error)

CORS no protege el servidor — protege el navegador. curl, Postman y cualquier cliente que no sea un navegador ignoran completamente CORS. El error de CORS que ves en la consola del navegador significa que la API respondió — el navegador fue quien bloqueó que tu código JavaScript leyera esa respuesta.


2. Los Headers de CORS #

En la respuesta del servidor (response headers) #

HeaderQué controlaEjemplo
Access-Control-Allow-OriginQué origen puede leer la respuestahttps://miapp.com o *
Access-Control-Allow-MethodsQué métodos HTTP están permitidosGET, POST, PUT, DELETE
Access-Control-Allow-HeadersQué headers puede enviar el clienteContent-Type, Authorization
Access-Control-Allow-CredentialsSi se permiten cookies y headers de authtrue
Access-Control-Max-AgeSegundos que el navegador cachea el preflight86400
Access-Control-Expose-HeadersHeaders de la respuesta que el JS puede leerX-Total-Count

En la petición del cliente (request headers — enviados automáticamente por el navegador) #

HeaderQué informa
OriginEl origen del frontend que hace la petición
Access-Control-Request-MethodEl método que se va a usar (en preflight)
Access-Control-Request-HeadersLos headers que se van a enviar (en preflight)

3. Peticiones Simples vs Preflighted #

No todas las peticiones desencadenan el mismo flujo de CORS.

Peticiones simples #

Una petición es “simple” si cumple todas estas condiciones:

  • Método: GET, POST o HEAD
  • Content-Type: text/plain, multipart/form-data o application/x-www-form-urlencoded
  • Sin headers personalizados (no Authorization, no X-Custom-Header)

Las peticiones simples van directamente al servidor. El navegador lee el Access-Control-Allow-Origin de la respuesta y decide si deja que tu JS acceda a ella.

JS en localhost:3000
  ──GET /api/cursos──▶  API en localhost:4000
  ◀──200 OK + Access-Control-Allow-Origin: http://localhost:3000──
Navegador: ✅ el origen coincide → JS puede leer la respuesta

Peticiones preflighted #

Cualquier petición que no sea “simple” desencadena primero una petición OPTIONS automática — el preflight. El navegador pregunta al servidor “¿puedo hacer esta petición?”. Solo si el servidor responde correctamente, el navegador envía la petición real.

Condiciones que activan el preflight:

  • Método: PUT, DELETE, PATCH
  • Content-Type: application/json
  • Cualquier header personalizado (Authorization, X-Request-ID, etc.)
JS en localhost:3000 quiere hacer POST con JSON y Authorization:

1. PREFLIGHT (automático, antes de la petición real):
   ──OPTIONS /api/cursos──▶  API en localhost:4000
     Origin: http://localhost:3000
     Access-Control-Request-Method: POST
     Access-Control-Request-Headers: Content-Type, Authorization

   ◀──204 No Content──
     Access-Control-Allow-Origin: http://localhost:3000
     Access-Control-Allow-Methods: GET, POST, PUT, DELETE
     Access-Control-Allow-Headers: Content-Type, Authorization

2. PETICIÓN REAL (solo si el preflight fue exitoso):
   ──POST /api/cursos──▶  API en localhost:4000
     Authorization: Bearer eyJ...
     Content-Type: application/json
   ◀──201 Created──

Si el servidor no responde al OPTIONS con los headers correctos, la petición real nunca se envía. El error de CORS en el navegador en estos casos viene del preflight fallido, no de la petición real.


4. Configuración en Express #

npm install cors
npm install -D @types/cors

Permitir todos los orígenes — solo para desarrollo #

import cors from "cors";

app.use(cors());
// equivale a: Access-Control-Allow-Origin: *

"*" permite cualquier origen pero tiene una restricción importante: es incompatible con credentials: true. Si tu frontend envía cookies o el header Authorization, no puedes usar "*".

Lista blanca explícita #

const ORIGENES_PERMITIDOS = [
  "http://localhost:3000",       // desarrollo frontend
  "https://miapp.com",           // producción
  "https://staging.miapp.com",   // staging
];

app.use(
  cors({
    origin: (origin, callback) => {
      // origin es undefined en peticiones server-to-server (curl, Postman)
      if (!origin || ORIGENES_PERMITIDOS.includes(origin)) {
        callback(null, true);
      } else {
        callback(new Error(`Origen no permitido: ${origin}`));
      }
    },
    methods:     ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
    allowedHeaders: ["Content-Type", "Authorization"],
    credentials: true,           // permite cookies y el header Authorization
    maxAge:      86400,          // cachea el preflight 24 horas
  })
);

credentials: true — cuándo y por qué #

credentials: true es necesario cuando el frontend envía:

  • Cookies (document.cookie, cookies de sesión)
  • El header Authorization en peticiones fetch con credentials: 'include'

Cuando credentials: true, el navegador exige que Access-Control-Allow-Origin sea un origen específico — nunca "*". Si el servidor responde con "*" y el cliente envía credenciales, el navegador bloquea igualmente.

// ❌ Incompatible — el navegador lo rechaza aunque llegue la respuesta
app.use(cors({ origin: "*", credentials: true }));

// ✅ Origen específico con credenciales
app.use(cors({ origin: "http://localhost:3000", credentials: true }));

5. Configuración en NestJS #

NestJS expone la configuración de CORS a través de app.enableCors():

// main.ts
const app = await NestFactory.create(AppModule);

// Forma simple — todos los orígenes (solo desarrollo)
app.enableCors();

// Forma completa — equivale a la configuración de cors() en Express
app.enableCors({
  origin: (origin, callback) => {
    const permitidos = [
      "http://localhost:3000",
      "https://miapp.com",
    ];
    if (!origin || permitidos.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error("Origen no permitido"));
    }
  },
  methods:         "GET,POST,PUT,PATCH,DELETE,OPTIONS",
  allowedHeaders:  "Content-Type,Authorization",
  credentials:     true,
  maxAge:          86400,
});

await app.listen(4000);

Internamente, enableCors() configura el mismo paquete cors de Express.

Leer orígenes desde variables de entorno #

// .env
CORS_ORIGINS=http://localhost:3000,https://miapp.com,https://staging.miapp.com
// main.ts
const origenes = (process.env.CORS_ORIGINS ?? "")
  .split(",")
  .map(o => o.trim())
  .filter(Boolean);

app.enableCors({
  origin: (origin, callback) => {
    if (!origin || origenes.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error("Origen no permitido"));
    }
  },
  credentials: true,
});

Esto permite cambiar los orígenes permitidos por entorno (desarrollo, staging, producción) sin tocar el código.


6. Exponer Headers Personalizados al Cliente #

Por defecto, el navegador solo expone al JavaScript del cliente un subconjunto de headers de la respuesta. Si tu API devuelve headers personalizados (como X-Total-Count para paginación), debes declararlos explícitamente:

app.enableCors({
  origin:         "http://localhost:3000",
  exposedHeaders: ["X-Total-Count", "X-Request-Id"],
  // sin esto, response.headers.get("X-Total-Count") devuelve null en el frontend
});

7. El Error de CORS — Cómo Leerlo #

El error más común en la consola del navegador:

Access to fetch at 'http://localhost:4000/api/cursos' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.

Lo que significa: la petición llegó al servidor, el servidor respondió, pero la respuesta no tenía el header Access-Control-Allow-Origin — o tenía un valor que no coincide con el origen del frontend.

Diagnóstico rápido #

Error: "No 'Access-Control-Allow-Origin' header"
→ CORS no está configurado en el servidor, o el middleware no está activo

Error: "has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin'
header must not be the wildcard '*' when the request's credentials mode is 'include'"
→ Estás usando origin: "*" con credentials: true — son incompatibles

Error en preflight: "Method DELETE is not allowed by Access-Control-Allow-Methods"
→ El método DELETE no está en la lista de métodos permitidos

Error: "Response to preflight request doesn't pass access control check"
→ El endpoint OPTIONS no está respondiendo correctamente — verifica que el middleware
   de CORS esté antes que las rutas y que maneje OPTIONS

Preguntas Frecuentes #

¿Por qué Postman no tiene errores de CORS pero el navegador sí? #

CORS es una restricción del navegador, no del servidor. Postman, curl, Node.js fetch y cualquier cliente que no sea un navegador no aplican la política de mismo origen. El servidor siempre responde — solo el navegador decide si tu JavaScript puede leer esa respuesta.

¿Tengo que configurar CORS en desarrollo si el frontend y el backend corren en el mismo host? #

Sí, si corren en puertos distintos. localhost:3000 y localhost:4000 son orígenes distintos aunque el host sea localhost. Solo estarías exento si sirves el frontend desde el mismo proceso Node.js que la API.

¿credentials: true es solo para cookies? #

No — es para cualquier petición que incluya credenciales en el sentido de HTTP: cookies, certificados de cliente TLS, o headers de autenticación HTTP básica. El header Authorization: Bearer ... no cuenta como credencial en este sentido — puedes enviarlo con origin: "*". Las cookies sí cuentan y sí requieren credentials: true.

Nota: si usas fetch con credentials: 'include' en el frontend, eso fuerza al navegador a tratar la petición como credencial y exige que el servidor responda con Access-Control-Allow-Credentials: true y un origen específico (no *).

¿Qué es maxAge y vale la pena configurarlo? #

maxAge (en segundos) le dice al navegador cuánto tiempo puede cachear la respuesta del preflight. Sin él, el navegador hace un OPTIONS antes de cada petición no simple — lo que puede agregar latencia visible en aplicaciones con muchas peticiones. Un valor de 86400 (24 horas) elimina prácticamente todo el overhead de preflight en producción.

¿Puedo bloquear CORS completamente para una API pública? #

“Bloquear CORS” no tiene sentido para una API pública — CORS abre acceso desde orígenes cruzados. Una API completamente pública usa origin: "*" para que cualquier frontend pueda consumirla. “Sin CORS configurado” no bloquea las peticiones — solo hace que el navegador bloquee la lectura de las respuestas en frontends de otros orígenes.

¿El middleware de CORS debe ir antes o después de los Guards y rutas? #

Siempre antes de cualquier otra cosa — incluyendo rutas, Guards y middleware de autenticación. Si el preflight OPTIONS llega al Guard de JWT antes del middleware de CORS, el Guard lo rechaza con 401 antes de que CORS pueda procesarlo. El navegador recibe un error de autenticación, no la respuesta de CORS, y el error que ves en la consola es confuso.

// ✅ CORS siempre primero
const app = await NestFactory.create(AppModule);
app.enableCors({ ... });         // primero
app.useGlobalGuards(...);        // después
app.useGlobalPipes(...);
app.useGlobalFilters(...);
await app.listen(4000);

Resumen #

ConceptoClave
Same-Origin PolicyEsquema + host + puerto deben coincidir — el navegador bloquea lo demás
CORS abre excepcionesEl servidor declara qué orígenes cruzados puede leer su respuesta
CORS no protege el servidorcurl y Postman ignoran CORS — solo aplica en navegadores
Petición simpleGET/POST sin headers personalizados ni JSON → sin preflight
Petición preflightedPUT/DELETE/PATCH, JSON body, Authorization → OPTIONS primero
Access-Control-Allow-Origin: *Válido si no hay credenciales — incompatible con credentials: true
credentials: trueNecesario para cookies — exige origen específico, no "*"
Lista blanca de orígenesFunción (origin, callback) que valida contra un array de permitidos
maxAge: 86400Cachea el preflight 24 horas — elimina el overhead de OPTIONS repetidos
exposedHeadersDeclara qué headers personalizados puede leer el JS del cliente
CORS antes que todoenableCors() o app.use(cors()) antes de rutas, Guards y middleware de auth
Variable de entorno CORS_ORIGINSLista separada por comas — permite cambiar orígenes sin tocar código