Ya sabemos leer una petición y construir una respuesta correcta. Falta la decisión que hay en medio: qué código atiende cada petición. Eso es enrutar.

Un enrutador no es más que una función que, dada una pareja (método, ruta), encuentra el manejador que le corresponde. Suena trivial, y el primer intento cabe en diez líneas de if/else. El problema es que ese primer intento se desmorona en cuanto aparece la primera ruta con un identificador dentro, /eventos/evt-001, y a partir de ahí cada ruta nueva empeora el desorden. En esta lección construimos un enrutador de verdad: una tabla de rutas, patrones tipo /eventos/:id compilados a expresiones regulares, extracción de parámetros, 404 cuando la ruta no existe, 405 con cabecera Allow cuando existe pero con otro método, y un try/catch central que traduce nuestros error.codigo a estados HTTP usando la tabla de la lección anterior. Es la primera vez que aparece la idea de manejador de errores centralizado, que Express formalizará en la lección 06-07.

Contenido

  1. Qué es enrutar exactamente
  2. El intento ingenuo: if/else sobre req.url
  3. Dónde se rompe: rutas con parámetros
  4. La tabla de rutas
  5. compilarPatron: de /eventos/:id a expresión regular
  6. Normalizar la ruta antes de casarla
  7. Casar, extraer parámetros y responder 404 o 405
  8. Despacho asíncrono y manejo de errores centralizado
  9. Las rutas de la API de Escena Viva
  10. Por qué a partir del Módulo 6 usaremos Express

  1. Qué es enrutar exactamente

Enrutar es casar una pareja (método, ruta) con un manejador. Nada más. Toda la complejidad de los enrutadores reales —Express, Fastify, Koa— nace de tres exigencias añadidas, y un enrutador que las resuelva ya es útil de verdad:

  • Los patrones tienen partes variables. /eventos/evt-001 y /eventos/evt-002 deben ir al mismo manejador, que necesita saber cuál de los dos le ha tocado.
  • El método forma parte de la identidad de la ruta. GET /pedidos (listar) y POST /pedidos (crear) son cosas distintas con el mismo camino.
  • Los fallos tienen que distinguirse. No es lo mismo "esa ruta no existe" (404) que "esa ruta existe, pero no con ese método" (405).

  1. El intento ingenuo: if/else sobre req.url

Así empieza todo el mundo, y para dos rutas está perfectamente bien:

async function manejarPeticion(req, res) {
  if (req.method === 'GET' && req.url === '/salud') return responderJson(res, 200, { estado: 'ok' });
  if (req.method === 'GET' && req.url === '/eventos') return responderJson(res, 200, await obtenerCatalogo());
  return responderError(res, 404, 'Ruta no encontrada', 'RECURSO_NO_ENCONTRADO');
}

Fíjate en los return: son la disciplina de la lección anterior, la que evita ERR_HTTP_HEADERS_SENT. Y este código ya tiene un fallo silencioso: compara con req.url en vez de con el pathname. GET /eventos?sala=Sala%20B%C3%B3veda no casa con '/eventos', porque la cadena incluye la consulta. El resultado es un 404 desconcertante que aparece solo cuando alguien añade un filtro. La corrección es la de 04-02: parsear con new URL y comparar contra url.pathname.

  1. Dónde se rompe: rutas con parámetros

Ahora añade GET /eventos/evt-001. Como el identificador es variable, la comparación por igualdad ya no sirve, y aparece el troceo manual:

// El camino hacia el desastre
const partes = url.pathname.split('/');            // ['', 'eventos', 'evt-001']

if (req.method === 'GET' && partes[1] === 'eventos' && partes.length === 3) {
  return manejarEvento(req, res, partes[2]);
}
if (req.method === 'GET' && partes[1] === 'eventos' && partes[3] === 'sesiones') {
  return manejarSesiones(req, res, partes[2]);
}

Con cuatro rutas es soportable. Con veinte es ilegible, y por el camino te encontrarás con problemas muy concretos:

Problema Síntoma
Índices mágicos (partes[2]) Nadie recuerda qué es el 2; añadir un prefijo /api lo rompe todo
Condiciones de longitud partes.length === 3 se olvida y /eventos/x/y/z casa por accidente
El método se repite en cada rama Un DELETE /eventos cae en el 404 genérico en vez de dar 405
Sin decodificar, y orden frágil Un %20 llega con el porcentaje puesto; mover un if deja una ruta inalcanzable

