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
- Qué es enrutar exactamente
- El intento ingenuo:
if/elsesobrereq.url - Dónde se rompe: rutas con parámetros
- La tabla de rutas
compilarPatron: de/eventos/:ida expresión regular- Normalizar la ruta antes de casarla
- Casar, extraer parámetros y responder
404o405 - Despacho asíncrono y manejo de errores centralizado
- Las rutas de la API de Escena Viva
- Por qué a partir del Módulo 6 usaremos Express
- 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-001y/eventos/evt-002deben 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) yPOST /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).
- El intento ingenuo:
if/else sobre req.url
if/else sobre req.urlAsí 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.
- 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.
- 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]
compilarPatron: de /eventos/:id a expresión regular
compilarPatron: de /eventos/:id a expresión regularNecesitamos 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.
- 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.
- Casar, extraer parámetros y responder
404 o 405
404 o 405Ya 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:
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.
- 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_ENCONTRADOy se olvidan. La traducción a404la hacecuerpoParaError, en un solo sitio. - El dominio sigue sin conocer HTTP.
Evento.reservarlanzaAFORO_INSUFICIENTEigual que cuando lo llamaba la CLI del Módulo 1. - Nada se pierde. Un error sin traducción conocida se convierte en
500y se registra enstderr, que es exactamente lo que queremos para enterarnos de él. res.headersSentdecide 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.
- 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
- 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.urlen vez deurl.pathname. Cualquier parámetro de consulta rompe la coincidencia y produce un404inexplicable. - Usar
.+en lugar de[^/]+para los parámetros. El patrón se come segmentos enteros y atropella otras rutas. - Olvidar los anclajes
^y$./eventoscasaría dentro de/admin/eventos/borrar. - Registrar
/eventos/:idantes que/eventos/destacados. Gana la primera que casa, y la literal queda inalcanzable. - Devolver
404cuando corresponde405. Y si devuelves405, la cabeceraAllowes obligatoria. - Bajar la ruta a minúsculas. Destroza identificadores legítimos como
EV-2026-000123. Si quieres tolerancia, redirige con301. - Poner un
try/catchen cada manejador. Repetición, incoherencia y errores que se tragan. Uno central y basta. - Consejo: expón
enrutador.rutasy añade una rutaGET /rutasen 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
- ¿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
