Validación en runtime con Zod

Avanzado
TypeScript
TypeScript
Actualizado: 17/04/2026

Por qué validar en tiempo de ejecución

El sistema de tipos de TypeScript se borra al compilar. Todo dato que entra en la aplicación por la red, por un formulario, por una variable de entorno o por una cola de mensajes llega sin tipar. Aceptar ese dato como si ya tuviera la forma esperada es una de las principales fuentes de errores en producción.

// Peligroso: esto compila pero no hay ninguna garantia
const respuesta = await fetch("/api/usuario")
const usuario: Usuario = await respuesta.json()
usuario.email.toLowerCase() // puede explotar si falta email

La solución profesional consiste en validar en el borde con una librería diseñada para tal fin. Zod se ha impuesto como el estándar del ecosistema TypeScript porque define el esquema y deriva el tipo desde una única fuente de verdad.

Regla práctica: todo dato que cruza una frontera del proceso (red, disco, formulario, env, IPC) debe pasar por un esquema Zod. Dentro del dominio, el tipado estático basta.

Esquemas básicos con Zod

Un esquema describe la forma y las restricciones de un dato. Se construye encadenando métodos sobre el objeto z:

import { z } from "zod"

const EsquemaUsuario = z.object({
    id: z.string().uuid(),
    email: z.string().email(),
    edad: z.number().int().min(0).max(150),
    activo: z.boolean().default(true),
    roles: z.array(z.enum(["admin", "editor", "lector"])).nonempty()
})

type Usuario = z.infer<typeof EsquemaUsuario>
// { id: string; email: string; edad: number; activo: boolean; roles: ["admin" | "editor" | "lector", ...("admin" | "editor" | "lector")[]] }

El helper z.infer extrae el tipo TypeScript resultante. A partir de ese momento, el equipo edita solo el esquema y el tipo queda sincronizado automáticamente.

flowchart LR
    A[Dato externo] --> B[EsquemaUsuario.parse]
    B -->|OK| C[Usuario tipado]
    B -->|Error| D[ZodError con paths]
    C --> E[Logica de dominio]

parse frente a safeParse

Zod ofrece dos formas de ejecutar la validación. parse lanza una excepción ZodError cuando el dato no cumple. safeParse devuelve un resultado discriminado con success: true | false, ideal para flujos donde el error no es excepcional.

import { z } from "zod"

const EsquemaLogin = z.object({
    email: z.string().email(),
    password: z.string().min(8)
})

// Modo que lanza
try {
    const datos = EsquemaLogin.parse(entrada)
    autenticar(datos)
} catch (error) {
    if (error instanceof z.ZodError) {
        console.error(error.issues)
    }
}

// Modo con resultado explicito
const resultado = EsquemaLogin.safeParse(entrada)
if (resultado.success) {
    autenticar(resultado.data)
} else {
    renderizarErroresFormulario(resultado.error.issues)
}

Cada issue incluye path, message y code, lo que permite construir mapas de errores por campo sin código auxiliar.

Refinements y transformaciones

Las restricciones de negocio raramente encajan en los tipos primitivos. Zod permite añadir refinements con refine y superRefine, y transformaciones con transform, manteniendo el tipo derivado.

const EsquemaRegistro = z.object({
    email: z.string().email(),
    password: z.string().min(12),
    confirmacion: z.string()
}).refine((data) => data.password === data.confirmacion, {
    message: "Las contrasenas no coinciden",
    path: ["confirmacion"]
})

const EsquemaFecha = z.string()
    .regex(/^\d{4}-\d{2}-\d{2}$/)
    .transform((valor) => new Date(valor))
    .refine((fecha) => !isNaN(fecha.getTime()), {
        message: "Fecha invalida"
    })

type EntradaFecha = z.infer<typeof EsquemaFecha>
// Date, no string

La transformación cambia el tipo de salida, pero el de entrada permanece. Esto permite aceptar strings del cliente y devolver objetos de dominio ricos en una sola pasada.

Discriminated unions y variantes

Para mensajes o eventos con distintas variantes, la opción idiomática es z.discriminatedUnion. Zod verifica el discriminador primero y afina el error si la variante es incorrecta.

const EsquemaEvento = z.discriminatedUnion("tipo", [
    z.object({ tipo: z.literal("login"), usuarioId: z.string() }),
    z.object({ tipo: z.literal("compra"), pedidoId: z.string(), total: z.number() }),
    z.object({ tipo: z.literal("error"), codigo: z.number(), mensaje: z.string() })
])

type Evento = z.infer<typeof EsquemaEvento>

function procesar(evento: Evento) {
    switch (evento.tipo) {
        case "login":
            return iniciarSesion(evento.usuarioId)
        case "compra":
            return registrarCompra(evento.pedidoId, evento.total)
        case "error":
            return reportarError(evento.codigo, evento.mensaje)
    }
}

Casos típicos en proyectos reales

Validar variables de entorno

Un error de configuración puede tumbar la aplicación en frío. Un esquema de entorno detecta el problema al arrancar:

import { z } from "zod"

const EsquemaEntorno = z.object({
    NODE_ENV: z.enum(["development", "test", "production"]),
    PORT: z.coerce.number().int().min(1).max(65535).default(3000),
    DATABASE_URL: z.string().url(),
    LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info")
})

export const env = EsquemaEntorno.parse(process.env)
// env está tipado y validado antes de que corra nada

Validar la respuesta de una API externa

Cuando se consume una API de terceros, nada garantiza que su contrato se mantenga. Un esquema convierte un fallo silencioso en un error localizado:

const EsquemaTiempo = z.object({
    ciudad: z.string(),
    temperatura: z.number(),
    condicion: z.enum(["soleado", "nublado", "lluvia", "nieve"])
})

async function obtenerTiempo(ciudad: string) {
    const respuesta = await fetch(`https://api.ejemplo.com/clima/${ciudad}`)
    const datos = await respuesta.json()
    return EsquemaTiempo.parse(datos)
}

Alternativas ligeras

Zod no es la única opción. Valibot ofrece una API muy similar con un tamaño de bundle notablemente menor gracias a su enfoque modular, lo que lo hace interesante en aplicaciones web que priorizan el tiempo de carga. ArkType apuesta por sintaxis de tipo TypeScript. Effect Schema se integra con la librería Effect para pipelines funcionales. El criterio de elección suele depender del peso en cliente, de la integración con el framework y de la familiaridad del equipo.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de TypeScript

Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Comprender por qué el tipado estático no cubre los bordes de la aplicación. Definir esquemas primitivos y compuestos con Zod. Derivar tipos estáticos desde esquemas con z.infer. Aplicar parse y safeParse para validar entradas externas. Componer esquemas con refinements, transformaciones y discriminated unions tipadas.