La solución no es escribir mejores if: es separar la declaración de las rutas de la lógica que las casa. Declaras una tabla; un motor genérico la recorre.

  1. La tabla de rutas

Una ruta es un objeto con tres campos —{ metodo: 'GET', patron: '/eventos/:id', manejador: obtenerEvento }— y el enrutador es una lista de ellos más dos operaciones: registrar y despachar.

Con esa forma, añadir una ruta es añadir una fila, y el motor no cambia nunca. Este es el camino completo de una petición, y todo lo que viene a continuación es rellenar esas cajas:

flowchart LR
  A[request] --> B[normalizar ruta]
  B --> C{buscar en la tabla}
  C -->|casa metodo y patron| D[manejador con parametros]
  C -->|casa patron, no metodo| E[405 + Allow]
  C -->|no casa nada| F[404]
  D -->|lanza error| G[try/catch central]
  G --> H[estadoParaError]

  1. compilarPatron: de /eventos/:id a expresión regular

Necesitamos convertir la cadena '/eventos/:id' en algo que sepa decir si '/eventos/evt-001' casa y, además, extraer evt-001. La herramienta natural es una expresión regular con grupos de captura. Se construye en tres pasos:

// Compila un patron tipo '/eventos/:id/sesiones' a una expresion regular
// y la lista ordenada de nombres de sus parametros.
function compilarPatron(patron) {
  const nombres = [];

  // Paso 1: escapar los caracteres con significado especial en una regex.
  // Un patron como '/informes/2026.json' tiene un punto que debe ser literal.
  const escapado = patron.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

  // Paso 2: sustituir cada :nombre por un grupo de captura de UN segmento.
  const fuente = escapado.replace(/:([a-zA-Z][a-zA-Z0-9]*)/g, (_, nombre) => {
    nombres.push(nombre);
    return '([^/]+)';
  });

  // Paso 3: anclar. Sin ^ y $ la ruta casaria por trozos.
  return { expresion: new RegExp(`^${fuente}$`), nombres };
}

Merece la pena leer despacio la expresión resultante. Para '/eventos/:id/sesiones' obtenemos:

compilarPatron('/eventos/:id/sesiones');
// { expresion: /^\/eventos\/([^\/]+)\/sesiones$/, nombres: ['id'] }

Pieza a pieza:

Fragmento Qué significa
^ … $ La ruta empieza y termina aquí: no vale casar por el medio
\/eventos\/ Texto literal (la barra no necesita escape con new RegExp, pero no molesta)
( … ) Grupo de captura: lo que case aquí se podrá recuperar después
[^/]+ Uno o más caracteres que no sean barra: nunca cruza a otro segmento

Los dos detalles críticos son [^/]+ y los anclajes. [^/]+ en vez de .+: con .+, el patrón /eventos/:id casaría con /eventos/evt-001/sesiones y id valdría 'evt-001/sesiones', atropellando otra ruta. Y sin ^ y $, /eventos casaría dentro de /admin/eventos/borrar, lo que además de un error funcional es un agujero de seguridad.

También importa + en vez de *: con *, /eventos/ (barra final, sin identificador) casaría con id valiendo cadena vacía, y acabarías buscando el evento ''.

Con el patrón compilado, casar una ruta y extraer los parámetros es directo:

function emparejar(compilado, ruta) {
  const coincidencia = compilado.expresion.exec(ruta);
  if (coincidencia === null) return null;

  const parametros = {};
  compilado.nombres.forEach((nombre, indice) => {
    // El grupo 0 es la coincidencia completa; los parametros empiezan en 1.
    // Se decodifica AQUI, un segmento cada vez (ver leccion 04-02).
    parametros[nombre] = decodeURIComponent(coincidencia[indice + 1]);
  });

  return parametros;
}

Devolver null frente a {} es deliberado: un patrón sin parámetros que casa devuelve un objeto vacío, que es distinto de "no casa". Confundir ambos con un if (!parametros) sería un error, porque {} es un valor verdadero pero null no.

  1. Normalizar la ruta antes de casarla

La misma página se puede pedir de varias formas, y todas deben llevar al mismo sitio:

