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
- El criterio del módulo 5 aplicado a cada paquete
- helmet: cabeceras de seguridad
- cors: peticiones desde otro origen
- morgan: registro HTTP
- compression: ancho de banda
- express-rate-limit: límites de uso
- cookie-parser: preparando el módulo 8
- El orden correcto:
crearAplicacion()completa
- 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 conocidasResultado 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.
- 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 servimospublico/.
- 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: 600Y 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.
- 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.
- 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.
- 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.
- 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.
- El orden correcto:
crearAplicacion() completa
crearAplicacion() completaEste 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: '*'concredentials: true. El navegador rechaza la combinación. Y si "lo arreglas" devolviendo elOriginrecibido 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
filterpor defecto y elige un solo sitio. - Instalar
cookie-parsersin 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; usacombinedo tu formateador JSON.- Consejo: añade
npm audit --audit-level=highal scriptcomprobar. 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=moderateCriterios 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/eventosEl 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
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
