De Handlerstein a Handler: por qué Zod debe vivir en tu middleware
Capítulo 1: El Handlerstein y la Maldición del SRP
Desarrollar serverless no nos hace inmunes al Principio de Responsabilidad Única (SRP). Basta con revisar mis primeros códigos para ver cómo un requerimiento sencillo termina mutando en un handlerstein: múltiples validaciones, reglas de negocio que parecen sacadas bajo la manga, manejo de errores para casos específicos que podrían generalizarse y mucho "pegamento" para que todo viva en armonía.
El problema radica en que el software es un ente vivo porque los requisitos del cliente son cambiantes. No ser ordenados en un inicio puede significar un caos total en el futuro.
La función del handler es clara: recibir la solicitud, delegar acciones y retornar una respuesta. A priori no seguir este "mantra" parece ser inofensivo, después de todo es mucho más práctico centralizarlo. Pero este atajo se convierte en una trampa: código repetitivo fácil de romper, difícil de testear e imposible mantener. Me apiado del próximo que deba mantener ese código y tengo pesadillas cada noche esperando que esa próxima persona no sea yo.
Capítulo 2: Confesiones de un Handler Pecador (y Cómo NO Hacerlo)
Lo sé, ya hemos vista mucha teoría pero lo que quieren aquí es leer código. Este snippet es un handler antiguo que desarrollé hace unos años y que estuvo en producción (¿o está? dejo la duda abierta). Lo modifiqué para proteger la privacidad del cliente (y mi estabilidad laboral), pero no se preocupen, mantiene intactos sus pecados capitales.
export const handlerReportes = async event => {
try {
// ❌ Validación manual de headers para el auth (debería ser middleware)
const auth = new HandlerToken(event.headers.authorization);
auth.isValidToken();
let rawData = [] as ReporteBaseInterface[];
const reporteService = new ReporteService(new Database(DatabaseConfig));
// ❌ Parseo manual de query params (debería ser middleware)
if (!event.queryStringParameters) {
rawData = await reporteService.findAllGroupByCategoria();
} else {
const queryParams = HandlerEvent.getQueryParams<{ date: string }>(
event
);
rawData = await reporteService.findAllGroupByCategoria(
queryParams.date
);
}
// ❌ Transformación manual con try/catch (debería ser service o helper)
const failedItems = [];
const successfulItems = rawData
.map(item => {
try {
const model = new ReporteBaseModel(item);
return {
valor: model.valor,
categoria: model.categoria
// ...
};
} catch (error) {
failedItems.push({
...item,
error: { message: error.message }
});
}
})
.filter(Boolean);
return {
ok: true,
data: {
successfulItems,
failedItems
}
};
} catch (error) {
// ❌ Manejo genérico de errores (debería ser middleware)
return { ok: false, error: error.message };
}
}
Si bien este handler tiene material suficiente para un juicio por mis crímenes contra el SRP y evidencias contundentes para condenarme por al menos a 5 cadenas perpetuas en nombre del Clean Code, nos enfocaremos en el núcleo del artículo: externalizar validaciones con Zod.
Capítulo 3: Zod, el Exorcista de los Handlers
Zod es una librería de Node que va más allá de validar datos: nos permite definir schemas para garantizar que la información cumpla con reglas específicas tanto en desarrollo como en producción.
Se instala fácilmente con este comando
$ npm i zodCrearemos los schemas de Query Params y Headers utilizando Zod.
import { z } from "zod";
export const QueryParamsSchema = z.object({
date: z.string().optional()
}).strict();
export const HeadersSchema = z.object({
authorization: z.string().regex(/^Bearer \w+$/)
}).strict();
// ❌ Antes (validación manual)
const auth = new HandlerToken(headers.authorization);
const queryParams = HandlerEvent.getQueryParams<{ date: string }>(event);
// ✅ Ahora (validación con Zod)
const { authorization } = HeadersSchema.parse(event.headers);
const { date } = QueryParamsSchema.parse(event.queryStringParameters || {});
Ahora el código anterior donde hacíamos validaciones manuales se transforma en uno más fácil de leer y mantener, editando solo los schemas en caso de que el cliente necesite añadir un nuevo valor en el query param o en el header para hacer alguna búsqueda. Además, nos entrega las siguientes ventajas:
- Código más declarativo y fácil de leer.
- Tipado automático ya que TypeScript sabe que authorization es string y date es string o undefined.
- Validación detallada con mensajes de error claros.
Sé que hay detalles que se están omitiendo, pero por motivos de simplicidad lo dejaré de esta forma.
Aunque hemos mejorado mucho la validación, todavía hay un problema: el handler sigue siendo responsable de validar los datos. ¿No sería mejor que esto ocurriera automáticamente antes de ejecutar la lógica principal? Ahí es donde entra el segundo protagonista de este artículo: La librería Middy.
Capítulo 4: Middy, El Jedi Silencioso de la Orquestación Serverless
Middy será nuestro héroe silencioso, aquel que trabaja desde las sombras en el mundo de serverless. Si hablamos en el contexto del desarrollo de las APIs, un middleware es una pieza de código que intercepta una solicitud antes o después del handler. Esto es muy útil si lo que queremos es validar los datos de entrada (como en este caso los query params y el header), pero también se puede utilizar para transformar la data, ejecutar rate limits, manejar errores, entre otras cosas.
¿Por qué Middy? Es la librería estándar para AWS Lambda en Node, con una sintaxis similar a Express pero optimizada para serverless.
Se instala fácilmente con estos comandos:
$ npm i @middy/core
$ npm i -D @types/aws-lambda # Solo para typescriptVamos a tomar nuestro handler actual (donde Zod validaba manualmente) y moveremos esas validaciones a middlewares específicos. Con esto, se dedicará solo a una cosa: orquestar la lógica de negocio.
import { ZodSchema } from "zod";
export const validateHeaders = (schema: ZodSchema) => ({
before: async request => {
const validateData = schema.parse(request.event.headers);
request.event.headers = validateData;
// Aquí podría ir la lógica de validación
},
});
export const validateQueryParams = (schema: ZodSchema) => ({
before: async request => {
const validateData = schema.parse(
request.event.queryStringParameters || {}
);
request.event.queryStringParameters = validateData;
},
});
Y ahora solo nos queda eliminar las validaciones del handler para añadirlas como middleware utilizando Middy
const handlerReportes = async event => {
try {
const { date } = event.queryStringParameters;
let rawData = [] as ReporteBaseInterface[];
const reporteService = new ReporteService(new Database(DatabaseConfig));
rawData = await reporteService.findAllGroupByCategoria(date);
// ...
}
catch(error){
// ...
}
}
export const handler = middy(handlerReportes)
.use(validateHeaders(HeadersSchema))
.use(validateQueryParams(QueryParamsSchema))
Ahora el handler es mucho más fácil de leer, las responsabilidades están más separadas, los middlewares creados se pueden volver a utilizar en otros handlers y es posible testear más partes de nuestro código de forma aislada.
Como verán, Zod y Middy son unos aliados implacables a la hora de desexorcizar un handlerstein y convertirlo nuevamente en un handler. Pasamos de un gigantesco código que hacía demasiadas acciones de forma dudosa a uno donde se puede apreciar mejor las responsabilidades de cada uno.
Capítulo 5: El Camino Jedi Continúa. Ejercicios y Alternativas
Cabe recalcar que esta es solo una versión simplificada, porque, seamos sinceros, en producción hay muchas más cosas que validar. Les dejo un par de ejercicios e ideas que pueden hacer con ayuda de estas dos librerías:
- "¿Y dónde está mi validación del body?": Podrías crear un validateBodyMiddleware siguiendo la nomenclatura de los otros middlewares.
- "Demasiados middlewares, tener uno header, query parameters y body me vuele loco ¿no existe otra alternativa?": Intenta crear un validateRequest unificado que acepte schemas para body/headers/queries.
- "No me gusta Zod, ¿tengo más alternativas?": Zod es solo una de las tantas librerías que permiten validar datos, pero existen otras como Joi, Ajv o Jup. Puedes crear tus propios middlewares utilizando las librerías de validación que más te gusten.
- "Mis queryparams pierden tipado": Podrías investigar qué tipado debe tener
eventy hacerlo cuadrar con el que entrega un schema de Zod.- HINT: z.infer(<typeof ...>)
- "¿Se puede rescatar algo de aquí para tener algo de seguridad en mis endpoints?": Justamente puedes utilizar middy para añadir un rate limit y así evitar un ataque DDoS.
- HINT: rate-limiter-flexible
Esta solución es parte de mi camino Jedi para llevar el handler al lado luminoso de la fuerza, pero sé que no es el único válido: si dominas otras técnicas de la Orden (o incluso las artes oscuras Sith), podrías dejármelo en los comentarios. La rebelión contra el código spaguetti en serverless necesita todas las tropas posibles, por eso recuerda: Zod y Middy son tus aliados para mantener el orden en la galaxia serverless. Que la fuerza del SRP te acompañe.
Referencias:
Cap. 8 . "Una clase debe tener una sola razón para cambiar": Esto aplica directamente a handlers. Si validas, parseas y orquestas en el mismo lugar, el refactor será inevitable.


Member discussion