Petición Problema Decisión
/eventos/ Barra final sobrante Quitarla (salvo en la raíz /)
/EVENTOS Mayúsculas No cambiarlas: las rutas distinguen mayúsculas por norma
/eventos//evt-001 Barra duplicada Colapsarla en una
/eventos%2Fevt-001 Barra codificada No decodificar la ruta entera: se hace por segmentos
/eventos?sala=X Consulta incluida Casar contra pathname, no contra req.url
// Deja la ruta en forma canonica para poder casarla de forma estable.
function normalizarRuta(pathname) {
  const colapsada = pathname.replace(/\/{2,}/g, '/');
  return colapsada.length > 1 ? colapsada.replace(/\/+$/, '') : colapsada;
}

Sobre las mayúsculas, la norma es clara y contraintuitiva: el dominio no distingue mayúsculas, la ruta sí. /Eventos y /eventos son recursos distintos. Bajar la ruta a minúsculas "por comodidad" parece amable hasta que un identificador legítimo lleva mayúsculas (EV-2026-000123) y lo destrozas. Si quieres ser tolerante, hazlo con una redirección 301 a la forma canónica, no casando en silencio: por la misma razón la barra final se normaliza en vez de registrar dos rutas, porque un mismo contenido viviendo en dos direcciones confunde a las cachés y penaliza en los buscadores.

  1. Casar, extraer parámetros y responder 404 o 405

Ya tenemos todas las piezas. Este es src/servidor/enrutador.js completo:

// src/servidor/enrutador.js
// Enrutador minimo: una tabla de { metodo, patron, manejador } y un
// despachador que casa (metodo, ruta), extrae parametros y centraliza errores.

const { responderError } = require('./respuestas.js');

// compilarPatron, emparejar y normalizarRuta: vistos en los apartados 5 y 6.

function crearEnrutador() {
  const rutas = [];

  function registrar(metodo, patron, manejador) {
    rutas.push({ metodo, patron, compilado: compilarPatron(patron), manejador });
    return { registrar, get, post, despachar };   // encadenable
  }

  const get = (patron, manejador) => registrar('GET', patron, manejador);
  const post = (patron, manejador) => registrar('POST', patron, manejador);

  async function despachar(req, res) {
    const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);
    const ruta = normalizarRuta(url.pathname);

    // HEAD se sirve con el manejador de GET: Node ya descarta el cuerpo.
    const metodo = req.method === 'HEAD' ? 'GET' : req.method;
    const metodosPermitidos = new Set();   // para poder dar un 405 util

    for (const registro of rutas) {
      const parametros = emparejar(registro.compilado, ruta);
      if (parametros === null) continue;   // ni siquiera el camino coincide

      metodosPermitidos.add(registro.metodo);
      if (registro.metodo === metodo) {
        return registro.manejador(req, res, { parametros, consulta: url.searchParams, url });
      }
    }

    if (metodosPermitidos.size > 0) {
      // La ruta existe: el 405 DEBE incluir Allow con los metodos validos.
      if (metodosPermitidos.has('GET')) metodosPermitidos.add('HEAD');
      const permitidos = [...metodosPermitidos].sort().join(', ');
      return responderError(res, 405, `Metodo ${req.method} no permitido en ${ruta}`,
        'METODO_NO_PERMITIDO', { permitidos }, { Allow: permitidos });
    }

    return responderError(res, 404, `No existe la ruta ${ruta}`, 'RECURSO_NO_ENCONTRADO');
  }

  return { registrar, get, post, despachar, rutas };
}

module.exports = { crearEnrutador, compilarPatron, emparejar, normalizarRuta };

El 405 es la parte que casi todo el mundo se salta, y la norma es explícita: una respuesta 405 debe incluir la cabecera Allow con los métodos aceptados. Verlo funcionando:

curl -i -X DELETE http://localhost:3000/eventos
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD
Content-Type: application/json; charset=utf-8

Un cliente que recibe un 404 piensa que se equivocó de URL y se rinde. Uno que recibe 405 con Allow: GET, HEAD sabe exactamente qué hacer. Es la diferencia entre una API que se puede explorar y una que hay que adivinar.

Sobre el orden y la especificidad: nuestro enrutador recorre la tabla y gana la primera que casa, así que las rutas más específicas van primero. Con /eventos/:id y /eventos/destacados registradas en ese orden, /eventos/destacados caería en la primera con id = 'destacados'. Los enrutadores serios ordenan por especificidad; el nuestro delega esa responsabilidad en ti, y por eso conviene registrar siempre los literales antes que los patrones con parámetros.

  1. Despacho asíncrono y manejo de errores centralizado

Los manejadores son async, porque casi todos leen el catálogo. Eso significa que cualquiera puede lanzar, y no queremos repetir un try/catch en cada uno. Lo subimos una sola vez al despachador:

