Saltar al contenido principal
Logo de JSCamp

Creando esquemas con Zod para validar nuestra API

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

Suscribirse

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 safeParse para manejar errores sin excepciones.
  • Cómo validar parcialmente con .partial() para PATCH/PUT.