← Node.js: JavaScript fuera del navegador

Sesión 5

Del servidor a mano al framework

Tres horas. Media hora para contar qué le falta a tu servidor y ver cómo lo resuelve Express, y dos horas y media reescribiéndolo pieza a pieza hasta dejar el registro y los errores en un solo sitio.

Hoy · Hoja de ruta

  1. 1. Aprende: Qué problemas concretos tiene tu servidor, qué pieza de Express resuelve cada uno, qué es un middleware y cómo se centralizan los errores.
  2. 2. Haz: Documenta los límites de tu código, reescríbelo con Express ruta a ruta y añade registro y tratamiento central de errores.
  3. 3. Comprueba: Se comporta igual que el de la semana pasada, con menos código, y ninguna ruta tiene ya su propio try/catch.

Antes de empezar · 5 minutos, sin apuntes

  1. ¿Cuántas líneas ocupa hoy tu enrutado, y cuál es su nivel máximo de anidamiento?
  2. ¿Qué tendrías que tocar para añadir una ruta con dos parámetros?
  3. ¿Cuántos try/catch hay repartidos por tus rutas?

Se explica

25 minutos · conceptos y demostración

Hoy se instala la única dependencia de la unidad, y se instala después de haber escrito a mano lo que hace. Ese orden no es casual: es lo que convierte un framework en una herramienta en lugar de en una caja negra.

La cuenta de lo que falta

Lo que necesitas Lo que tienes Lo que costaría
Rutas con parámetros Partir el texto a mano Un comparador de patrones
Método y ruta a la vez Condicionales anidados Una tabla de rutas
Cuerpo JSON ya analizado Leerlo en cada ruta Un paso previo común
Errores en un solo sitio Un try/catch por ruta Un tratamiento central
Registro de peticiones Repetido en cada rama Un paso previo común
Estáticos Lo has escrito tú Bastante código

Ninguna es imposible. Todas juntas son un framework, y ya existe uno pequeño y muy extendido que hace exactamente eso.

Esta sesión es el punto de la unidad

Si hubiéramos empezado por Express, cada una de sus piezas sería magia con nombre. Después de dos semanas escribiendo esto a mano, app.get("/api/productos/:id", ...) no es magia: es la escalera de condicionales que tienes delante, resuelta.

Y el día que Express se quede corto —una cabecera rara, un flujo que no encaja— sabrás bajar un nivel, porque ese nivel lo has escrito tú.

Mira tu servidor: seguro que hay cosas que haces antes de decidir la ruta —registrar la petición, analizar la URL— y cosas que haces después —cerrar, capturar errores—. Esa idea de «una cadena de pasos por los que atraviesa toda petición» tiene nombre, y es la pieza central de la sesión:

Una petición atravesando la cadena
  1. Registro
  2. Analizar el cuerpo
  3. Ruta
  4. Errores
  5. Respuesta

El primer Express

npm install express
import express from "express";

const app = express();

app.use(express.json());                     // analiza el cuerpo JSON
app.use(express.static("publico"));          // sirve los estáticos

app.get("/api/productos", async (peticion, respuesta) => {
  respuesta.json(await listar());
});

app.get("/api/productos/:id", async (peticion, respuesta) => {
  const producto = await obtener(Number(peticion.params.id));
  if (!producto) return respuesta.status(404).json({ error: "No encontrado" });
  respuesta.json(producto);
});

app.listen(PUERTO, () => console.log(`Escuchando en el puerto ${PUERTO}`));

Compáralo con lo de la semana pasada. Las dos primeras líneas sustituyen tu lector de cuerpo y tu servidor de estáticos enteros; :id sustituye el partido de texto a mano; y respuesta.json pone la cabecera, serializa y cierra.

A mano Con Express
Condicionales por método y ruta app.get, app.post, app.put, app.delete
Partir la ruta para el identificador peticion.params.id
Analizar la cadena de consulta peticion.query
Acumular y analizar el cuerpo express.json()
Escribir estado, cabeceras y cerrar respuesta.status(...).json(...)
Tu servidor de ficheros express.static(...)

Express no valida por ti

Que peticion.body exista no significa que traiga lo que esperas: puede llegar vacío, con campos de más, con tipos que no son. Tus funciones de validación de la UD3 siguen siendo igual de necesarias, y ahora más, porque quien llama a la API puede ser cualquiera.

Un framework quita trabajo repetitivo. No quita responsabilidad.

Express recorre lo declarado en orden hasta que algo responde. De ahí dos consecuencias que causan casi todos los desconciertos del primer día: el analizador de cuerpo va antes que las rutas que lo usan, y el 404 va al final, después de todas.

app.use(express.json());            // antes de las rutas que leen el cuerpo
app.get("/api/productos", ...);
app.use((peticion, respuesta) => respuesta.status(404).json({ error: "No encontrado" }));

Qué es un middleware

Middleware

Una función que recibe la petición, la respuesta y una tercera función, next. Puede mirar o modificar la petición, puede responder y cortar la cadena, o puede llamar a next para que siga el paso siguiente.