// Envuelve el despacho: un unico punto donde se traducen los errores.
function crearManejadorHttp(enrutador) {
  return function manejarPeticion(req, res) {
    // Promise.resolve() captura tambien lo que lance despachar de forma sincrona.
    Promise.resolve().then(() => enrutador.despachar(req, res)).catch((error) => {
      if (res.headersSent) {
        // Ya salieron las cabeceras: no se puede desmentir el estado.
        console.error('[error] tras enviar cabeceras:', error);
        return res.destroy();
      }
      const { estado, codigo, mensaje } = cuerpoParaError(error);
      responderError(res, estado, mensaje, codigo);
    });
  };
}

Esto es un antes y un después en la arquitectura del servidor:

  • Los manejadores dejan de manejar errores HTTP. Si el evento no existe, lanzan EVENTO_NO_ENCONTRADO y se olvidan. La traducción a 404 la hace cuerpoParaError, en un solo sitio.
  • El dominio sigue sin conocer HTTP. Evento.reservar lanza AFORO_INSUFICIENTE igual que cuando lo llamaba la CLI del Módulo 1.
  • Nada se pierde. Un error sin traducción conocida se convierte en 500 y se registra en stderr, que es exactamente lo que queremos para enterarnos de él.
  • res.headersSent decide el último recurso. Si la respuesta ya estaba en marcha (por ejemplo, a mitad de servir un fichero), lo honrado es destruir la conexión: el cliente detectará la respuesta truncada.

Este patrón —un envoltorio que captura y traduce— es exactamente lo que Express llama manejador de errores, con su firma de cuatro argumentos (err, req, res, next). Lo veremos en la lección 06-07 y reconocerás la idea al instante, porque acabas de escribirla.

  1. Las rutas de la API de Escena Viva

Con el motor listo, declarar la API es casi leer una lista:

// src/servidor/rutas-api.js
// Declaracion de las rutas HTTP de Escena Viva. Los manejadores NO
// manejan errores: lanzan con error.codigo y decide el enrutador.

const { obtenerCatalogo, obtenerEventoPorId } = require('../catalogo-datos.js');
const { responderJson } = require('./respuestas.js');

const arranque = Date.now();

async function buscarEventoObligatorio(id) {
  const evento = await obtenerEventoPorId(id);
  if (!evento) {
    const error = new Error(`No existe el evento ${id}`);
    error.codigo = 'EVENTO_NO_ENCONTRADO';
    throw error;
  }
  return evento;
}

function registrarRutas(enrutador) {
  enrutador.get('/salud', (req, res) => {
    const segundosEnMarcha = Math.round((Date.now() - arranque) / 1000);
    responderJson(res, 200, { estado: 'ok', version: process.version, segundosEnMarcha });
  });

  enrutador.get('/eventos', async (req, res, { consulta }) => {
    const sala = consulta.get('sala');
    const eventos = (await obtenerCatalogo())
      .filter((evento) => sala === null || evento.sala === sala);

    responderJson(res, 200, { total: eventos.length, eventos });
  });

  enrutador.get('/eventos/:id', async (req, res, { parametros }) => {
    responderJson(res, 200, await buscarEventoObligatorio(parametros.id));
  });

  enrutador.get('/eventos/:id/sesiones', async (req, res, { parametros }) => {
    const evento = await buscarEventoObligatorio(parametros.id);
    responderJson(res, 200, { eventoId: evento.id, total: evento.numeroSesiones, sesiones: evento.sesiones });
  });

  return enrutador;
}

module.exports = { registrarRutas };

Y el servidor de la lección 04-01 se simplifica hasta una línea: http.createServer(crearManejadorHttp(registrarRutas(crearEnrutador()))). Comprobación completa de la API:

curl -s http://localhost:3000/salud                       # 200 {"estado":"ok",...}
curl -s http://localhost:3000/eventos | head -3           # 200, total 3
curl -s http://localhost:3000/eventos/evt-002             # 200, Noche de Monologos
curl -s http://localhost:3000/eventos/evt-999             # 404 EVENTO_NO_ENCONTRADO
curl -s http://localhost:3000/eventos/evt-003/sesiones    # 200, total 2
curl -i -X POST http://localhost:3000/eventos             # 405 con Allow: GET, HEAD
curl -i http://localhost:3000/no-existe                   # 404 RECURSO_NO_ENCONTRADO

  1. Por qué a partir del Módulo 6 usaremos Express

