La aplicación de Escena Viva tiene ahora una estructura sólida pero una sola ruta. En esta lección la llenamos: recuperamos los cuatro endpoints de la API del módulo 4 (/salud, /eventos, /eventos/:id, /eventos/:id/sesiones), añadimos las sesiones y los pedidos, y los organizamos en routers modulares con controladores separados. Por el camino compararemos cada mecanismo de Express con el enrutador.js que escribiste a mano, porque la mejor forma de entender app.get('/eventos/:id') es recordar lo que costaba hacerlo sin framework.

Contenido

  1. Métodos de ruta y anatomía de un manejador
  2. Parámetros de ruta y req.params
  3. Parámetros opcionales, comodines y expresiones regulares
  4. Los otros datos de la petición: query, body, headers, ip
  5. express.Router(): routers modulares
  6. Reorganizar la API de Escena Viva
  7. Controladores separados
  8. router.route() y router.param()
  9. El orden importa
  10. Los métodos de res

  1. Métodos de ruta y anatomía de un manejador

Express expone un método por cada verbo HTTP, más all para todos:

aplicacion.get('/eventos', manejador);        // leer una coleccion
aplicacion.post('/pedidos', manejador);       // crear un recurso
aplicacion.put('/sesiones/:id', manejador);   // reemplazar por completo
aplicacion.patch('/sesiones/:id', manejador); // modificar parcialmente
aplicacion.delete('/pedidos/:id', manejador); // eliminar
aplicacion.all('/api/*resto', manejador);     // cualquier metodo

Cada uno registra una entrada en la pila interna de Express: un patrón, un método y una o varias funciones. Es literalmente lo que hacía tu enrutador.registrar(metodo, patron, manejador), con la diferencia de que Express admite varias funciones por ruta: en aplicacion.post('/pedidos', validarCuerpo, comprobarAforo, crearPedido) las tres se ejecutan en orden y cada una decide si continúa. Eso ya es middleware, y es el tema de 06-04.

Anatomía de un manejador

// peticion: IncomingMessage extendido; respuesta: ServerResponse extendido.
function manejador(peticion, respuesta, siguiente) {
  // Tres salidas posibles:
  // 1. Responder: respuesta.json(...) / respuesta.send(...) — la cadena termina.
  // 2. Continuar: siguiente() — pasa al siguiente manejador que encaje.
  // 3. Fallar: siguiente(error) — salta al manejador de errores (06-07).
  respuesta.json({ estado: 'ok' });
}

Una regla de oro: cada camino de ejecución debe terminar en exactamente una de esas tres cosas. Si un if no responde ni llama a siguiente(), la petición se queda colgada hasta que el cliente se rinde. Lo diagnosticaremos en 06-04.

  1. Parámetros de ruta y req.params

aplicacion.get('/eventos/:id', (peticion, respuesta) => {
  respuesta.json({ idSolicitado: peticion.params.id });
});
// curl -s localhost:3000/eventos/evt-002  ->  {"idSolicitado":"evt-002"}

Compáralo con lo que hacía tu módulo 4. compilarPatron('/eventos/:id') recorría el patrón, sustituía cada :nombre por un grupo de captura, guardaba los nombres en un array y construía la regex /^\/eventos\/([^/]+)$/; después emparejar la ejecutaba sobre la ruta y reconstruía a mano el objeto { id: 'evt-002' }. Express hace lo mismo, con diferencias que importan:

Aspecto Tu compilarPatron Express (path-to-regexp)
Descodificación de la URL Llamabas a decodeURIComponent tú Automática: /eventos/Sala%20B%C3%B3veda llega descodificado
Caracteres especiales y casos límite Escapado a mano; riesgo de regex catastrófica Escapado seguro y patrones acotados y probados
Múltiples parámetros Funcionaba, pero el código crecía /eventos/:idEvento/sesiones/:idSesion sin coste

Varios parámetros en una ruta:

aplicacion.get('/eventos/:idEvento/sesiones/:idSesion', (peticion, respuesta) => {
  const { idEvento, idSesion } = peticion.params;
  respuesta.json({ idEvento, idSesion });
});
// GET /eventos/evt-001/sesiones/ses-001-2
//   -> {"idEvento":"evt-001","idSesion":"ses-001-2"}

