Middleware de validación
La verificación debe ir delante del handler. Para construirla hay que escribir una función que funcione con cualquier ruta, sea cual sea el schema que esa ruta necesite.
Un middleware de Express es una función que se invoca en el camino hacia un handler, con tres argumentos: la solicitud, la respuesta y next, que pasa el control hacia adelante. Entonces lo que necesitamos es una función que produzca uno de esos middlewares, con un schema ya incorporado.
validate(registerSchema) -> middleware que valida contra registerSchema
validate(loginSchema) -> middleware que valida contra loginSchemaUna función que devuelve una función es una función de orden superior, y aquí funciona como una fábrica: se llama una sola vez cuando se configuran las rutas, y el middleware que produce se ejecuta en cada solicitud.
La estructura de la fábrica
Empieza con el esquema general, en back-end/middleware/validate.ts:
import type { Request, Response, NextFunction } from 'express'
import * as z from 'zod'
export function validate(schema: z.ZodType) {
return (req: Request, res: Response, next: NextFunction): void => {
// Validar req.body contra el schema.
}
}Dos capas, que cumplen dos funciones en dos momentos distintos.
La función externa validate se ejecuta una sola vez, mientras la aplicación arranca, y su único argumento es el schema. z.ZodType es la clase base de la que extienden todos los schemas de Zod, así que el parámetro acepta un schema de objeto, un schema de string, lo que sea.
La función interna es la que Express conserva y llama en cada solicitud. Devuelve void porque no retorna ningún valor; o bien responde la solicitud o llama a next.
La externa se ejecuta una sola vez, cuando arranca la aplicación, y su trabajo es recordar el schema. La interna se ejecuta en cada solicitud, y su trabajo es hacer la verificación.
Rechazar una solicitud incorrecta
Dentro de la función interna, ejecuta el schema sobre el body:
const result = schema.safeParse(req.body)safeParse es la opción indicada aquí, porque un fallo es algo que hay que reportar y no una excepción que haya que capturar.
Cuando falla, la respuesta tiene que decir qué campo falló y qué salió mal:
if (!result.success) {
const errors = result.error.issues.map((issue) => ({
field: issue.path[0],
message: issue.message,
}))
res.status(400).json({ success: false, errors })
return
}Pasan tres cosas. Los issues se convierten en pares de campo y mensaje, la respuesta se envía como un 400 que los lleva, y return detiene la función.
Ese return es toda la protección de esto. res.json() envía una respuesta pero sigue ejecutando código, así que sin él la función continúa hacia next() y el handler termina operando sobre datos que acaban de fallar la validación.
res.json() solo hace la primera. El return hace la segunda, y omitirlo es la versión clásica de este error.
Dejar pasar los datos válidos
Todo lo que viene después de ese return solo se ejecuta cuando la validación pasó:
;(req as any).validatedData = result.data
next()Los datos ya parseados se adjuntan al request, y luego next() le pasa el control al handler de la ruta, que lee req.validatedData y nunca toca req.body.
Aquí importa usar result.data en vez de req.body. Zod puede haber convertido un string a número, recortado espacios en blanco, puesto un email en minúsculas o eliminado una clave que el schema no describía. El valor parseado conserva todo eso. El body en crudo no conserva nada de eso.
Aquí está el archivo completo:
import type { Request, Response, NextFunction } from 'express'
import * as z from 'zod'
export function validate(schema: z.ZodType) {
return (req: Request, res: Response, next: NextFunction): void => {
const result = schema.safeParse(req.body)
if (!result.success) {
const errors = result.error.issues.map((issue) => ({
field: issue.path[0],
message: issue.message,
}))
res.status(400).json({ success: false, errors })
return
}
;(req as any).validatedData = result.data
next()
}
}next() es la entrega del control. Hasta que se llama, la solicitud se queda con esta función y no avanza más. Eso es lo que hace que el mecanismo funcione: una solicitud que falla nunca se entrega, así que el código que viene después solo llega a ver datos que ya pasaron la validación.
Ponlo a prueba
Este middleware tiene tres errores. Dos hacen que deje de funcionar; uno es silencioso y peor.
export function validate(schema: z.ZodType) {
return (req: Request, res: Response, next: NextFunction): void => {
const result = schema.safeParse(req.body)
if (!result.success) {
res.status(400).json({ success: false, errors: result.error.issues })
}
;(req as any).validatedData = req.body
next()
}
}Compara tus respuestas
1. Falta el return después del 400. La respuesta se envía, la ejecución continúa, next() se ejecuta, y el handler procesa una solicitud que falló la validación. El cliente ve un rechazo y de todos modos el trabajo se realiza.
2. req.body en el lugar donde debería ir result.data. Aunque se agregue el return, el handler recibe el body en crudo. Se descartan todas las conversiones, recortes y cambios a minúsculas que hacía el schema, y cualquier clave que el schema debería haber eliminado sigue presente.
3. result.error.issues sin procesar en la respuesta. Esto sí funciona, por eso es el silencioso. Los objetos issue de Zod describen tu schema al cliente, incluyendo los tipos esperados y las restricciones que fallaron, y no están pensados para que una persona los lea. Conviértelos en pares de campo y mensaje.
El error 2 es el que vale la pena analizar con calma. El middleware parece funcionar: las solicitudes válidas pasan, las inválidas se rechazan, y las pruebas dan en verde. Lo que desaparece en silencio es cada transformación que hacía el schema, algo que aparece mucho más tarde como datos inconsistentes en la base de datos.
Hacia dónde sigue esto
El middleware está terminado y valida contra cualquier schema que se le entregue. Lo que todavía no tiene es un schema, ni una ruta delante de la cual colocarse.
Construir un endpoint validado escribe ambas cosas, y luego va ampliando el schema campo por campo hasta que cada dato del formulario de registro queda verificado.

