Zod na Prática: Validação de Dados Type-First com TypeScript do Zero ao Avançado

O que é Zod?
Zod é uma biblioteca de validação de dados type-first para TypeScript que permite definir esquemas de validação de forma declarativa e inferir tipos automaticamente. Criada por Colin McDonnell, a Zod se tornou o padrão da indústria para validação de dados no ecossistema TypeScript, com mais de 30 milhões de downloads semanais no npm.
Diferente de bibliotecas como Joi ou Yup, a Zod foi projetada desde o início com TypeScript em mente, oferecendo inferência de tipos impecável e uma API minimalista que elimina a necessidade de manter tipos e validadores em sincronia manualmente.
Por que Zod?
Em aplicações modernas, os dados chegam de diversas fontes — APIs externas, formulários, arquivos de configuração, bancos de dados. Cada uma dessas fontes pode enviar dados inesperados ou malformados. Sem validação robusta, você corre o risco de propagar dados inválidos pelo sistema, causando bugs difíceis de rastrear.
A Zod resolve este problema com:
- Inferência automática de tipos: O tipo TypeScript é derivado do esquema, não o contrário
- API encadeável: Esquemas compostos de forma intuitiva
- Mensagens de erro customizáveis: Controle total sobre as mensagens retornadas
- Zero dependências: Leve e rápido
- Suporte a parsing assíncrono: Validações que dependem de dados externos
Instalação
Adicionar Zod ao seu projeto é simples com npm ou yarn:
npm install zod
# ou
yarn add zod
# ou
pnpm add zod
Não é necessário configurar nada — a Zod funciona out-of-the-box com TypeScript 4.1+.
Primeiros Passos: Esquemas Básicos
Vamos começar com exemplos práticos de esquemas fundamentais:
import { z } from "zod";
// String
const nomeSchema = z.string();
type Nome = z.infer<typeof nomeSchema>; // string
// Número com validações
const idadeSchema = z.number().min(0).max(150);
type Idade = z.infer<typeof idadeSchema>; // number
// Booleano
const ativoSchema = z.boolean();
// Validação de email
const emailSchema = z.string().email("Email inválido");
// Validação de URL
const urlSchema = z.string().url();
// Enum
const StatusSchema = z.enum(["ativo", "inativo", "pendente"]);
type Status = z.infer<typeof StatusSchema>;
// "ativo" | "inativo" | "pendente"
Objetos e Composição de Esquemas
O poder da Zod aparece na validação de objetos complexos. Vamos modelar um usuário:
const UsuarioSchema = z.object({
id: z.string().uuid(),
nome: z.string().min(3, "Nome deve ter no mínimo 3 caracteres").max(100),
email: z.string().email("Formato de email inválido"),
idade: z.number().int().positive().optional(),
endereco: z.object({
rua: z.string().min(1),
cidade: z.string().min(1),
cep: z.string().regex(/^\d{5}-\d{3}$/, "CEP deve seguir o formato 00000-000"),
}).optional(),
roles: z.array(z.enum(["admin", "user", "moderator"])).min(1),
});
type Usuario = z.infer<typeof UsuarioSchema>;
// O tipo Usuario é inferido AUTOMATICAMENTE do esquema!
Parsing e Tratamento de Erros
A Zod oferece três estratégias para validar dados:
parse() — Modo Eager
Lança uma exceção detalhada se os dados forem inválidos:
try {
const usuario = UsuarioSchema.parse(dadosRecebidos);
// usuario possui tipo Usuario — totalmente tipado
console.log(usuario.nome);
} catch (erro) {
if (erro instanceof z.ZodError) {
console.error("Erros de validação:", erro.errors);
// erro.errors é um array de { path, message, code }
}
}
safeParse() — Modo Seguro (Recomendado)
Retorna um objeto com sucesso/erro, sem lançar exceções:
const resultado = UsuarioSchema.safeParse(dadosRecebidos);
if (resultado.success) {
// resultado.data é Usuario
console.log("Dados válidos:", resultado.data);
} else {
// resultado.error é ZodError
console.log("Erros:", resultado.error.format());
/* Formato:
{
_errors: [],
nome: { _errors: ["Nome deve ter no mínimo 3 caracteres"] },
email: { _errors: ["Formato de email inválido"] }
}
*/
}
Transformação e Refinamento
A Zod permite transformar dados durante a validação, criando pipelines poderosos:
const EmailLimpoSchema = z.string()
.email()
.transform(email => email.toLowerCase().trim());
const SlugSchema = z.string()
.transform(slug => slug
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
);
// Refinamento personalizado
const SenhaForteSchema = z.string()
.min(8, "Senha deve ter no mínimo 8 caracteres")
.refine(senha => /[A-Z]/.test(senha), {
message: "Senha deve conter pelo menos uma letra maiúscula",
})
.refine(senha => /[0-9]/.test(senha), {
message: "Senha deve conter pelo menos um número",
});
SuperRefine: Validação Avançada
Para validações complexas que dependem de múltiplos campos, use superRefine:
const CadastroSchema = z.object({
senha: z.string().min(8),
confirmacaoSenha: z.string(),
email: z.string().email(),
}).superRefine((dados, ctx) => {
if (dados.senha !== dados.confirmacaoSenha) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Senhas não conferem",
path: ["confirmacaoSenha"],
});
}
});
Integração com React Hook Forms
Uma das aplicações mais comuns da Zod é com formulários React. A integração com React Hook Form é nativa:
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const FormularioSchema = z.object({
nome: z.string().min(3, "Nome muito curto"),
email: z.string().email("Email inválido"),
idade: z.coerce.number().min(18, "Idade mínima: 18 anos"),
});
type FormData = z.infer<typeof FormularioSchema>;
function MeuFormulario() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(FormularioSchema),
});
const onSubmit = (data: FormData) => {
console.log("Dados válidos:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("nome")} placeholder="Nome" />
{errors.nome && <span>{errors.nome.message}</span>}
<input {...register("email")} placeholder="Email" />
{errors.email && <span>{errors.email.message}</span>}
<input type="number" {...register("idade")} placeholder="Idade" />
{errors.idade && <span>{errors.idade.message}</span>}
<button type="submit">Enviar</button>
</form>
);
}
Validação de APIs com Zod
Em APIs REST ou GraphQL, a Zod é perfeita para validar dados de entrada no servidor:
// Exemplo com Next.js API Route
import { z } from "zod";
import { NextResponse } from "next/server";
const CriarUsuarioSchema = z.object({
nome: z.string().min(3).max(100),
email: z.string().email(),
tipo: z.enum(["free", "pro", "enterprise"]).default("free"),
});
export async function POST(request: Request) {
try {
const body = await request.json();
const dados = CriarUsuarioSchema.parse(body);
// dados está totalmente tipado como:
// { nome: string; email: string; tipo: "free" | "pro" | "enterprise" }
const usuario = await criarUsuario(dados);
return NextResponse.json(usuario, { status: 201 });
} catch (erro) {
if (erro instanceof z.ZodError) {
return NextResponse.json(
{ erro: "Dados inválidos", detalhes: erro.errors },
{ status: 400 }
);
}
return NextResponse.json(
{ erro: "Erro interno" },
{ status: 500 }
);
}
}
Zod + TypeScript: A Magia da Inferência
O grande diferencial da Zod é a inferência de tipos. Veja como ela elimina duplicação:
// ❌ ABORDAGEM TRADICIONAL: tipos e validadores separados
type Produto = {
id: string;
nome: string;
preco: number;
categorias: string[];
};
function validarProduto(dados: unknown): dados is Produto {
return (
typeof dados === "object" &&
dados !== null &&
typeof dados.id === "string" &&
// ... dezenas de linhas de validação manual
);
}
// ✅ COM ZOD: um esquema gera tipo + validador
const ProdutoSchema = z.object({
id: z.string().uuid(),
nome: z.string().min(1).max(200),
preco: z.number().positive(),
categorias: z.array(z.string()).min(1),
});
type Produto = z.infer<typeof ProdutoSchema>;
// ✅ ProdutoSchema.parse() valida em tempo de execução
// ✅ type Produto é inferido em tempo de compilação
// ✅ Um único schema mantém tudo sincronizado
Casos de Uso Avançados
Discriminated Unions
Para dados que podem ter diferentes formatos dependendo de um campo discriminador:
const EventoSchema = z.discriminatedUnion("tipo", [
z.object({
tipo: z.literal("click"),
elemento: z.string(),
coordenadas: z.tuple([z.number(), z.number()]),
}),
z.object({
tipo: z.literal("submit"),
formulario: z.string(),
dados: z.record(z.string()),
}),
z.object({
tipo: z.literal("navegacao"),
destino: z.string().url(),
}),
]);
type Evento = z.infer<typeof EventoSchema>;
// Evento é: { tipo: "click"; elemento: string; coordenadas: [number, number] }
// | { tipo: "submit"; formulario: string; dados: Record<string, string> }
// | { tipo: "navegacao"; destino: string }
z.coerce — Coerção Automática
Converta strings para números automaticamente em formulários:
const InputSchema = z.object({
idade: z.coerce.number().int().min(0).max(150),
preco: z.coerce.number().positive(),
ativo: z.coerce.boolean(),
});
// InputSchema.parse({ idade: "25", preco: "99.90", ativo: "true" })
// → { idade: 25, preco: 99.9, ativo: true }
Conclusão
A Zod se consolidou como a biblioteca de validação de dados mais importante do ecossistema TypeScript. Sua combinação de API intuitiva, inferência de tipos impecável e zero dependências a torna a escolha ideal para qualquer projeto — desde pequenos scripts até aplicações enterprise.
Se você ainda não usa Zod nos seus projetos, comece hoje mesmo. A instalação leva segundos, a integração é imediata, e os benefícios — código mais seguro, tipos mais precisos, e eliminação de bugs causados por dados malformados — são sentidos desde o primeiro uso.
Com a crescente adoção de TypeScript no mercado, dominar ferramentas como Zod não é mais diferencial — é requisito para desenvolvimento profissional de software moderno.







