Escena Viva ya tiene routers, controladores y middleware propio. Si la desplegaras hoy funcionaría, y también funcionaría cualquiera que quisiera hacerle daño: no envía cabeceras de seguridad, no controla desde qué origen la llaman, no registra las peticiones en un formato estándar, no comprime nada y no impide que un script compre las 1189 entradas libres del catálogo en un bucle de diez segundos. En esta lección montamos el kit mínimo: cinco paquetes, uno más mencionado, y para cada uno la misma tríada —qué problema resuelve, cómo se configura y qué pasa si no lo pones—, todos pasados antes por el filtro de auditoría del módulo 5.

Contenido

  1. El criterio del módulo 5 aplicado a cada paquete
  2. helmet: cabeceras de seguridad
  3. cors: peticiones desde otro origen
  4. morgan: registro HTTP
  5. compression: ancho de banda
  6. express-rate-limit: límites de uso
  7. cookie-parser: preparando el módulo 8
  8. El orden correcto: crearAplicacion() completa

  1. El criterio del módulo 5 aplicado a cada paquete

Antes de escribir npm install, la comprobación que ya sabes hacer:

npm view helmet version license repository.url maintainers  # metadatos basicos
npm view helmet time.modified   # un paquete de seguridad sin publicar en tres años alarma
npm view helmet dependencies    # cada dependencia es superficie de ataque
npm audit                       # tras instalar: vulnerabilidades conocidas

Resultado del análisis para los paquetes de esta lección:

Paquete Dependencias propias Mantenimiento Veredicto
helmet Ninguna Activo, versión mayor reciente Aceptar
cors 2 (object-assign, vary) Estable, cambios poco frecuentes Aceptar con vigilancia
morgan y compression 4 y 5, todas del equipo de Express Estables y activos Aceptar
express-rate-limit Ninguna Muy activo, buena documentación Aceptar
cookie-parser 2 Estable Aceptar (módulo 8)

Que helmet y express-rate-limit tengan cero dependencias es exactamente el tipo de dato que buscabas en el módulo 5: menos código de terceros, menos superficie, menos riesgo de que un mantenedor comprometido acabe en tu node_modules. Y como siempre, cuidado con el tipogrifo: los paquetes se llaman helmet (no helmetjs), cors (no express-cors) y express-rate-limit (no express-ratelimit). Instálalos con npm install helmet cors morgan compression express-rate-limit y comprueba después con npm ls --all --parseable | wc -l cuánto ha crecido el árbol y con npm audit si algo viene con vulnerabilidades.

  1. helmet: cabeceras de seguridad

Qué problema resuelve. Los navegadores implementan una decena de mecanismos de defensa que solo se activan si el servidor los pide con cabeceras; sin ellas se comportan de la forma más permisiva posible. Basta con aplicacion.use(helmet()), y estas son las cabeceras que aparecen:

Cabecera Valor por defecto (resumido) Qué evita
Content-Security-Policy default-src 'self'; script-src 'self'; ... Que se ejecute JavaScript de orígenes no autorizados (la defensa real contra XSS)
X-Content-Type-Options nosniff Que el navegador adivine el tipo de un fichero e interprete como script algo que no lo es
Strict-Transport-Security max-age=31536000; includeSubDomains Que el navegador use HTTP tras la primera visita por HTTPS
X-Frame-Options SAMEORIGIN Que tu página se cargue en un iframe ajeno (clickjacking)
Referrer-Policy no-referrer Filtrar la URL completa de origen a sitios externos
Cross-Origin-Opener-Policy y -Resource-Policy same-origin Aislar tu ventana de otras pestañas y evitar que otros sitios incrusten tus recursos
Origin-Agent-Cluster y X-DNS-Prefetch-Control ?1 / off Aislamiento de procesos y resolución DNS anticipada que filtra navegación
X-Permitted-Cross-Domain-Policies none Políticas heredadas de Flash/PDF

Qué pasa si no lo pones: todas esas defensas quedan desactivadas. Ninguna es imprescindible por sí sola ni sustituye a validar la entrada, pero juntas convierten varios ataques posibles en imposibles por una línea de código.

La CSP y tu front-end estático