Importante: req.params siempre contiene cadenas. Si esperas un número, conviértelo y valídalo. En 06-06 lo haremos con esquemas en vez de a mano.

  1. Parámetros opcionales, comodines y expresiones regulares

Aquí es donde Express 5 rompió con Express 4; presta atención si te encuentras código antiguo.

Necesidad Express 4 Express 5
Parámetro opcional /eventos/:id? /eventos{/:id}
Comodín /archivos/* → req.params[0] Anónimo no permitido (lanza error); con nombre, /archivos/*ruta → req.params.ruta
Segmento repetible /:seccion+ /*seccion (captura el resto)
// Parametro opcional: responde a /eventos y a /eventos/evt-001.
aplicacion.get('/eventos{/:id}', (peticion, respuesta) => {
  const { id } = peticion.params;
  respuesta.json(id === undefined ? { tipo: 'coleccion' } : { tipo: 'elemento', id });
});

// Comodin con nombre: GET /descargas/informes/2026/ocupacion.csv
//   -> peticion.params.ruta === 'informes/2026/ocupacion.csv'
aplicacion.get('/descargas/*ruta', (peticion, respuesta, siguiente) => {
  try {
    // El comodin es la puerta clasica al recorrido de directorios:
    // reutilizamos resolverDentroDe del modulo 3.
    respuesta.sendFile(resolverDentroDe(DIRECTORIO_INFORMES, peticion.params.ruta));
  } catch (error) {
    siguiente(error); // codigo RUTA_NO_PERMITIDA -> 403
  }
});

Fíjate en la lección que se repite: el comodín es cómodo y peligroso. Que Express extraiga la ruta por ti no significa que sea segura; resolverDentroDe sigue siendo imprescindible.

Para casos que la sintaxis de segmentos no cubre, se admite una expresión regular como patrón; con ella, las capturas llegan por índice numérico: aplicacion.get(/^\/eventos\/(evt-\d{3})$/, (p, r) => r.json({ id: p.params[0] })). Se puede, pero úsalo con moderación: una regex en la ruta es difícil de leer y esconde una validación donde nadie la busca. Es mejor aceptar :id y validar el formato con un esquema (06-06), porque así el error que devuelves es explicativo en vez de un 404 mudo.

  1. Los otros datos de la petición

Propiedad Contenido Notas
req.params Parámetros de ruta Siempre cadenas, ya descodificadas
req.query Query string interpretada Solo lectura en Express 5
req.body Cuerpo interpretado undefined sin express.json() (06-04)
req.headers / req.get(n) Cabeceras en minúsculas / una cabecera req.get('Content-Type') ignora mayúsculas
req.ip, req.protocol, req.secure IP y esquema del cliente Dependen de trust proxy (06-02)
req.method, req.path, req.originalUrl Método y ruta originalUrl conserva la URL completa antes de montajes
// GET /api/eventos?sala=Sala%20Boveda&pagina=2&ordenar=fecha
aplicacion.get('/api/eventos', (peticion, respuesta) => {
  const { sala, pagina = '1', ordenar = 'fecha' } = peticion.query;
  respuesta.json({ sala, pagina: Number.parseInt(pagina, 10), ordenar });
});

El error clásico al migrar a Express 5

En Express 4, req.query era una propiedad normal y mucho código "normalizaba" la query reasignándola:

// FUNCIONABA en Express 4 y FALLA en Express 5.
peticion.query = normalizar(peticion.query); // TypeError: Cannot set property query

// Correcto en Express 5: el resultado normalizado va a otra propiedad.
peticion.consulta = normalizar(peticion.query);

En Express 5, req.query es un getter que interpreta la query string bajo demanda y la memoriza; no tiene setter. Ese es exactamente el patrón que usará el middleware validar() de 06-06 con req.datosValidados.

  1. express.Router(): routers modulares

Un Router es una mini-aplicación: tiene sus rutas y su middleware, pero no escucha en ningún puerto. Se monta en la aplicación (o en otro router) bajo un prefijo.

const rutasEventos = express.Router();
rutasEventos.get('/', listarEventos);      // ruta relativa
rutasEventos.get('/:id', obtenerEvento);   // ruta relativa
// El prefijo se decide al montar, no al declarar.
aplicacion.use('/api/eventos', rutasEventos);
// Resultado: GET /api/eventos y GET /api/eventos/:id

Las ventajas frente a declarar todo en app.js: rutas relativas (el router no sabe dónde está montado, así que cambiar /api/eventos por /api/v2/eventos es una línea), middleware acotado (rutasPedidos.use(limitador) afecta solo a los pedidos), ficheros pequeños (un router por recurso, en vez de un app.js interminable) y reutilización (el mismo router se puede montar dos veces bajo prefijos distintos).

  1. Reorganizar la API de Escena Viva

// src/rutas/index.js — punto unico de montaje.
const express = require('express');
const { rutasEventos } = require('./eventos.js');
const { rutasSesiones } = require('./sesiones.js');
const { rutasPedidos } = require('./pedidos.js');
function crearRutasApi() {
  const api = express.Router();
  api.get('/salud', (p, r) => r.json({ estado: 'ok', instante: new Date().toISOString() }));
  api.use('/eventos', rutasEventos);
  api.use('/sesiones', rutasSesiones);
  api.use('/pedidos', rutasPedidos);
  return api;
}

module.exports = { crearRutasApi };
// src/rutas/eventos.js
const express = require('express');
const cc = require('../controladores/eventos.js');

const rutasEventos = express.Router();

// Se ejecuta una sola vez por peticion que traiga :id, antes del manejador.
rutasEventos.param('id', cc.cargarEvento);
rutasEventos.get('/', cc.listarEventos);
rutasEventos.get('/:id', cc.obtenerEvento);
rutasEventos.get('/:id/sesiones', cc.listarSesionesDelEvento);

module.exports = { rutasEventos };

// src/rutas/sesiones.js y src/rutas/pedidos.js siguen el mismo patron.
rutasSesiones.get('/:idSesion', obtenerSesion);
rutasSesiones.get('/:idSesion/precios', obtenerPreciosDeSesion);
rutasPedidos.route('/').post(crearPedido);
rutasPedidos.route('/:idPedido').get(obtenerPedido);

// Y el montaje en src/app.js:
aplicacion.use(express.json({ limit: configuracion.limiteCuerpo }));
aplicacion.use('/api', crearRutasApi());
aplicacion.use(express.static(configuracion.directorioPublico, { index: 'index.html' }));

Toda la API cuelga de /api y el front-end estático de publico/ se sirve en la raíz. Un solo use decide el versionado de la API entera.

  1. Controladores separados

El controlador traduce HTTP a dominio. No contiene reglas de negocio: llama al dominio y decide el código de estado.

// src/controladores/eventos.js
const { obtenerCatalogo, obtenerEventoPorId } = require('../catalogo-datos.js');

/** GET /api/eventos — listado resumido del catalogo. */
async function listarEventos(peticion, respuesta) {
  const eventos = (await obtenerCatalogo()).map((e) => ({
    id: e.id, titulo: e.titulo, sala: e.sala, numeroSesiones: e.numeroSesiones,
    aforoTotal: e.aforoTotal, entradasVendidas: e.entradasVendidas, agotado: e.agotado,
  }));
  respuesta.json({ total: eventos.length, eventos });
}

