Tres horas. Media hora para decidir qué valida cada ruta y qué código devuelve, y dos horas y media implementando las cinco operaciones de tu recurso según el contrato de ayer.
Hoy · Hoja de ruta
- 1. Aprende: Cómo se implementan las lecturas con filtros validados, cómo se recibe y se valida un recurso nuevo, y qué distingue reemplazar de modificar.
- 2. Haz: Las cinco operaciones de tu recurso, con sus errores y sus códigos.
- 3. Comprueba: Ninguna entrada inválida llega a tocar los datos, y repetir una operación no produce efectos distintos.
Antes de empezar · 5 minutos, sin apuntes
- ¿Qué debe devolver
/api/productos?max=abc? ¿Y?categoria=inexistente? - ¿Qué pasaría si alguien envía un campo
iden la creación? - ¿Puede el cliente saltarse tu validación de la UD4?
Se explica
25 minutos · conceptos y demostración
Hoy se implementa el contrato de ayer. Todo lo que aparece se reduce a dos preguntas que se repiten en cada ruta: qué entra y quién lo comprueba, y qué código dice lo que ha pasado.
Leer: la ruta recoge, el servicio decide
export async function listarProductos(peticion, respuesta, next) {
try {
const { categoria, max, orden, q } = peticion.query;
const productos = await servicio.listar({ categoria, max, orden, busqueda: q });
respuesta.json(productos);
} catch (error) {
next(error);
}
}
La ruta no filtra: recoge los parámetros y se los pasa al servicio. Las funciones que hacen el trabajo son las de la UD3, que no saben nada de HTTP, y por eso las mismas sirven aquí y en el CLI.
Los parámetros de la consulta llegan como texto, igual que en la UD4:
const maximo = max === undefined ? null : Number(max);
if (maximo !== null && (Number.isNaN(maximo) || maximo < 0)) {
throw new ErrorDeValidacion([{ campo: "max", mensaje: "Debe ser un número no negativo" }]);
}
Un filtro inválido es un 400; un filtro sin resultados es un 200 vacío
Pedir max=abc es una petición mal formada: 400 con su detalle. Pedir una categoría que existe pero no tiene productos es una petición perfectamente válida cuya respuesta es una lista vacía, con 200.
Devolver 404 por una lista vacía es un error de diseño frecuente: la colección existe, y el cliente sabe leer un array de cero elementos.
En la obtención por identificador conviven los dos errores del contrato, dentro del mismo try que delega en next:
const id = Number(peticion.params.id);
if (!Number.isInteger(id) || id < 1) {
throw new ErrorDeValidacion([{ campo: "id", mensaje: "Debe ser un entero positivo" }]);
}
const producto = await servicio.obtener(id);
if (!producto) throw new ErrorNoEncontrado("Producto no encontrado");
Una función decide, por último, qué sale, en lugar de devolver el objeto tal como está guardado:
function aRespuesta({ id, nombre, precio, categoria, stock }) {
return { id, nombre, precio, categoria, disponible: stock > 0 };
}
Devolver el objeto en su forma almacenada resulta inmediato y expone información que no corresponde publicar: notas internas, márgenes o el propio stock. Una función que decide la forma pública del recurso deja explícito qué sale, y evita que añadir un campo interno lo publique sin querer.
Crear: la validación, en tres pasos
const CAMPOS_PERMITIDOS = ["nombre", "precio", "categoria", "stock", "descripcion"];
export function validarProductoNuevo(cuerpo) {
if (cuerpo === null || typeof cuerpo !== "object" || Array.isArray(cuerpo)) {
throw new ErrorDeValidacion([{ campo: "cuerpo", mensaje: "Se esperaba un objeto JSON" }]);
}
const errores = [];
const limpio = {};
// 1 · solo lo permitido entra
for (const campo of CAMPOS_PERMITIDOS) {
if (cuerpo[campo] !== undefined) limpio[campo] = cuerpo[campo];
}
// 2 · obligatorios y tipos
if (typeof limpio.nombre !== "string" || limpio.nombre.trim() === "") {
errores.push({ campo: "nombre", mensaje: "Es obligatorio" });
}
const precio = Number(limpio.precio);
if (Number.isNaN(precio) || precio <= 0) {
errores.push({ campo: "precio", mensaje: "Debe ser un número mayor que cero" });
}
// 3 · todos los errores a la vez
if (errores.length > 0) throw new ErrorDeValidacion(errores);
return { ...limpio, nombre: limpio.nombre.trim(), precio };
}
Lista blanca, no lista negra
Copiar el cuerpo entero al objeto guardado —lo que hace un { ...peticion.body } sin filtrar— permite a quien llama meter campos que tú no habías previsto: un id que pisa el tuyo, un rol que no debería poder tocar, o basura que se queda en tus datos para siempre.
Enumera los campos que aceptas y descarta el resto. Es una línea más y cierra una familia entera de agujeros.
La respuesta incluye el recurso creado y la cabecera Location con su URL, porque quien crea no elige el identificador: lo asigna el servidor. Por eso id no está entre los campos permitidos.
const datos = validarProductoNuevo(peticion.body); // lanza si no vale
const creado = await servicio.crear(datos);
respuesta
.status(201)
.location(`/api/productos/${creado.id}`)
.json(aRespuesta(creado));
- Nativa · comodidad
- Cliente · buena experiencia
- Servidor · la única obligatoria
Cualquiera puede llamar a tu API con un cliente HTTP y saltarse las dos primeras. Lo comprobarás hoy mismo.
Modificar y borrar
| Método | Significa | Cuerpo |
|---|---|---|
| PUT | Reemplaza el recurso entero | Todos los campos |
| PATCH | Modifica los campos enviados | Solo los que cambian |
// PUT: lo que no llega, se pierde
const reemplazo = validarProductoNuevo(peticion.body);
// PATCH: se combina con lo que había
const cambios = validarCambios(peticion.body); // ninguno obligatorio
El error clásico es implementar PUT combinando los campos: entonces tienes dos rutas que hacen lo mismo y un contrato que miente. Si solo vas a ofrecer una, ofrece PATCH y dilo en el contrato.
const borrado = await servicio.borrar(id);
if (!borrado) throw new ErrorNoEncontrado("Producto no encontrado");
respuesta.status(204).end();
Un 204 no lleva cuerpo: la operación ha ido bien y no hay nada que devolver. Sobre el segundo borrado existen dos posturas defendibles —404 porque ya no está, o 204 porque el resultado deseado se cumple— pero elige una y escríbela en el contrato.
Leer, modificar y guardar no es una operación indivisible
Dos peticiones que llegan casi a la vez leen la misma versión del fichero, cada una aplica su cambio y la segunda escribe encima: el cambio de la primera desaparece sin que nadie se entere. Es la carrera que demostraste en la UD5, ahora con dos clientes en lugar de dos procesos.
Se resuelve con una versión en el recurso, que el cliente devuelve al modificar: si no coincide, el servidor responde 409 en lugar de pisar. Es el mismo problema que en el módulo de servidor se resuelve con transacciones.
Se trabaja
150 minutos · las cinco operaciones
Los tres primeros pasos implementan el contrato. Los dos últimos lo atacan: uno se salta tu propio cliente, y el otro comprueba la propiedad que permite reintentar sin miedo.
Paso 1 · Las lecturas completas · 35 min
- Implementa la lista con filtro por categoría, texto y precio máximo.
- Añade orden por dos campos, ascendente y descendente.
- Implementa la obtención por identificador con sus dos errores.
- Valida todos los parámetros y responde 400 con detalles.
- Escribe la función que decide la forma pública del recurso, y comprueba que un campo interno nuevo no aparece en la respuesta.
- Añade paginación con
limiteypagina, devolviendo también el total.
Antes de continuar: ?categoria=inexistente devuelve 200 con lista vacía y ?max=abc devuelve 400. Si los dos dan lo mismo, la distinción no está hecha.
Paso 2 · La creación · 40 min
- Implementa
POST /api/productoscon validación en tres pasos. - Devuelve 201,
Locationy el recurso creado. - Devuelve 400 con todos los errores, no solo el primero.
- Descarta los campos no permitidos y demuéstralo enviando un
idy un campo inventado. - Comprueba qué ocurre si el cuerpo no es JSON válido, y que da 400 y no 500.
- Responde 409 si ya existe un producto con el mismo nombre.
Paso 3 · El CRUD cerrado · 40 min
- Implementa PATCH con validación de los campos enviados, ninguno obligatorio.
- Decide si ofreces PUT; si lo haces, que reemplace de verdad.
- Implementa DELETE con 204 y su comportamiento documentado para el segundo borrado.
- Añade una fecha de modificación que mantiene el servidor, no el cliente.
- Impide modificar campos que el cliente no debería tocar, como el identificador o la fecha de creación.
- Actualiza
peticiones.httpcon todos los casos nuevos.
Paso 4 · Sáltate tu propio cliente · 20 min
Tu formulario de la UD4 valida. Comprueba qué pasa cuando alguien no lo usa: desde el fichero .http, envía estas peticiones directamente a la API.
| Petición enviada a mano | Qué debería responder | Qué responde | Corrección |
|---|---|---|---|
| Producto con precio negativo | |||
| Producto sin nombre | |||
Producto con "id": 1 incluido |
|||
| Producto con un campo inventado | |||
Producto con "precio": "veinte" |
|||
| Cuerpo que no es un objeto, sino un array | |||
| Cuerpo vacío |
Las dos últimas son las que se olvidan. Ninguna de las siete debe devolver 500, y ninguna debe llegar a escribir en el fichero de datos.
Escribe al terminar, en una línea, qué habría pasado si tu única validación fuera la del formulario.
Paso 5 · Repetir sin consecuencias · 15 min
- Ejecuta dos veces seguidas el mismo PATCH. Compara el recurso después de la primera y de la segunda.
- Ejecuta dos veces el mismo DELETE. Anota qué responde cada una.
- Ejecuta dos veces el mismo POST. Cuenta los productos.
- Rellena la tabla con lo observado y compárala con la del contrato de la sesión 1.
| Operación | Primera vez | Segunda vez | ¿Coincide con el contrato? |
|---|---|---|---|
| GET | |||
| PUT o PATCH | |||
| DELETE | |||
| POST |
- Explica en dos líneas por qué esa propiedad importa cuando la red falla y el cliente reintenta.
Ampliación si has completado el trabajo
Primero termina y comprueba los cinco pasos. El primer reto convierte la validación en dato; el segundo resuelve la carrera que arrastras desde la UD5.
Reto 1 · Las reglas de validación, como tabla
Tu validarProductoNuevo tiene las reglas escritas a mano. Con dos recursos más, serán tres funciones casi idénticas.
const ESQUEMA_PRODUCTO = {
nombre: { tipo: "texto", obligatorio: true, minimo: 2, maximo: 120 },
precio: { tipo: "numero", obligatorio: true, minimo: 0.01 },
categoria: { tipo: "texto", obligatorio: true, valores: ["teclados", "ratones"] },
stock: { tipo: "entero", obligatorio: false, minimo: 0, porDefecto: 0 },
descripcion: { tipo: "texto", obligatorio: false, maximo: 500 }
};
- Escribe
validar(cuerpo, esquema)que recorra el esquema, aplique cada regla y devuelva los datos limpios o lance con todos los errores. - Que la lista blanca salga del propio esquema: lo que no esté declarado, se descarta.
- Aplica los valores por defecto de los campos opcionales ausentes.
- Escribe un segundo esquema para PATCH, donde nada sea obligatorio, derivándolo del primero en lugar de repetirlo.
- Añade un tipo nuevo —
fecha— sin tocar la funciónvalidar. Si has tenido que tocarla, el diseño no era genérico. - Escribe el esquema del segundo recurso de tu API y comprueba que la misma función sirve. Anota en tres líneas qué has ganado y qué has perdido respecto a los
ifescritos a mano: hay una pérdida real y conviene nombrarla.
Reto 2 · Dos clientes que se pisan
Reproduce la carrera de la UD5, ahora con dos peticiones HTTP en lugar de dos procesos.
- Pide el mismo producto desde dos clientes. Los dos tienen ahora la misma versión.
- Desde el primero, envía un PATCH que cambie el precio. Desde el segundo, sin volver a leer, envía otro que cambie el stock.
- Consulta el producto. Uno de los dos cambios ha desaparecido. Explica exactamente por qué, siguiendo el orden de lectura y escritura.
- Añade al recurso un campo de versión que el servidor incrementa en cada modificación y devuelve en cada lectura.
- Haz que PATCH exija esa versión y responda 409 si no coincide con la actual. Decide si va en el cuerpo o en una cabecera, y busca cuál es la cabecera estándar para esto.
- Repite el apartado 2 con la protección puesta. El segundo cliente debe recibir un 409, y su mensaje debe decirle qué hacer: volver a leer y reintentar.
- Explica en tres líneas por qué esta estrategia se llama optimista, y cuál sería la alternativa pesimista. La segunda es la que verás en el módulo de servidor.
Cierre
5 minutos · comprobación y recuerdo
Lista de verificación de la sesión
- La ruta recoge y valida; el servicio decide.
- Un filtro inválido responde 400 y una búsqueda sin resultados, 200 con lista vacía.
- Solo entran los campos permitidos, y el identificador lo asigna el servidor.
- Se devuelven todos los errores de validación de una vez.
- Decides explícitamente qué campos salen en la respuesta.
- Ninguna de las peticiones hostiles devuelve 500 ni escribe en los datos.
Antes de cerrar · 3 minutos, sin mirar
- ¿Qué devuelve
?max=abcy qué devuelve?categoria=inexistente? - ¿Por qué se decide explícitamente qué campos salen?
- ¿Qué es una lista blanca de campos y qué evita?
- ¿Qué devuelve una creación correcta?
- Diferencia entre PUT y PATCH.
- ¿Qué ocurre si dos clientes modifican el mismo recurso a la vez?
Ver respuestas
1 · Un 400 el primero, porque el parámetro está mal formado; un 200 con lista vacía el segundo, porque la petición era válida.
2 · Para no publicar campos internos al añadirlos al modelo.
3 · Enumerar los campos que se aceptan y descartar el resto; evita que quien llama introduzca campos no previstos.
4 · Un 201 con Location y el recurso creado.
5 · Con PUT los campos que no llegan se pierden; con PATCH se conservan.
6 · Que la segunda escritura pise a la primera sin que nadie lo note, salvo que haya una versión que lo detecte.
Microprueba semanal 2 · 5–10 minutos
Individual, sin IA y sin apuntes.
/api/productos?max=abcy/api/productos?categoria=inexistente: qué código devuelve cada uno y por qué.- Explica qué es una lista blanca de campos y qué evita.
- Diferencia entre PUT y PATCH, con un ejemplo de tu proyecto.