Aquí está el problema práctico. La CSP por defecto de helmet incluye script-src 'self', lo que bloquea todo JavaScript en línea. Si publico/index.html tiene un <button onclick="comprar('ses-001-1')"> o un <script> con constantes de las salas, ambas cosas dejarán de funcionar y en la consola del navegador aparecerá Refused to execute inline script because it violates the following Content-Security-Policy directive. La reacción instintiva es desactivar la CSP. No lo hagas: es la única cabecera de la lista que de verdad para un XSS. Las salidas correctas, por orden de preferencia: saca el JavaScript en línea a publico/app.js y usa addEventListener en lugar de onclick (una hora de trabajo y arregla el problema para siempre); si algún script en línea es inevitable, usa un nonce por respuesta; y solo como último recurso relaja la directiva concreta, nunca la CSP entera.

// Opcion 2: nonce por peticion, generado antes de helmet.
const { randomBytes } = require('node:crypto');
aplicacion.use((peticion, respuesta, siguiente) => {
  respuesta.locals.nonce = randomBytes(16).toString('base64');
  siguiente();
});

aplicacion.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      // El nonce se recalcula en cada respuesta: no es reutilizable.
      scriptSrc: ["'self'", (peticion, respuesta) => `'nonce-${respuesta.locals.nonce}'`],
      styleSrc: ["'self'"],
      imgSrc: ["'self'", 'data:'],
      connectSrc: ["'self'"], // desde donde puede hacer fetch el front-end
      objectSrc: ["'none'"],
      frameAncestors: ["'none'"],
    },
  },
  // Solo tiene sentido si de verdad sirves por HTTPS.
  strictTransportSecurity: configuracion.esProduccion
    ? { maxAge: 31_536_000, includeSubDomains: true }
    : false,
}));

API pura sin front-end: si Escena Viva solo sirviera JSON, la CSP sería casi irrelevante y podrías dejar helmet() por defecto. El conflicto aparece porque servimos publico/.

  1. cors: peticiones desde otro origen

Qué problema resuelve. El navegador aplica la política del mismo origen: una página solo lee respuestas de su mismo origen (esquema + host + puerto).

Página API ¿Mismo origen? Motivo
http://localhost:3000 http://localhost:3000 Sí Idénticos
http://localhost:5173 http://localhost:3000 No Puerto distinto
https://escenaviva.test http://escenaviva.test No Esquema distinto
https://escenaviva.test https://api.escenaviva.test No Host (subdominio) distinto

Cuando no coinciden, el navegador hace la petición pero oculta la respuesta al JavaScript salvo que el servidor autorice con Access-Control-Allow-Origin. Ojo con el matiz: CORS no protege tu servidor, protege al usuario del navegador; un curl o un script en Node lo ignoran por completo.

La petición de verificación previa (preflight)

Para peticiones "no simples" —cualquier POST con Content-Type: application/json, o con cabeceras personalizadas como X-Id-Peticion— el navegador envía antes un OPTIONS preguntando permiso:

OPTIONS /api/pedidos HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, x-id-peticion

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET,POST
Access-Control-Allow-Headers: content-type,x-id-peticion
Access-Control-Max-Age: 600

Y solo si esa respuesta autoriza, el navegador envía el POST real. Access-Control-Max-Age es importante para el rendimiento: sin él, el navegador repite el OPTIONS antes de cada petición.

Configuración real de Escena Viva