// Middleware de router.param: carga el evento una sola vez. Si no existe,
// propaga el error de dominio (EVENTO_NO_ENCONTRADO -> 404).
async function cargarEvento(peticion, respuesta, siguiente, valorId) {
  peticion.evento = await obtenerEventoPorId(valorId);
  siguiente();
}

/** GET /api/eventos/:id — el evento ya viene cargado por cargarEvento. */
const obtenerEvento = (peticion, respuesta) => respuesta.json(peticion.evento.toJSON());

/** GET /api/eventos/:id/sesiones */
function listarSesionesDelEvento(peticion, respuesta) {
  const { id, numeroSesiones, sesiones } = peticion.evento;
  respuesta.json({
    idEvento: id, total: numeroSesiones,
    sesiones: sesiones.map((s) => ({
      id: s.id, inicio: s.inicio, aforo: s.aforo, libres: s.libres,
      agotada: s.agotada, precioCentimos: s.precioCentimos,
    })),
  });
}

module.exports = { listarEventos, cargarEvento, obtenerEvento, listarSesionesDelEvento };

Tres cosas que no verás en este fichero, y es deliberado: no hay try/catch (en Express 5, si obtenerEventoPorId rechaza con código EVENTO_NO_ENCONTRADO, ese rechazo llega solo al manejador de errores, que lo traducirá a 404 con la tabla ESTADO_POR_CODIGO del módulo 4; ver 06-07); no hay lectura de ficheros, que es de catalogo-datos.js; y no hay reglas de aforo, que son de dominio/.