Nuestro enrutador funciona, es corto y lo entiendes entero. Y aun así, en un proyecto real, casi nadie escribiría esto. Vale la pena ser honestos sobre lo que no tiene:

Le falta Qué implica
Middleware No hay forma de decir "esto se ejecuta antes de todas las rutas" (registro, CORS, autenticación)
Sub-enrutadores Todas las rutas en una lista plana; nada de montar /api/v1 con sus rutas dentro
Orden por especificidad Registrar en mal orden hace inalcanzable una ruta, sin aviso
Patrones ricos Nada de opcionales, comodines ni restricciones por parámetro
Parseo de cuerpo, cookies, contenido negociado Cada cosa hay que hacerla a mano (empezamos en 04-05)

Entonces, ¿por qué hemos hecho esto? Porque Express no es magia, es esta misma tabla con más años encima. Cuando en la lección 06-03 escribas app.get('/eventos/:id', manejador), sabrás que por dentro hay un patrón compilado a expresión regular, un objeto req.params que sale de los grupos de captura y un despachador que recorre una lista. Y cuando algo no funcione —una ruta que no se alcanza, un 404 inesperado, un manejador de errores que no se dispara—, no estarás depurando una caja negra.

Errores Comunes y Consejos

  • Casar contra req.url en vez de url.pathname. Cualquier parámetro de consulta rompe la coincidencia y produce un 404 inexplicable.
  • Usar .+ en lugar de [^/]+ para los parámetros. El patrón se come segmentos enteros y atropella otras rutas.
  • Olvidar los anclajes ^ y $. /eventos casaría dentro de /admin/eventos/borrar.
  • Registrar /eventos/:id antes que /eventos/destacados. Gana la primera que casa, y la literal queda inalcanzable.
  • Devolver 404 cuando corresponde 405. Y si devuelves 405, la cabecera Allow es obligatoria.
  • Bajar la ruta a minúsculas. Destroza identificadores legítimos como EV-2026-000123. Si quieres tolerancia, redirige con 301.
  • Poner un try/catch en cada manejador. Repetición, incoherencia y errores que se tragan. Uno central y basta.
  • Consejo: expón enrutador.rutas y añade una ruta GET /rutas en desarrollo que las liste. Es la documentación más barata que existe.

Ejercicios

Ejercicio 1: probar compilarPatron sin servidor

Escribe src/laboratorio/probar-patrones.js que, sin levantar ningún servidor, compruebe estos casos e imprima una tabla patron | ruta | casa | parametros: /eventos contra /eventos y /eventos/evt-001; /eventos/:id contra /eventos/evt-001, /eventos/evt-001/sesiones y /eventos/; /eventos/:id/sesiones/:sesionId contra /eventos/evt-002/sesiones/ses-002-1; y /informes/2026.json contra /informes/2026Xjson. Explica el último caso.

Ejercicio 2: DELETE y el 405

Registra DELETE /eventos/:id en el enrutador con un manejador que lance ESTADO_INVALIDO si el evento tiene entradas vendidas. Comprueba con curl -i que: DELETE /eventos/evt-001 da 409, DELETE /eventos/evt-999 da 404, PUT /eventos/evt-001 da 405 con Allow: DELETE, GET, HEAD, y DELETE /eventos da 405 con Allow: GET, HEAD.

Ejercicio 3: contar rutas y tiempos

Añade al enrutador un contador por ruta (usando registro.patron como clave) con el número de peticiones y el tiempo total en milisegundos, y una ruta GET /metricas que lo devuelva ordenado por tiempo medio descendente. Mide el efecto: ¿qué ruta es la más lenta y por qué?

Soluciones

Solución 1. El caso interesante es el último: /informes/2026.json contra /informes/2026Xjson no casa, porque el paso 1 de compilarPatron escapó el punto y lo convirtió en literal. Sin ese escape, . casaría con cualquier carácter y la X colaría.

const { compilarPatron, emparejar } = require('../servidor/enrutador.js');

const casos = [
  ['/eventos', '/eventos'],
  ['/eventos/:id', '/eventos/evt-001'],
  ['/eventos/:id', '/eventos/evt-001/sesiones'],   // null: [^/]+ no cruza segmentos
  ['/eventos/:id', '/eventos/'],                   // null: + exige al menos un caracter
  ['/informes/2026.json', '/informes/2026Xjson']   // null: el punto esta escapado
];