function registrar(peticion, respuesta, next) {
  const inicio = Date.now();

  respuesta.on("finish", () => {
    const ms = Date.now() - inicio;
    console.log(`${peticion.method} ${peticion.originalUrl} → ${respuesta.statusCode} (${ms} ms)`);
  });

  next();
}

app.use(registrar);

Lo que hace útil este ejemplo es dónde se registra: al terminar la respuesta, de modo que el estado y el tiempo ya son conocidos. Una sola declaración cubre además todas las rutas, presentes y futuras.

Un middleware que no responde ni llama a next cuelga la petición

La petición se queda dentro de la cadena, sin avanzar y sin respuesta, hasta que el cliente se cansa. No hay error, no hay traza, no hay nada en los registros. Es el fallo más desconcertante de Express, y siempre es el mismo olvido.

Errores en un solo sitio

app.get("/api/productos/:id", async (peticion, respuesta, next) => {
  try {
    const producto = await obtener(Number(peticion.params.id));
    if (!producto) throw new ErrorNoEncontrado("Producto no encontrado");
    respuesta.json(producto);
  } catch (error) {
    next(error);        // se lo pasa al manejador de errores
  }
});

// El último de todos, y con cuatro parámetros
app.use((error, peticion, respuesta, next) => {
  if (error instanceof ErrorDeValidacion) {
    return respuesta.status(400).json({ error: error.message, detalles: error.errores });
  }
  if (error instanceof ErrorNoEncontrado) {
    return respuesta.status(404).json({ error: error.message });
  }

  console.error(error);      // la traza, para ti
  respuesta.status(500).json({ error: "Error interno del servidor" });
});

Los cuatro parámetros son lo que distingue a un manejador de errores de un middleware normal: si quitas el next final, Express lo tratará como uno cualquiera y no lo llamará nunca.

La traza es para ti; el mensaje, para quien llama

Devolver al cliente el error completo revela rutas de ficheros, versiones y estructura interna, que es precisamente la información que busca quien intenta atacar un sistema. Tampoco aporta nada a quien solo pretendía consultar un producto.

Registra el detalle en el servidor y responde un mensaje genérico con el estado correcto. Esa es la razón de que el manejador tenga la última palabra sobre qué sale.

Se trabaja

150 minutos · del servidor propio al framework

El primer paso documenta lo que tienes antes de sustituirlo; sin él, la reescritura no enseña nada. Los dos últimos miden el cambio y provocan a propósito los tres fallos del primer día con Express.

Paso 1 · El informe · 25 min

Sobre tu propio código, antes de instalar nada:

  1. Cuenta las líneas del enrutado y el nivel máximo de anidamiento.
  2. Marca todo lo que se repite en más de una ruta.
  3. Escribe qué habría que cambiar para añadir GET /api/categorias/:nombre/productos.
  4. Escribe un comparador de rutas con parámetros y comprueba lo que cuesta. Basta con que funcione para un parámetro.
  5. Redacta media página: qué problemas tiene tu servidor y qué esperas de un framework.

Guarda ese informe. Al final de la sesión lo vas a contrastar con lo que ha ocurrido de verdad.

Paso 2 · La reescritura · 45 min

Es el trabajo central de la sesión.

  1. Instala Express y anota qué cambió en package.json y en el fichero de bloqueo.
  2. Reescribe las rutas de lectura y comprueba con tu fichero peticiones.http de la sesión 4.
  3. Reescribe la creación con validación y 201.
  4. Sustituye tu servidor de estáticos por express.static.
  5. Añade el 404 final.
  6. Mueve las rutas a src/rutas.js con un router propio, para que servidor.js solo monte la aplicación.

Antes de continuar: ejecuta el peticiones.http entero contra la versión nueva. Las respuestas deben ser idénticas a las de la semana pasada, código por código y cuerpo por cuerpo. Si alguna cambia, la reescritura ha modificado el comportamiento y eso no era el encargo.

Paso 3 · La cadena completa · 45 min

  1. Escribe el middleware de registro con método, ruta, estado y tiempo.
  2. Escribe uno que rechace cuerpos demasiado grandes.
  3. Convierte todas tus rutas para que deleguen los errores con next.
  4. Escribe el manejador de errores central con sus tres casos.
  5. Comprueba que un fallo interno responde 500 sin filtrar la traza al cliente, y que la traza sí aparece en tu consola.
  6. Añade un identificador único a cada petición, sácalo en los registros y devuélvelo en la respuesta de error. Así, cuando alguien te diga «me ha dado un error», podrás encontrar su petición exacta.

Paso 4 · Antes y después, en números · 15 min

Recupera el informe del paso 1 y compáralo con el resultado.

Medida A mano Con Express Qué se ha ido
Líneas del enrutado
Nivel máximo de anidamiento
Número de try/catch
Líneas para leer el cuerpo
Líneas para servir estáticos
Líneas para añadir una ruta nueva

La última fila es la importante: mide el coste del próximo cambio, no el de este. Un framework se paga una vez y se cobra en cada ruta que añadas después.

Responde también a esto: ¿qué parte de lo que esperabas en tu informe no te ha dado Express, y sigue siendo trabajo tuyo?