// src/controladores/pedidos.js — POST /api/pedidos
async function crearPedido(peticion, respuesta) {
  // req.body existe gracias a express.json() (06-04);
  // en 06-06 lo sustituiremos por req.datosValidados.
  const pedido = await registrarPedido(peticion.body);
  respuesta.status(201).location(`/api/pedidos/${pedido.id}`).json(pedido.toJSON());
}

  1. router.route() y router.param()

router.route()

Encadena varios métodos sobre la misma ruta sin repetir el patrón: rutasSesiones.route('/:idSesion').get(obtenerSesion).patch(actualizarSesion).delete(cancelarSesion).

Ventajas: el patrón se escribe una vez (menos riesgo de que una copia se desincronice) y queda visualmente claro qué operaciones admite el recurso. Además, Express genera automáticamente el 405 Method Not Allowed con la cabecera Allow correcta para los métodos no registrados de esa ruta — exactamente lo que tú programaste a mano en enrutador.js.

router.param()

rutasEventos.param('id', cargarEvento) registra un middleware que se ejecuta una vez por petición cuando la ruta contiene ese parámetro, antes de cualquier manejador. Con ello, las tres rutas /, /:id y /:id/sesiones comparten la carga del evento sin repetir una línea, y los controladores reciben req.evento ya resuelto.

A favor En contra
Elimina duplicación en rutas que comparten parámetro y deja los controladores muy limpios El controlador depende de algo que no se ve en su fichero
Punto único para el 404 del recurso Se ejecuta también donde quizá no lo necesitas, y confunde si dos routers usan el mismo nombre con semánticas distintas

Cuidado importante: router.param() es local al router donde se registra. Si montas rutasSesiones en otro sitio, su param no se hereda. Y si el mismo nombre (:id) significa cosas distintas en dos routers, usa nombres distintos (:idEvento, :idSesion) como hemos hecho aquí.

  1. El orden importa

Express recorre su pila en el orden de registro y se queda con la primera entrada que encaje en método y ruta. Esto explica el 90 % de los "mi ruta no se ejecuta".

// MAL: el comodin se declara primero y se come todo lo demas.
rutasEventos.get('/*resto', servirPaginaGenerica);
rutasEventos.get('/destacados', listarDestacados);   // NUNCA se ejecuta
rutasEventos.get('/:id', obtenerEvento);             // NUNCA se ejecuta

// BIEN: de lo mas especifico a lo mas generico.
rutasEventos.get('/destacados', listarDestacados);   // literal
rutasEventos.get('/:id', obtenerEvento);             // parametro
rutasEventos.get('/*resto', servirPaginaGenerica);   // comodin