for (const [patron, ruta] of casos) {
  const parametros = emparejar(compilarPatron(patron), ruta);
  console.log(`${patron.padEnd(28)} | ${ruta.padEnd(30)} | ${parametros !== null} | ${JSON.stringify(parametros)}`);
}

Solución 2. El manejador solo se ocupa del dominio; los estados los pone la tabla de 04-02:

enrutador.registrar('DELETE', '/eventos/:id', async (req, res, { parametros }) => {
  const evento = await buscarEventoObligatorio(parametros.id);   // lanza 404
  if (evento.entradasVendidas > 0) {
    const error = new Error(`El evento ${evento.id} tiene ${evento.entradasVendidas} entradas vendidas`);
    error.codigo = 'ESTADO_INVALIDO';                            // -> 409
    throw error;
  }
  responderSinContenido(res, 204);
});

evt-001 tiene 276 entradas vendidas, así que devuelve 409. Nota que el PUT da 405 con DELETE y GET en Allow: el enrutador ha recorrido la tabla entera acumulando los métodos de todos los registros cuyo patrón casaba, y por eso el Allow sale completo.

Solución 3. El contador se envuelve alrededor de la llamada al manejador, dentro de despachar:

const metricas = new Map();

async function invocarConMetrica(registro, req, res, contexto) {
  const inicio = process.hrtime.bigint();
  try {
    return await registro.manejador(req, res, contexto);
  } finally {
    const ms = Number(process.hrtime.bigint() - inicio) / 1e6;
    const previo = metricas.get(registro.patron) ?? { peticiones: 0, msTotal: 0 };
    metricas.set(registro.patron, { peticiones: previo.peticiones + 1, msTotal: previo.msTotal + ms });
  }
}

El finally es imprescindible: sin él, una petición que lanza no se contabilizaría y las medias mentirían justo en los casos que más interesan. La ruta más lenta será /eventos/:id/sesiones, porque obtenerEventoPorId recorre el catálogo tras leerlo; con la memorización de la lección 03-01, la primera petición cuesta una lectura de disco y las siguientes casi nada. Ese salto entre la primera medición y las demás se ve perfectamente en /metricas.

Conclusión

Escena Viva ya tiene una API navegable. Has visto que enrutar es casar (método, ruta) con un manejador, que el if/else sobre req.url está mal desde el primer momento —compara la consulta junto a la ruta— y que se vuelve insostenible en cuanto aparecen los parámetros: índices mágicos, condiciones de longitud y un orden frágil que nadie se atreve a tocar.

La alternativa es src/servidor/enrutador.js: una tabla de { metodo, patron, manejador } y un motor genérico. compilarPatron traduce /eventos/:id a /^\/eventos\/([^\/]+)$/, y ahora sabes exactamente por qué cada pieza es como es: los anclajes ^ y $ para que la ruta case entera, [^/]+ para que un parámetro no se coma un segmento vecino, + en vez de * para rechazar el valor vacío, y el escape previo para que un punto del patrón sea un punto de verdad. emparejar extrae los grupos de captura y los decodifica por segmentos, normalizarRuta colapsa barras y quita la final, y las mayúsculas se respetan porque las rutas las distinguen. Los fallos también: 404 si nada casa, y 405 con la cabecera Allow obligatoria si la ruta existe con otro método.

Y sobre todo, tienes tu primer manejador de errores centralizado: un único catch en el despachador que llama a cuerpoParaError, traduce EVENTO_NO_ENCONTRADO a 404 y ESTADO_INVALIDO a 409, registra los 500 sin filtrarlos y comprueba res.headersSent antes de intentar responder. Los manejadores de src/servidor/rutas-api.js han quedado limpios: hacen su trabajo y lanzan con error.codigo. Esa idea es la que Express formaliza en 06-07, y la reconocerás al instante porque la acabas de escribir.

Con GET /eventos, GET /eventos/:id, GET /eventos/:id/sesiones y GET /salud en marcha, la API devuelve JSON. Pero una plataforma de venta de entradas también tiene que enseñar una página. En la próxima lección, Sirviendo Archivos Estáticos, montaremos el mini front-end de Escena Viva desde este mismo servidor: mapearemos la URL a una ruta de disco protegida con resolverDentroDe —y verás en directo cómo GET /../../datos/eventos.json se lleva tus datos sin esa defensa—, enviaremos los ficheros con createReadStream y pipeline en vez de readFile, y aprenderemos a responder 304 Not Modified con ETag para que la segunda visita no descargue nada.

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