Paso 5 · Los tres desconciertos del primer día · 20 min

Provócalos a propósito, en este orden, y anota el síntoma exacto de cada uno. Son los tres que te van a pasar, y reconocerlos ahorra tardes enteras.

  1. Pon express.json() después de la ruta que lee el cuerpo. ¿Qué vale peticion.body? ¿Da algún error?
  2. Pon el 404 final antes de las rutas. ¿Qué responde GET /api/productos?
  3. Escribe un middleware que no responda ni llame a next. ¿Qué ve el cliente? ¿Qué aparece en tus registros?
Fallo provocado Síntoma ¿Hay error en consola? Cómo lo reconocerás la próxima vez
1
2
3

El tercero es el peor de los tres, precisamente porque la columna del error está vacía.

Añade un cuarto por tu cuenta: quita el next final del manejador de errores, dejándolo con tres parámetros. Comprueba qué pasa cuando una ruta falla.

Ampliación si has completado el trabajo

Primero termina y comprueba los cinco pasos. El primer reto es el que de verdad cierra la unidad: escribir tú lo que acabas de instalar.

Reto 1 · Tu propio mini-framework

Has visto qué hace Express. Escribe una versión mínima tuya, sobre el módulo http nativo, y comprueba cuánto de la magia era magia.

  1. Implementa use(fn) que acumule middlewares en una lista.
  2. Implementa el recorrido de esa lista: cada middleware recibe next, y llamarlo pasa al siguiente. Si ninguno responde, se acaba en un 404.
  3. Implementa get(patron, fn) y post(patron, fn) con un comparador que entienda :parametro y deje el valor en peticion.params.
  4. Implementa un middleware json() propio que acumule el cuerpo y lo analice, tratando el JSON inválido como un 400.
  5. Reescribe con tu framework dos rutas de tu API y comprueba que responden igual.
  6. Cuenta las líneas que te ha costado y compáralas con lo que hace Express. Escribe en tres líneas qué has entendido de Express que antes no entendías, y qué no has implementado de lo que él sí hace.

Reto 2 · Que las rutas no repitan el try/catch

Todas tus rutas asíncronas terminan igual: un try con la lógica y un catch que llama a next. Eso es repetición, y la repetición se elimina.

  1. Escribe una función envolver(fn) que reciba un manejador asíncrono y devuelva otro que capture cualquier rechazo y lo pase a next.
  2. Aplícala a todas tus rutas y borra los try/catch. Comprueba que los errores siguen llegando al manejador central.
  3. Provoca un error dentro de una ruta asíncrona sin el envoltorio y anota qué pasa. En versiones antiguas de Express la petición se quedaba colgada; comprueba qué hace la tuya.
  4. Averigua desde qué versión de Express eso cambió, y qué implica para el código que encuentres por ahí.
  5. Como segunda parte, escribe un middleware que limite el tamaño del cuerpo y devuelva 413. Decide el límite y justifícalo.
  6. Explica en tres líneas por qué este envoltorio es, conceptualmente, lo mismo que aplicarATodos de la UD3: una función que recibe una función y devuelve otra.
Objetivo mínimoEl informe escrito, todas las rutas reescritas respondiendo igual, y registro y errores centralizados.
Si lo tienesLa tabla de antes y después completa, y los tres desconciertos provocados con su síntoma anotado.
RetoEl mini-framework propio con middlewares y rutas con parámetros, o el envoltorio asíncrono con el límite de tamaño.

Cierre

5 minutos · comprobación y recuerdo

Lista de verificación de la sesión

  • Nombras seis problemas concretos de tu servidor a mano.
  • Tu servidor está reescrito con Express y responde exactamente igual.
  • Sabes qué pieza sustituye a cada cosa que escribiste.
  • El registro y los errores están en un solo sitio.
  • Sigues validando la entrada: Express no lo hace por ti.
  • El cliente nunca ve una traza.

Antes de cerrar · 3 minutos, sin mirar

  1. ¿Qué hace express.json() y dónde debe declararse?
  2. ¿Dónde vive el identificador de /api/productos/:id, y de qué tipo llega?
  3. ¿Por qué el 404 va al final?
  4. ¿Qué ocurre si un middleware no responde ni llama a next?
  5. ¿En qué se distingue un manejador de errores de un middleware normal?
  6. ¿Por qué no se devuelve la traza al cliente?
Ver respuestas

1 · Analiza el cuerpo JSON de la petición, y va antes de las rutas que lo usan.

2 · En peticion.params.id, y llega como texto.

3 · Porque Express recorre en orden: si va antes, responde 404 a todo.

4 · Que la petición se queda colgada sin respuesta ni error.

5 · En que tiene cuatro parámetros, empezando por el error.

6 · Porque revela información interna del sistema y no ayuda a quien llama.

Microprueba semanal 5 · 5–10 minutos

Individual, sin IA y sin apuntes.

  1. Nombra tres problemas del servidor escrito a mano y qué pieza de Express resuelve cada uno.
  2. ¿Qué ocurre si un middleware no responde ni llama a next?
  3. ¿En qué se distingue un manejador de errores de un middleware normal?