Con el orden malo, una petición a /api/eventos/destacados encaja con /*resto, que responde, y la cadena termina ahí: las otras dos rutas son código muerto. La regla práctica, en orden de declaración: primero las rutas literales (/destacados, /salud), después las que llevan parámetro (/:id), luego las de comodín (/*resto) y al final el 404. Es el mismo problema que ya tenías en el módulo 4: tu enrutador.js también recorría el array de rutas en orden. Express no lo resuelve por ti; hereda la misma regla.

La ruta 404 final

// Al final de src/app.js, despues de todas las rutas y estaticos.
aplicacion.use((peticion, respuesta, siguiente) => {
  const mensaje = `No existe la ruta ${peticion.method} ${peticion.originalUrl}`;
  siguiente(Object.assign(new Error(mensaje), { codigo: 'RUTA_NO_ENCONTRADA' }));
});

Es un use sin ruta, así que encaja con todo lo que haya llegado hasta ahí sin ser atendido. No responde: propaga un error, para que el manejador central de 06-07 dé la respuesta en el formato { error: { codigo, mensaje, estado } } como cualquier otro error.

  1. Los métodos de res

Método Qué hace Qué añade sobre node:http
res.json(objeto) Serializa y envía JSON Fija Content-Type y Content-Length, aplica json spaces, calcula ETag
res.send(valor) Envía cadena, Buffer u objeto Deduce el Content-Type según el tipo del argumento
res.status(codigo) Fija el estado Devuelve res, así que se encadena
res.sendStatus(codigo) Estado + cuerpo con el texto del estado res.sendStatus(204) responde en una línea
res.location(url) Fija la cabecera Location Codifica la URL correctamente
res.redirect([codigo], url) Location + estado + cuerpo Por defecto 302; usa 301 o 308 para permanentes
res.sendFile(rutaAbsoluta) Envía un fichero ETag, Content-Type, rangos y streaming: es tu estaticos.js
res.set(n, v) / res.type(t) Cabeceras y Content-Type set admite un objeto; type acepta atajos ('json')
res.end() Termina la respuesta El mismo de node:http, sin extras
// Ejemplos concretos en Escena Viva.
respuesta.status(201).location(`/api/pedidos/${pedido.id}`).json(pedido.toJSON());
respuesta.sendStatus(204); // cancelacion aceptada, sin cuerpo

// Redireccion permanente de una URL antigua.
aplicacion.get('/eventos/:id', (p, r) => r.redirect(301, `/api/eventos/${p.params.id}`));

// Varias cabeceras de una vez.
respuesta.set({ 'Cache-Control': 'public, max-age=60', 'X-Sala': 'Teatro Almendra' });

Comprueba la equivalencia con el módulo 4: curl -si localhost:3000/api/eventos/evt-001 devuelve 200 OK con Content-Type: application/json; charset=utf-8, Content-Length y un ETag: W/"19c-...". Ese ETag lo calculaba tu estaticos.js a mano con un hash; res.json() lo hace por defecto para cualquier respuesta.

Errores Comunes y Consejos

  • Declarar /:id antes que /destacados. El parámetro captura la palabra literal y la ruta específica queda muerta. De lo específico a lo genérico, siempre.
  • Usar * sin nombre en Express 5. Ya no está permitido: la aplicación falla al arrancar. Migra a *nombre.
  • Reasignar req.query. TypeError en Express 5; guarda el resultado normalizado en otra propiedad.
  • Esperar que req.body exista sin express.json(). Será undefined y obtendrás un TypeError: el fallo más común al escribir el primer POST.
  • Meter lógica de negocio en el controlador. Si calcula ocupaciones o valida aforos, esa lógica no se puede reutilizar desde los informes ni probar sin HTTP.
  • Olvidar que router.param() es local al router: no se hereda al montar en otro sitio.
  • Confiar en un :id sin validar. Llega como cadena y puede ser cualquier cosa. Un 404 mudo no ayuda al cliente; en 06-06 devolveremos un 400 explicativo.
  • Consejo: en desarrollo, imprime la tabla de rutas al arrancar recorriendo aplicacion.router.stack. Ver el orden real de registro cura muchos misterios.

Ejercicios

Ejercicio 1: filtrado y paginación por query

Amplía GET /api/eventos para que admita ?sala=, ?agotado=true|false, ?pagina= y ?porPagina= (por defecto 1 y 10, máximo 50). Devuelve { pagina, porPagina, total, totalPaginas, eventos }. No reasignes req.query. Con los datos semilla, ?porPagina=2 debe devolver 2 páginas para los 3 eventos.

Ejercicio 2: router de sesiones con route() y param()

Crea src/rutas/sesiones.js con un router.param('idSesion', cargarSesion) que busque la sesión recorriendo el catálogo y la deje en req.sesion (propagando SESION_NO_ENCONTRADA si no existe), y expón con router.route('/:idSesion') un GET que devuelva el detalle. Comprueba que PUT /api/sesiones/ses-001-1 devuelve 405 con la cabecera Allow.

Ejercicio 3: el orden que rompe

Escribe una aplicación con tres rutas mal ordenadas (/*resto, /destacados, /:id), demuestra con curl que solo se ejecuta la primera, reordénalas y vuelve a probar. Añade además un endpoint /depuracion/rutas que devuelva la lista de rutas registradas en su orden real.

Soluciones

Solución 1

// src/controladores/eventos.js (fragmento)
const MAXIMO_POR_PAGINA = 50;

async function listarEventos(peticion, respuesta) {
  // Lectura sin reasignar req.query; toda la conversion es manual.
  const { sala, agotado } = peticion.query;
  const pagina = Math.max(1, Number.parseInt(peticion.query.pagina ?? '1', 10) || 1);
  const bruto = Number.parseInt(peticion.query.porPagina ?? '10', 10) || 10;
  const porPagina = Math.min(MAXIMO_POR_PAGINA, Math.max(1, bruto));

  let eventos = await obtenerCatalogo();
  if (sala) eventos = eventos.filter((e) => e.sala === sala);
  if (agotado === 'true' || agotado === 'false') {
    eventos = eventos.filter((e) => e.agotado === (agotado === 'true'));
  }

  const total = eventos.length;
  const desde = (pagina - 1) * porPagina;
  respuesta.json({
    pagina, porPagina, total,
    totalPaginas: Math.max(1, Math.ceil(total / porPagina)),
    eventos: eventos.slice(desde, desde + porPagina).map((e) => e.toJSON()),
  });
}

Toda esta conversión manual —parseInt, topes, ?? '1'— es justo lo que sustituiremos por un esquema en 06-06.

Solución 2

// src/controladores/sesiones.js
async function cargarSesion(peticion, respuesta, siguiente, valorId) {
  const catalogo = await obtenerCatalogo();
  for (const evento of catalogo) {
    const sesion = evento.buscarSesion(valorId);
    if (sesion) {
      peticion.sesion = sesion;
      peticion.eventoDeLaSesion = evento;
      siguiente();
      return;
    }
  }
  const mensaje = `No existe la sesion ${valorId}`;
  siguiente(Object.assign(new Error(mensaje), { codigo: 'SESION_NO_ENCONTRADA' }));
}

const obtenerSesion = (peticion, respuesta) =>
  respuesta.json({ ...peticion.sesion, idEvento: peticion.eventoDeLaSesion.id });

module.exports = { cargarSesion, obtenerSesion };

Y curl -si -X PUT localhost:3000/api/sesiones/ses-001-1 devuelve 405 Method Not Allowed con Allow: GET, HEAD, generado por Express sin que tú escribas nada.

Solución 3

// orden.js — version que falla; reordena para comparar.
const aplicacion = require('express')();
aplicacion.get('/*resto', (p, r) => r.json({ ruta: 'comodin' }));
aplicacion.get('/destacados', (p, r) => r.json({ ruta: 'destacados' }));
aplicacion.get('/:id', (p, r) => r.json({ ruta: 'parametro' }));

// La lista de rutas en su orden real se saca de la pila interna.
aplicacion.get('/depuracion/rutas', (peticion, respuesta) => {
  respuesta.json(
    aplicacion.router.stack.filter((c) => c.route).map((c) => c.route.path)
  );
});

aplicacion.listen(3000);

Con ese orden, curl -s localhost:3000/destacados y curl -s localhost:3000/evt-001 devuelven los dos {"ruta":"comodin"}. Tras reordenar (destacados, :id, *resto) devuelven {"ruta":"destacados"} y {"ruta":"parametro"}. Nota: /depuracion/rutas tampoco se alcanza mientras el comodín esté declarado antes; es el propio ejercicio demostrándose a sí mismo.

Conclusión

Escena Viva ya tiene una API organizada: tres routers montados bajo /api, controladores que solo traducen HTTP a dominio, router.param() cargando el evento una sola vez y dejándolo en req.evento, y router.route() agrupando los métodos de cada recurso con el 405 y su cabecera Allow gratis. Has visto que :id y req.params son tu compilarPatron mejor resuelto, que los comodines de Express 5 exigen nombre y siguen necesitando resolverDentroDe, que req.query es de solo lectura y por qué eso rompe código antiguo, y que el orden de declaración de las rutas es una regla que Express hereda, no que resuelve.

Queda un cabo suelto muy visible: POST /api/pedidos lee req.body, y req.body no existe por sí solo: aparece únicamente si antes de la ruta se ha ejecutado express.json(). Ese "antes de la ruta" es la puerta al concepto central del framework. En la siguiente lección, Middleware, veremos qué es exactamente esa cadena de funciones (req, res, next) que atraviesa cada petición: cómo se recorre, qué significa next(), next(error) y —el error más frustrante de Express— no llamar a next() en absoluto. Escribiremos los middleware propios de Escena Viva (registro con duración, identificador de petición, control de caché) y desmontaremos express.json() y express.static() comparándolos con el cuerpo.js y el estaticos.js que escribiste a mano.

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