Tres horas. Media hora para entender qué sabe cada capa y qué no debe saber ninguna, y dos horas y media separando tu proyecto hasta poder cambiarle el almacén sin tocar una sola ruta.
Hoy · Hoja de ruta
- 1. Aprende: Qué hace cada capa, cómo se sostiene la coherencia de los datos y cómo se traduce cada error interno a una respuesta HTTP en un solo sitio.
- 2. Haz: Separa ruta, servicio y repositorio; sustituye el almacén por otro; cierra el manejador central de errores.
- 3. Comprueba: Puedes cambiar el almacén sin tocar nada más, y ninguna ruta decide ya un código de estado por su cuenta.
Antes de empezar · 5 minutos, sin apuntes
- ¿Cuántos ficheros tocarías hoy si cambiaras el fichero JSON por una base de datos?
- ¿Qué pasa si el fichero de datos tiene un producto sin precio, o dos con el mismo identificador?
- ¿Cuántos sitios de tu código deciden hoy un código de estado?
Se explica
25 minutos · conceptos y demostración
La sesión tiene una sola prueba, y hoy se pasa o no se pasa: cambiar el sitio donde viven los datos sin tocar nada por encima. Todo lo demás son las condiciones para que eso sea posible.
Las tres capas
- Ruta · HTTP
- Servicio · las reglas
- Repositorio · el almacén
| Capa | Sabe de | No sabe de |
|---|---|---|
| Ruta | Peticiones, respuestas y códigos | Ficheros ni reglas de negocio |
| Servicio | Reglas y validaciones | HTTP ni de dónde salen los datos |
| Repositorio | Cómo se guardan y se leen | Reglas ni HTTP |
// repositorio/productos.js
export async function porId(id) {
const productos = await leerProductos();
return productos.find((p) => p.id === id) ?? null;
}
// servicio/productos.js
export async function borrar(id) {
const producto = await repositorio.porId(id);
if (!producto) return false;
if (producto.stock > 0) throw new ErrorDeConflicto("No se puede borrar con stock");
return repositorio.eliminar(id);
}
// rutas/productos.js
export async function borrarProducto(peticion, respuesta, next) {
try {
const borrado = await servicio.borrar(Number(peticion.params.id));
if (!borrado) throw new ErrorNoEncontrado("Producto no encontrado");
respuesta.status(204).end();
} catch (error) {
next(error);
}
}
La prueba de que las capas están bien separadas
Busca en tu servicio la palabra respuesta, y en tu repositorio la palabra peticion. Si aparecen, la separación es decorativa.
Y la prueba de fuego: si cambiar el fichero JSON por otro almacén obliga a tocar algo fuera del repositorio, todavía no están separadas. Ese es exactamente el ejercicio de hoy.
Con un solo recurso y un fichero, esta separación parece burocracia. Sus tres razones aparecen enseguida: se puede probar el servicio sin levantar un servidor; se puede cambiar el almacén sin tocar la API; y cuando el proyecto tiene ocho recursos, todos se organizan igual y cualquiera sabe dónde mirar. Es, además, la arquitectura que verás en el módulo de servidor con otros nombres: controlador, servicio y repositorio.
El repositorio como frontera
// La interfaz que el servicio conoce
export async function todos() {}
export async function porId(id) {}
export async function guardar(producto) {}
export async function eliminar(id) {}
Mientras el conjunto de funciones y lo que devuelven no cambie, el servicio no distingue si detrás hay un fichero, una base de datos o una API ajena.
function normalizar(crudo) {
return {
id: Number(crudo.id),
nombre: String(crudo.nombre ?? "").trim(),
precio: Number(crudo.precio ?? 0),
stock: Number.isInteger(crudo.stock) ? crudo.stock : 0,
categoria: crudo.categoria ?? "sin-categoria"
};
}
El fichero de datos también es entrada externa
Alguien puede editarlo a mano, puede venir de una versión anterior con otros campos, puede haberse quedado a medias. Si el repositorio devuelve lo que encuentre sin mirar, un dato roto se propaga hasta la respuesta.
Normalizar al leer da al resto del programa la garantía de que un producto tiene la forma de un producto. Es la misma idea de validar en el borde, aplicada al borde de abajo.
Un proyecto que se prueba necesita además poder volver a un estado conocido, con un script que rellene el fichero con datos de ejemplo:
{ "scripts": { "sembrar": "node src/herramientas/sembrar.js" } }
Sin él, cada prueba deja los datos en un estado distinto y los fallos dejan de ser reproducibles.
La traducción de errores, en un solo sitio
const ESTADOS = {
ErrorDeValidacion: 400,
ErrorNoAutenticado: 401,
ErrorSinPermiso: 403,
ErrorNoEncontrado: 404,
ErrorDeConflicto: 409
};
export function manejadorDeErrores(error, peticion, respuesta, next) {
const estado = ESTADOS[error.name] ?? 500;
if (estado >= 500) {
console.error(`[${peticion.id}] ${peticion.method} ${peticion.originalUrl}`, error);
}
respuesta.status(estado).json({
error: estado >= 500 ? "Error interno del servidor" : error.message,
codigo: error.codigo ?? error.name ?? "ERROR",
detalles: error.detalles ?? [],
peticion: peticion.id
});
}
Añadir un tipo de error nuevo se reduce a añadir una línea a la tabla, sin que ninguna ruta necesite conocer qué código corresponde a su fallo.
export function identificar(peticion, respuesta, next) {
peticion.id = crypto.randomUUID();
respuesta.setHeader("X-Request-Id", peticion.id);
next();
}
Un identificador convierte «me da error» en un caso investigable
El cliente ve un mensaje genérico y un identificador. Ese mismo identificador está en tus registros junto a la traza completa. Quien reporta el problema te da el número, y tú encuentras exactamente su petición entre miles.
Es lo que permite no filtrar detalles internos sin quedarte ciego para diagnosticar.
Al otro lado se cobra el contrato de la sesión 1: una sola función en el cliente sirve para toda la API, hoy y cuando añadas rutas.
async function pedir(url, opciones) {
const respuesta = await fetch(url, opciones);
if (respuesta.ok) return respuesta.status === 204 ? null : respuesta.json();
const cuerpo = await respuesta.json().catch(() => ({}));
throw new ErrorDeApi(respuesta.status, cuerpo.error ?? "Error inesperado", cuerpo.detalles ?? []);
}
Si cada error tuviera una forma distinta, esta función no podría existir.
Se trabaja
150 minutos · separar, sustituir y diagnosticar
Los tres primeros pasos construyen la separación. Los dos últimos la ponen a prueba: uno cambia el almacén de verdad, el otro recorre los cinco errores desde el navegador hasta el registro.
Paso 1 · La separación · 40 min
- Crea
src/servicio/ysrc/repositorio/y reparte el código que hoy está mezclado. - Deja las rutas sin ninguna referencia a ficheros.
- Deja el servicio sin ninguna referencia a peticiones ni respuestas.
- Mueve al servicio las reglas de negocio que estaban en las rutas, como «no se puede borrar un producto con stock».
- Busca las dos palabras de la prueba —
respuestaen el servicio,peticionen el repositorio— y corrige lo que aparezca. - Escribe un programa que use el servicio directamente, sin HTTP, y cree y borre un producto.
Antes de continuar: ese programa del apartado 6 funciona sin que Express esté arrancado. Si necesita el servidor, el servicio todavía depende de la capa de arriba.
Paso 2 · Cambiar el almacén · 40 min
- Escribe en un comentario la lista exacta de funciones que el servicio espera del repositorio, con lo que recibe y lo que devuelve cada una.
- Normaliza los datos al leerlos del fichero, con una función
normalizar. - Escribe un segundo repositorio que guarde en memoria, con esa misma lista de funciones.
- Elige entre uno y otro con una variable de entorno, en un solo punto del programa.
- Escribe el script de siembra y añádelo a los
scriptsdepackage.json. - Prueba tu fichero
.httpcompleto contra los dos repositorios.
Paso 3 · Errores de punta a punta · 40 min
- Define los cinco tipos de error de tu aplicación, cada uno con su nombre y su código.
- Escribe la tabla de traducción y el manejador central.
- Añade el identificador de petición y sácalo en el registro y en la respuesta.
- Elimina todos los códigos de estado repartidos por las rutas. Al terminar,
status(no debería aparecer en ninguna ruta salvo para las respuestas correctas. - Escribe la función
pediren el cliente y úsala en toda la interfaz, sustituyendo cadafetchsuelto. - Muestra en el formulario los detalles de validación campo por campo, aprovechando el array
detalles.
Paso 4 · La prueba del cambio de almacén · 15 min
Hasta aquí has escrito la separación. Ahora se comprueba si es real.
- Anota el estado de tu repositorio con
git statusantes de empezar. - Cambia al repositorio en memoria y ejecuta el fichero
.httpcompleto. - Rellena la tabla con lo que ha hecho falta tocar.
| Qué ha habido que tocar para cambiar de almacén | Ficheros | ¿Debería haber hecho falta? |
|---|---|---|
| Rutas | ||
| Servicio | ||
| Repositorio | ||
| Configuración o arranque |
- Cualquier fila fuera del repositorio y del punto único de configuración señala una fuga: algo de arriba sabe cómo se guardan los datos. Localízala y escribe en una línea qué la causaba.
- Si las dos ejecuciones no dan el mismo resultado, anota en qué petición difieren. Hay una diferencia legítima entre un almacén que persiste y otro que no, y conviene saber nombrarla.
Paso 5 · Los cinco errores, desde el navegador · 15 min
Provoca cada error desde la interfaz, no desde el fichero .http, y recorre el camino entero.
| Error provocado | Código | Qué ve la persona | Qué aparece en el registro |
|---|---|---|---|
| Precio inválido en el formulario | |||
| Producto que no existe | |||
| Borrado de un producto con stock | |||
| Fichero de datos corrupto a propósito | |||
| Excepción inesperada lanzada a mano en el servicio |
Las dos últimas son las que importan: comprueba que la persona ve un mensaje genérico con su identificador, que la traza completa queda solo en el registro, y que el identificador de la pantalla coincide con el del registro.
Escribe al terminar, en una línea, qué habrías tenido que hacer para diagnosticar el quinto caso sin ese identificador.
Ampliación si has completado el trabajo
Primero termina y comprueba los cinco pasos. El primer reto lleva la frontera del repositorio hasta donde se rompe; el segundo convierte tus registros en una herramienta de diagnóstico.
Reto 1 · El tercer almacén
Dos repositorios que leen de un array y de un fichero se parecen demasiado para probar nada. Escribe un tercero contra algo que se comporte de otra manera: una API pública de solo lectura, o una base de datos ligera como SQLite.
- Impleméntalo con la misma lista de funciones, sin cambiar su forma.
- Anota cada punto en el que la interfaz se queda corta. Aparecerán varios: un almacén remoto puede tardar, puede paginar, puede fallar por red, y puede no permitir borrar.
- Decide qué hacer con lo que el almacén nuevo no sabe hacer. Fallar de forma explícita es una respuesta válida; fingir que ha funcionado, no.
- Traduce los fallos propios de ese almacén a los tipos de error de tu aplicación. El servicio no debería recibir nunca un error de red en crudo.
- Rediseña la interfaz si hace falta, y vuelve a probar los tres repositorios con ella. Cambiar la interfaz por culpa del tercero indica que estaba escrita a la medida del fichero.
- Explica en tres líneas qué has aprendido sobre tu primera interfaz. Una abstracción solo se valida con la segunda implementación, y se confirma con la tercera.
Reto 2 · Seguir el rastro de un fallo
Tus registros hoy sirven para mirarlos por encima. Conviértelos en algo sobre lo que se pueda buscar.
- Pasa el registro a formato estructurado: una línea por petición, en JSON, con identificador, método, ruta, código de estado y duración en milisegundos.
- Haz que el identificador llegue hasta el repositorio, de modo que una operación de escritura pueda registrarse asociada a la petición que la causó. Piensa cómo pasarlo sin que el servicio hable de HTTP.
- Escribe el registro en un fichero además de en la consola, y decide qué ocurre cuando ese fichero crece.
- Provoca veinte peticiones, tres de ellas con error, y escribe el comando que extrae de tu fichero de registro solo las que fallaron.
- Ahora el ejercicio de verdad: pide a un compañero que introduzca un fallo en tu código sin decirte cuál. Reprodúcelo desde la interfaz, apunta el identificador y diagnostica la causa mirando solo el registro, sin leer el código.
- Escribe qué dato te faltó en el registro para llegar antes. Añádelo, y repite el apartado 5 con un fallo distinto.
- Decide qué no debe aparecer nunca en un registro. Hay al menos tres categorías de dato que no pueden acabar ahí; nómbralas y comprueba que las tuyas están limpias.
Cierre
5 minutos · comprobación y recuerdo
Lista de verificación de la sesión
- Cada capa sabe solo lo suyo, y las reglas de negocio viven en el servicio.
- El servicio se puede usar sin levantar el servidor.
- Los datos se normalizan al leerse del almacén.
- Has cambiado de almacén sin tocar rutas ni servicio.
- Los errores se traducen a HTTP en un solo sitio.
- El cliente trata todos los errores con una sola función.
Antes de cerrar · 3 minutos, sin mirar
- ¿A qué capa pertenece escribir un 404, la regla del stock y leer el fichero de datos?
- ¿Cómo compruebas que tus capas están separadas de verdad?
- ¿Por qué se normalizan los datos al leerlos del almacén?
- ¿Qué permite sustituir un repositorio por otro?
- ¿Por qué el cliente nunca debe ver la traza de un error?
- ¿Para qué sirve el identificador de petición?
Ver respuestas
1 · El 404 a la ruta, la regla del stock al servicio, la lectura del fichero al repositorio.
2 · Buscando respuesta en el servicio y peticion en el repositorio, y comprobando que cambiar el almacén no obliga a tocar nada más.
3 · Porque el fichero también es entrada externa y puede llegar incompleto o con otra forma.
4 · Que los dos ofrezcan la misma lista de funciones con el mismo comportamiento.
5 · Porque revela detalles internos del servidor sin aportar nada a quien la lee.
6 · Para relacionar lo que ve la persona con la traza completa del registro.
Microprueba semanal 3 · 5–10 minutos
Individual, sin IA y sin apuntes.
- Di a qué capa pertenece cada cosa: escribir un 404, «no se puede borrar un producto con stock», y leer el fichero de datos.
- ¿Cómo compruebas que tus capas están separadas de verdad?
- ¿Por qué el cliente nunca debe ver la traza de un error?