Añadimos validación de datos en runtime con Zod: definimos esquemas, usamos safeParse y preparamos validación parcial para PATCH/PUT.
midudev
Más clases y directos en YouTube
Creando esquemas con Zod para validar nuestra API
Nuestra API ya “funciona”, pero hay un problema serio: no estamos validando nada. Eso significa que cualquier cliente puede enviarnos datos rotos, incompletos o con formato incorrecto… y nosotros los aceptamos como si nada. Resultado: bugs, datos inconsistentes y dolores de cabeza.
En esta clase solucionamos esto con Zod, una librería de validación por esquemas que nos permite definir de forma declarativa cómo deben ser los datos que entran a la API.
Por qué la validación es responsabilidad del controlador
Aunque usemos MVC y tengamos la API “ordenadita”, si el controlador no valida, realmente no está controlando. Antes de llamar al modelo, debemos comprobar que los datos cumplen las reglas de negocio básicas: tipos, requeridos, límites, etc.
Zod vs TypeScript: build time vs run time
Una duda típica: “si luego usamos TypeScript, ¿para qué Zod?”
- TypeScript valida en build time (durante el desarrollo/compilación).
- Zod valida en run time (cuando la API ya está levantada y recibe datos del exterior).
TypeScript no puede “adivinar” si el JSON que te llega por HTTP es válido. Zod sí puede comprobarlo en ejecución.
Instalación y estructura recomendada
Instalamos Zod y creamos una carpeta para separar los esquemas:
npm i zod
Estructura sugerida:
src/├── controllers/├── models/├── routes/└── schemas/ # Aquí vivirán nuestros esquemas Zod
Creando el esquema de Job
Vamos a crear un archivo src/schemas/jobs.js (o .ts si ya estás con TypeScript) donde definimos el esquema del recurso job.
// src/schemas/jobs.jsimport { z } from 'zod'export const jobSchema = z.object({ title: z .string() .min(3, { message: 'El título debe tener al menos 3 caracteres' }) .max(100, { message: 'El título no puede exceder los 100 caracteres' }), company: z.string(), location: z.string(), // opcional: no obligamos a que venga siempre description: z.string().optional(), data: z.object({ technology: z.array(z.string()), modality: z.string(), level: z.string(), }),})
Ideas extra (según tu dominio)
Zod te permite ir más allá de “string” y ser más específico:
Enums para limitar valores posibles:
const modalitySchema = z.enum(['remoto', 'presencial', 'hibrido'])
Objetos anidados y validaciones compuestas, sin montar un infierno de if.
Validar sin explotar la API: safeParse
Ahora que tenemos el esquema, creamos funciones helper para validación.
¿Por qué safeParse? Porque no lanza excepción: devuelve un objeto tratable, ideal para responder con un 400 sin romper nada.
// src/schemas/jobs.jsimport { z } from 'zod'export const jobSchema = z.object({ title: z.string().min(3).max(100), company: z.string(), location: z.string(), description: z.string().optional(), data: z.object({ technology: z.array(z.string()), modality: z.string(), level: z.string(), }),})export const validateJob = (input) => { return jobSchema.safeParse(input)}
Validación parcial para PATCH/PUT
Cuando actualizamos parcialmente (por ejemplo con PATCH), no queremos obligar a que venga todo el objeto: solo lo que se actualiza.
Zod lo hace fácil con .partial():
// src/schemas/jobs.jsexport const validatePartialJob = (input) => { return jobSchema.partial().safeParse(input)}
Esto permite casos como “solo quiero actualizar el title” sin tener que enviar company, location, etc.
Usando la validación en el controlador
Ejemplo de create (POST) usando validateJob:
// src/controllers/jobs.jsimport { validateJob } from '../schemas/jobs.js'export const createJob = (req, res) => { const result = validateJob(req.body) if (!result.success) { return res.status(400).json({ message: 'Datos inválidos', errors: result.error.format(), }) } // result.data contiene el input ya validado const job = result.data // ... aquí llamas al modelo para crear el job return res.status(201).json(job)}
Ejemplo de update parcial (PATCH) usando validatePartialJob:
// src/controllers/jobs.jsimport { validatePartialJob } from '../schemas/jobs.js'export const updateJob = (req, res) => { const result = validatePartialJob(req.body) if (!result.success) { return res.status(400).json({ message: 'Datos inválidos', errors: result.error.format(), }) } const partialUpdate = result.data // ... aquí llamas al modelo para aplicar el update parcial return res.json({ ok: true, updates: partialUpdate })}
Ventajas de hacerlo así
- Menos bugs: no entran datos rotos.
- Código más limpio: adiós a los if infinitos.
- Errores consistentes: mensajes claros y controlables.
- Listo para escalar: nuevos recursos, nuevos esquemas, misma estrategia.
Lo que hemos aprendido
- Por qué una API debe validar datos en runtime.
- Cómo definir esquemas declarativos con Zod.
- Diferencia clave entre TypeScript (build time) y Zod (run time).
- Cómo usar
safeParsepara manejar errores sin excepciones. - Cómo validar parcialmente con
.partial()para PATCH/PUT.