// src/middleware/cors.js
const cors = require('cors');
const { configuracion } = require('../config/index.js');
function crearCors() {
  return cors({
    // Lista blanca leida de ORIGENES_PERMITIDOS (06-02), nunca '*'.
    origin(origen, devolucion) {
      // Sin cabecera Origin (curl, apps moviles, servidor a servidor) se acepta.
      if (!origen || configuracion.origenesPermitidos.includes(origen)) {
        devolucion(null, true);
        return;
      }
      const mensaje = `Origen no permitido: ${origen}`;
      devolucion(Object.assign(new Error(mensaje), { codigo: 'RUTA_NO_PERMITIDA' }));
    },
    methods: ['GET', 'POST', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Accept', 'X-Id-Peticion', 'X-Canal-Venta'],
    // exposedHeaders: las que el JavaScript del navegador podra LEER.
    exposedHeaders: ['X-Id-Peticion', 'RateLimit-Remaining'],
    maxAge: 600,
    credentials: false,
  });
}

origin: '*' frente a lista blanca

origin: '*' Lista blanca
Quién puede llamar desde un navegador Cualquier sitio web del mundo Solo los que autorizas
Compatible con credentials: true No: el navegador lo rechaza Sí
Riesgo y cuándo es aceptable Una web maliciosa puede leer tus respuestas con la sesión del visitante; solo para API pública de solo lectura sin datos personales Acotado; para todo lo demás

La advertencia sobre credentials. Si activas credentials: true, el navegador envía cookies y cabeceras de autenticación en las peticiones entre orígenes, lo que abre la puerta a que otra web actúe en nombre del usuario con sesión abierta. Tres reglas si algún día lo necesitas (módulo 8): credentials: true exige un origen concreto, porque con '*' el navegador rechaza la respuesta; nunca devuelvas dinámicamente el Origin recibido sin comprobarlo contra la lista blanca, porque eso es una lista blanca falsa que acepta a cualquiera; y combínalo con cookies SameSite=Lax o Strict y protección CSRF. Qué pasa si no lo pones: el publico/app.js servido desde otro puerto recibirá el clásico blocked by CORS policy y no verá ni una respuesta. Si en producción el front-end se sirve desde el mismo origen que la API, CORS no hace falta: es el escenario más seguro.

  1. morgan: registro HTTP

Qué problema resuelve. Deja constancia de cada petición en un formato estándar que las herramientas de análisis saben leer. En 06-04 escribiste tu propio registro; morgan hace lo mismo con formatos consolidados y tokens propios.

aplicacion.use(morgan('dev'));      // desarrollo: compacto y coloreado por estado
// GET /api/eventos/evt-001 200 412 - 3.201 ms
aplicacion.use(morgan('combined')); // produccion: Combined Log Format (Apache/Nginx)
// 127.0.0.1 - - [14/Aug/2026:09:12:44 +0000] "GET /api/eventos HTTP/1.1" 200 812 "-" "curl/8.5.0"
Formato Contenido Para qué
dev Método, ruta, estado coloreado, tiempo Desarrollo
combined Formato Apache con Referer y User-Agent Producción con herramientas clásicas
common / tiny Sin Referer ni User-Agent / lo mínimo Alternativas más ligeras
Personalizado Los tokens que definas Registro estructurado (módulo 11)

Enlazarlo con el registro estructurado del módulo 11

En producción los registros se consultan con herramientas que necesitan JSON por línea, no texto legible; morgan lo permite definiendo tokens y pasando un stream propio:

// src/middleware/registro-http.js
const morgan = require('morgan');
const { configuracion } = require('../config/index.js');
// Tokens propios: exponen datos que morgan no conoce.
morgan.token('id-peticion', (peticion) => peticion.idPeticion ?? '-');
morgan.token('canal', (peticion) => peticion.get('X-Canal-Venta') ?? '-');

// Formateador que emite una linea de JSON por peticion.
function formatoJson(t, p, r) {
  return JSON.stringify({
    instante: new Date().toISOString(),
    idPeticion: t['id-peticion'](p, r),
    metodo: t.method(p, r),
    ruta: t.url(p, r),
    estado: Number(t.status(p, r)),
    bytes: Number(t.res(p, r, 'content-length') ?? 0),
    duracionMs: Number(t['response-time'](p, r)),
    canal: t.canal(p, r),
  });
}

function crearRegistroHttp() {
  if (!configuracion.esProduccion) {
    return morgan('dev', { skip: (peticion) => peticion.path === '/api/salud' });
  }
  // Diagnosticos por stderr, como marca la convencion del curso.
  return morgan(formatoJson, { stream: { write: (linea) => process.stderr.write(linea) } });
}

Con esto, morgan sustituye a crearRegistroPeticiones de 06-04 (mantén el tuyo si te gusta más: hacen lo mismo, y ahora entiendes por dentro el de terceros). La opción skip evita llenar los registros con los sondeos de salud del orquestador. Qué pasa si no lo pones: cuando algo falle en producción no tendrás ni idea de qué pidió el cliente, ni cuándo, ni cuánto tardó. Depurar a ciegas.

  1. compression: ancho de banda

Qué problema resuelve. El JSON comprime extraordinariamente bien: el catálogo completo de Escena Viva puede pasar de 12 KB a menos de 2 KB, y menos bytes es menos tiempo de carga y menos coste de tránsito.

aplicacion.use(compression({
  threshold: 1024, // por debajo no compensa: la CPU cuesta mas que los bytes
  level: 6,        // equilibrio habitual entre CPU y ratio
  // Permite al cliente desactivarla con la cabecera X-Sin-Compresion.
  filter(peticion, respuesta) {
    if (peticion.headers['x-sin-compresion']) return false;
    return compression.filter(peticion, respuesta);
  },
}));
Qué comprime bien Qué no debe comprimirse
JSON, HTML, CSS, JavaScript, SVG, CSV JPEG, PNG, WebP (ya comprimidos)
Los informes de ocupación del módulo 3 MP4, MP3, ZIP, gzip
Respuestas grandes de la API Respuestas por debajo del umbral

Comprimir lo ya comprimido gasta CPU y a veces aumenta el tamaño. El filter por defecto de compression ya consulta la tabla de tipos MIME y evita esos casos.

Por qué a veces se delega en el proxy inverso

En muchos despliegues (módulo 11) la compresión la hace Nginx o el CDN, no Node. Comprimir en Node funciona en cualquier despliegue sin configuración externa y es imprescindible si no hay proxy delante; comprimir en el proxy libera CPU del proceso y suele traer brotli optimizado y caché de respuestas comprimidas, pero exige control de ese proxy.

Regla práctica: actívala en Node por defecto; si más adelante mides que la CPU es el cuello de botella y hay un proxy delante, desactívala allí, pero nunca en los dos sitios a la vez. Para comprobarla, compara curl -s -H 'Accept-Encoding: gzip' -o /dev/null -w '%{size_download}\n' localhost:3000/api/eventos con la misma llamada sin la cabecera. Qué pasa si no lo pones: nada se rompe; simplemente envías tres o cuatro veces más bytes de los necesarios en cada respuesta.

  1. express-rate-limit: límites de uso

Qué problema resuelve. Nada impide que un script llame a POST /api/pedidos en bucle: con 1189 entradas libres en el catálogo y una petición cada 20 ms, el Auditorio Ribera se queda sin aforo en menos de medio minuto. También protege de la fuerza bruta contra el inicio de sesión del módulo 8.

// src/middleware/limites.js
const { rateLimit } = require('express-rate-limit');
const cuerpo429 = (m) => ({ error: { codigo: 'DEMASIADAS_PETICIONES', mensaje: m, estado: 429 } });

/** Limite general de la API: generoso, solo frena abusos evidentes. */
const limiteGeneral = rateLimit({
  windowMs: 15 * 60 * 1000,   // ventana de 15 minutos
  limit: 300,                 // 300 peticiones por IP y ventana
  standardHeaders: 'draft-7', // cabeceras RateLimit-* estandar
  legacyHeaders: false,       // sin las antiguas X-RateLimit-*
  message: cuerpo429('Has superado el limite de peticiones. Intentalo mas tarde.'),
});

/** Limite estricto para la compra: es la operacion que consume aforo. */
const limiteCompra = rateLimit({
  windowMs: 60 * 1000, // 1 minuto
  limit: 5,            // 5 pedidos por IP y minuto
  standardHeaders: 'draft-7',
  legacyHeaders: false,
  // Los rechazados por aforo tambien cuentan: si no, un bucle de intentos
  // fallidos quedaria sin limite.
  skipFailedRequests: false,
  message: cuerpo429('Demasiados intentos de compra. Espera un minuto.'),
});

// Aplicacion: general a toda la API, estricto solo en la compra.
api.use(limiteGeneral);
rutasPedidos.post('/', limiteCompra, crearPedido);

Al superarlo, el cliente recibe un 429 Too Many Requests con RateLimit-Limit: 5, RateLimit-Remaining: 0, RateLimit-Reset: 43 y Retry-After: 43. RateLimit-Remaining le dice cuántas peticiones le quedan y Retry-After cuántos segundos esperar; un cliente bien hecho las respeta en lugar de reintentar a ciegas.

Dos limitaciones que debes conocer ahora. Primera: el almacén por defecto es la memoria del proceso, así que con varios procesos (cluster del módulo 10) o varias instancias (módulo 11) cada uno lleva su cuenta y el límite real se multiplica; la solución es un almacén compartido en Redis (módulo 10). Segunda: depende de req.ip, que a su vez depende de trust proxy; mal configurado, o todos los clientes comparten la IP del proxy y se bloquean entre sí, o cualquiera falsifica la suya con una cabecera. En el módulo 8 profundizaremos con límites por usuario autenticado, ventanas deslizantes y retardo progresivo. Qué pasa si no lo pones: un solo cliente puede agotar el aforo, saturar el proceso o probar contraseñas sin límite.

  1. cookie-parser: preparando el módulo 8

aplicacion.use(cookieParser(configuracion.clavePasarela)) interpreta la cabecera Cookie y deja las cookies en req.cookies (y las firmadas en req.signedCookies). Escena Viva todavía no lo necesita: la API es anónima, no hay sesión y ningún endpoint depende de quién eres.

Lo dejamos anunciado porque en el módulo 8 aparecerán las sesiones con Passport y las cookies httpOnly, secure y SameSite, y entonces este middleware será el primero de la lista. Instalarlo ahora "por si acaso" sería añadir superficie sin necesidad: la disciplina del módulo 5 también consiste en no instalar lo que no usas.

  1. El orden correcto: crearAplicacion() completa

Este es el código final del módulo, comentado línea a línea. El orden no es arbitrario: cada posición tiene un motivo.

// src/app.js — version completa del modulo 6.
const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const { configuracion } = require('./config/index.js');
const { crearCors } = require('./middleware/cors.js');
const { crearRegistroHttp } = require('./middleware/registro-http.js');
const { idPeticion } = require('./middleware/id-peticion.js');
const { limiteGeneral } = require('./middleware/limites.js');
const { crearRutasApi } = require('./rutas/index.js');
// rutaNoEncontrada y manejadorDeErrores llegan en 06-07.
const { rutaNoEncontrada } = require('./middleware/no-encontrado.js');
const { manejadorDeErrores } = require('./middleware/errores.js');

function crearAplicacion({ servirEstaticos = true, registrar = true, limitar = true } = {}) {
  const aplicacion = express();

  // 0. AJUSTES, antes que nada: el limitador necesita 'trust proxy' para req.ip.
  aplicacion.disable('x-powered-by');
  aplicacion.set('trust proxy', configuracion.confiarEnProxy);
  aplicacion.set('case sensitive routing', true);
  aplicacion.set('json spaces', configuracion.esProduccion ? 0 : 2);

  // 1. IDENTIFICADOR: el primero, porque el registro, los errores y cualquier
  //    traza posterior lo van a usar.
  aplicacion.use(idPeticion);

  // 2. SEGURIDAD DE CABECERAS: cuanto antes, para que TODAS las respuestas las
  //    lleven, incluidas las de error y las de los estaticos.
  aplicacion.use(helmet({
    contentSecurityPolicy: politicaCsp(),
    strictTransportSecurity: configuracion.esProduccion,
  }));

  // 3. CORS antes de las rutas y de los limites: el OPTIONS de verificacion
  //    previa debe responderse sin consumir cupo.
  aplicacion.use(crearCors());

  // 4. REGISTRO: tras el identificador (para imprimirlo) y antes de todo lo que
  //    pueda responder, para que ninguna peticion se escape.
  if (registrar) aplicacion.use(crearRegistroHttp());
  aplicacion.use(compression({ threshold: 1024 })); // 5. COMPRESION

  // 6. LIMITES antes del CUERPO (7): rechazar una peticion abusiva es mas
  //    barato si no has parseado 100 KB de JSON.
  if (limitar) aplicacion.use('/api', limiteGeneral);
  aplicacion.use(express.json({ limit: configuracion.limiteCuerpo }));

  // 8. ESTATICOS: rutas disjuntas de la API; con fallthrough, un fichero
  //    inexistente continua hacia ella.
  const estaticos = { index: 'index.html', dotfiles: 'ignore', maxAge: '1h' };
  if (servirEstaticos) aplicacion.use(express.static(configuracion.directorioPublico, estaticos));

  aplicacion.use('/api', crearRutasApi()); // 9. LA API: los routers de 06-03.
  aplicacion.use(rutaNoEncontrada);        // 10. 404: encaja con lo no atendido.
  aplicacion.use(manejadorDeErrores);      // 11. ERRORES: SIEMPRE el ultimo.

  return aplicacion;
}

Las siete reglas de orden, para memorizarlas: identificador primero (todo lo demás lo referencia); seguridad pronto (que las cabeceras lleguen también a errores y estáticos); CORS antes de los límites (el OPTIONS previo no debe consumir cupo); límites antes del cuerpo (no gastar CPU parseando lo que vas a rechazar); cuerpo antes de las rutas (req.body no existe si no); 404 después de las rutas (solo captura lo no atendido); y errores el último (solo ve lo registrado antes que él). Y las banderas registrar y limitar no son un capricho: en el módulo 9 las pondrás a false para que las pruebas no llenen la salida de registros ni fallen al superar el límite en la petición número 301.

Errores Comunes y Consejos

  • Desactivar la CSP entera porque rompe el front-end. Es tirar la única defensa real contra XSS. Saca el JavaScript en línea a un fichero o usa nonces.
  • origin: '*' con credentials: true. El navegador rechaza la combinación. Y si "lo arreglas" devolviendo el Origin recibido sin comprobarlo, has construido una lista blanca que acepta a todo el mundo.
  • Creer que CORS protege el servidor. Protege al usuario del navegador; la autorización de verdad llega en el módulo 8.
  • Poner el limitador después de express.json() (parseas 100 KB antes de rechazar) o usarlo con memoria y varios procesos (con cuatro trabajadores el límite real es cuádruple; Redis en el módulo 10).
  • Comprimir imágenes o vídeo, o comprimir en Node y en el proxy a la vez. Gastas CPU para no ganar bytes: deja el filter por defecto y elige un solo sitio.
  • Instalar cookie-parser sin usar cookies. Superficie gratis: instálalo cuando lo necesites.
  • morgan('dev') en producción. Los códigos de color son secuencias de escape que ensucian los ficheros de registro; usa combined o tu formateador JSON.
  • Consejo: añade npm audit --audit-level=high al script comprobar. Cada uno de estos paquetes es código de terceros que se ejecuta en cada petición.

Ejercicios

Ejercicio 1: auditar antes de instalar

Para los cinco paquetes de esta lección, construye una tabla con: versión actual, licencia, fecha de la última publicación, número de dependencias directas y si aparece en npm audit. Decide justificadamente si aceptarías cada uno en un proyecto real y qué alternativa buscarías si alguno llevara dos años sin publicar.

Ejercicio 2: CORS con dos orígenes

Configura cors para que acepte http://localhost:5173 y https://escenaviva.test y rechace el resto. Demuestra con curl los tres casos: la petición de verificación previa OPTIONS desde un origen permitido (debe devolver las cabeceras Access-Control-Allow-*), un GET desde un origen no permitido (no debe llevar Access-Control-Allow-Origin) y un GET sin cabecera Origin (debe funcionar con normalidad).

Ejercicio 3: el límite que salva el aforo

Aplica limiteCompra a POST /api/pedidos con 5 peticiones por minuto. Escribe un script que lance 8 pedidos seguidos de 1 entrada para ses-001-1 e imprima el estado y las cabeceras RateLimit-* de cada uno. Comprueba que las tres últimas devuelven 429 con Retry-After y que el cuerpo respeta el formato { error: { codigo, mensaje, estado } }.

Soluciones

Solución 1

for p in helmet cors morgan compression express-rate-limit; do
  echo "== $p"; npm view "$p" version license time.modified dependencies
done && npm audit --audit-level=moderate

Criterios que deberías haber aplicado: licencia permisiva, publicación en los últimos 12-18 meses, pocas dependencias directas y de mantenedores reconocibles. Si cors llevara dos años sin publicar, la respuesta razonable no es abandonarlo —es un paquete pequeño y estable— sino vigilar sus incidencias abiertas y estar dispuesto a sustituirlo por un middleware propio de veinte líneas, porque CORS al final son cuatro cabeceras.

Solución 2

# 1. Verificacion previa desde un origen permitido -> 204 con
#    Access-Control-Allow-Origin, -Allow-Methods y -Max-Age.
curl -si -X OPTIONS http://localhost:3000/api/pedidos -H 'Origin: http://localhost:5173' \
  -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: content-type'

# 2. GET desde un origen NO permitido: el grep no devuelve nada, asi que el
#    navegador ocultaria la respuesta.
curl -si http://localhost:3000/api/eventos -H 'Origin: https://malicioso.test' \
  | grep -i 'access-control-allow-origin'

# 3. Sin cabecera Origin (curl normal, servidor a servidor): 200.
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/api/eventos

El tercer caso demuestra el punto clave: CORS es una defensa del navegador, no del servidor.

Solución 3

// probar-limite.js
const PEDIDO = { sesionId: 'ses-001-1', cantidad: 1, email: '[email protected]', canal: 'web' };

async function principal() {
  for (let intento = 1; intento <= 8; intento += 1) {
    const r = await fetch('http://localhost:3000/api/pedidos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(PEDIDO),
    });
    console.log(JSON.stringify({
      intento,
      estado: r.status,
      restantes: r.headers.get('RateLimit-Remaining'),
      reintentarEn: r.headers.get('Retry-After'),
    }));
  }
}

principal().catch((error) => {
  console.error('[probar-limite]', error.message);
  process.exitCode = 1;
});

Salida esperada: los intentos 1 a 5 devuelven 201 con RateLimit-Remaining bajando de 4 a 0; el 6, el 7 y el 8 devuelven 429 con Retry-After: 52. Sin el limitador, los ocho habrían vendido entrada; con el aforo de ses-001-1, un bucle sin freno lo agota en segundos.

Conclusión

Escena Viva sale ya con el kit mínimo de una API en producción. helmet añade una decena de cabeceras de seguridad por una línea, con el matiz importante de que su CSP obliga a sacar el JavaScript en línea del front-end —y esa es la solución correcta, no desactivarla—. cors autoriza a publico/app.js a llamar a la API desde otro origen mediante una lista blanca leída de la configuración, nunca con '*', y ahora sabes qué es una petición de verificación previa y por qué credentials merece respeto. morgan registra cada petición en formato estándar y, con tokens propios, en JSON por línea listo para el módulo 11. compression recorta el tamaño de las respuestas cuando compensa. Y express-rate-limit impide que un script agote el aforo, con la advertencia de que su almacén en memoria no sobrevive a varios procesos.

Sobre todo, has visto la versión completa de crearAplicacion() con las siete reglas de orden razonadas: identificador primero, seguridad pronto, CORS antes de los límites, límites antes del cuerpo, cuerpo antes de las rutas, 404 después de las rutas y errores el último. Pero queda un agujero muy grande: POST /api/pedidos sigue haciendo registrarPedido(peticion.body) con lo que venga en el cuerpo —una cantidad de -5, un sesionId que es un objeto, un correo de 40 000 caracteres—, y ningún middleware de esta lección mira el contenido de los datos.

En la siguiente lección, Validación de Datos de Entrada, cerramos ese agujero: por qué nunca se confía en el cliente aunque el formulario valide, dónde debe vivir la validación (en el borde, no en el dominio), los esquemas de zod para los pedidos de Escena Viva, un middleware genérico validar(esquema, origen) que deja el resultado en req.datosValidados —y que respeta que req.query sea de solo lectura—, y cómo devolver un 400 que de verdad ayude a quien llama sin revelar lo que no debe.

Curso de Node.js: De Principiante a Avanzado

Módulo 1: Introducción a Node.js

Módulo 2: Conceptos Básicos

Módulo 3: Sistema de Archivos y E/S

Módulo 4: HTTP y Servidores Web

Módulo 5: NPM y Gestión de Paquetes

Módulo 6: Framework Express.js

Módulo 7: Bases de Datos y ORMs

Módulo 8: Autenticación y Autorización

Módulo 9: Pruebas y Depuración

Módulo 10: Temas Avanzados

Módulo 11: Despliegue y DevOps

Módulo 12: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados