Validação com Zod no TypeScript
Zod no TypeScript: schema, infer e validação na borda da API sem duplicar tipos.
Por que isso é importante
Validar com Zod no TypeScript evita tipo mentiroso: schema na borda e z.infer no restante. Parse na entrada — não confie em cast.
O gap entre compile time e runtime
Galera, o TypeScript faz um trabalho incrível em te proteger durante o desenvolvimento. Mas quando a aplicação tá rodando, ele desaparece. O JavaScript puro que sobra não tem ideia dos seus tipos. Se um endpoint externo manda um campo como number em vez de string, ninguém reclama. O app simplesmente faz algo imprevisível.
Antes do Zod, as opções eram: validação manual (if/else infinito), Joi (sem tipagem nativa), ou class-validator (verboso, decorators). Zod nasceu pra ser TypeScript-first, com inferência de tipos automática e API limpa.
Instalação e primeiro schema
npm install zod
import { z } from 'zod';
// Define o schema
const UserSchema = z.object({
name: z.string().min(2, 'Nome muito curto'),
email: z.string().email('Email inválido'),
age: z.number().int().positive().optional(),
role: z.enum(['admin', 'user', 'editor']),
});
// Infere o tipo automaticamente
type User = z.infer<typeof UserSchema>;
// { name: string; email: string; age?: number; role: 'admin' | 'user' | 'editor' }
// Valida dados
const result = UserSchema.parse({
name: 'João',
email: 'joao@email.com',
role: 'admin',
});
// result é do tipo User, 100% validado
Percebe o poder? Você define o schema uma vez e extrai o tipo com z.infer. Zero duplicação. Se mudar o schema, o tipo atualiza automaticamente.
parse vs safeParse: quando usar cada um
O Zod oferece duas formas de validar. O parse() lança exceção se der errado. O safeParse() retorna um objeto de resultado sem lançar exceção. A escolha depende do contexto.
// parse() - lança ZodError se inválido
try {
const user = UserSchema.parse(dadosIncertos);
// user é User tipado
} catch (error) {
if (error instanceof z.ZodError) {
console.log(error.issues);
// [{ code: 'too_small', minimum: 2, path: ['name'], message: 'Nome muito curto' }]
}
}
// safeParse() - nunca lança exceção
const result = UserSchema.safeParse(dadosIncertos);
if (result.success) {
const user = result.data; // User tipado
} else {
const errors = result.error.issues;
// Trata erros sem try/catch
}
Regra prática: use parse() na inicialização (variáveis de ambiente, configs). Use safeParse() em APIs e formulários onde você precisa retornar erros pro usuário.
Validando API requests com Zod
Dá pra integrar Zod direto no handler da API pra validar body, query params e headers. Isso elimina a necessidade de verificação manual campo a campo.
// schemas/user.ts
import { z } from 'zod';
export const CreateUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
password: z.string().min(8).regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
'Senha precisa de maiúscula, minúscula e número'
),
});
export const UpdateUserSchema = CreateUserSchema.partial();
// Todos os campos ficam opcionais
export const QuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
search: z.string().optional(),
});
export type CreateUserInput = z.infer<typeof CreateUserSchema>;
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>;
export type QueryInput = z.infer<typeof QuerySchema>;
// routes/user.ts (Express)
import { CreateUserSchema, QuerySchema } from '../schemas/user';
app.post('/users', (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
errors: result.error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message,
})),
});
}
const user = result.data;
// user tem tipo CreateUserInput, validado
});
app.get('/users', (req, res) => {
const query = QuerySchema.parse(req.query);
// query.page é number, query.limit é number
// z.coerce converteu de string pra number automaticamente
});
Validando formulários no React
Zod combina perfeito com React Hook Form através do resolver @hookform/resolvers/zod. Você define o schema, conecta no form e a validação acontece automática, com mensagens de erro tipadas.
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const LoginSchema = z.object({
email: z.string().email('Digite um email válido'),
password: z.string().min(8, 'Mínimo 8 caracteres'),
});
type LoginForm = z.infer<typeof LoginSchema>;
export function LoginPage() {
const { register, handleSubmit, formState: { errors } } = useForm<LoginForm>({
resolver: zodResolver(LoginSchema),
});
const LoginForm) => {
// data já está validado e tipado
console.log(data.email, data.password);
};
return (
<form
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Entrar</button>
</form>
);
}
Schemas avançados: transform, refine e pipe
Zod vai muito além de validação simples. Dá pra transformar dados, adicionar validações customizadas e compor schemas de formas poderosas.
// transform: modifica o valor após validação
const SlugSchema = z.string()
.transform(val => val.toLowerCase().replace(/\s+/g, '-'));
SlugSchema.parse('Meu Post Legal'); // 'meu-post-legal'
// refine: validação customizada
const PasswordSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine(data => data.password === data.confirmPassword, {
message: 'Senhas não conferem',
path: ['confirmPassword'],
});
// discriminatedUnion: schemas condicionais
const EventSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
z.object({ type: z.literal('keypress'), key: z.string() }),
z.object({ type: z.literal('scroll'), deltaY: z.number() }),
]);
type Event = z.infer<typeof EventSchema>;
// TypeScript sabe o tipo exato baseado no campo 'type'
Tratamento de erros: formatando pro usuário
O ZodError carrega informações detalhadas sobre cada problema. Dá pra formatar de vários jeitos dependendo do destino: API response, toast de UI, log interno.
import { z } from 'zod';
const schema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().positive(),
});
const result = schema.safeParse({ name: '', email: 'abc', age: -1 });
if (!result.success) {
// Formato flat (lista de mensagens)
const flat = result.error.flatten();
// { fieldErrors: { name: [...], email: [...], age: [...] } }
// Formato formatado (aninhado)
const formatted = result.error.format();
// { name: { _errors: ['...'] }, email: { _errors: ['...'] } }
// Custom: mapa campo -> mensagem
const errorMap = result.error.issues.reduce((acc, issue) => {
const field = issue.path.join('.');
acc[field] = issue.message;
return acc;
}, {} as Record<string, string>);
// { name: 'String must contain at least 2 character(s)', ... }
}
Erros comuns com Zod
Atenção
Erro 1: Esquecer de usar z.coerce para query params. Dados de URL sempre chegam como string. Use z.coerce.number() em vez de z.number() para conversão automática.
Erro 2: Usar parse() em APIs sem try/catch. Se a validação falhar, o servidor retorna um 500 genérico. Sempre use safeParse() ou envolva em try/catch.
Erro 3: Criar tipos separados do schema. Se você tem um schema Zod E um type/interface manual, eles vão sair de sincronia. Use z.infer e pronto.
Erro 4: Não tratar mensagens de erro pro português. O Zod manda mensagens em inglês por padrão. Use o segundo argumento de cada validador para custom messages.
Passo a passo: integrando Zod no projeto
- Instale o Zod: npm install zod
- Crie uma pasta src/schemas/ para organizar seus schemas
- Defina schemas para cada entidade (User, Product, Order...)
- Exporte tipos com z.infer no mesmo arquivo do schema
- Use safeParse() nos handlers de API e formulários
- Configure mensagens de erro em português nos validadores
- Integre com React Hook Form via @hookform/resolvers/zod se usar React
Checklist: Zod no projeto TypeScript
Checklist: Validação com Zod
- Zod instalado e configurado
- Schemas criados para todas as entidades principais
- Tipos inferidos com z.infer (sem duplicação manual)
- APIs usando safeParse() com retorno de erros formatados
- Formulários integrados via zodResolver
- Mensagens de erro customizadas em português
- Variáveis de ambiente validadas com Zod na inicialização
- Testes unitários para schemas com edge cases
TypeScript Profissional: Projeto Completo
No CrazyStack você constrói um projeto completo com TypeScript
Node.js e React. Validação com Zod
tipagem forte e boas práticas do início ao deploy. Acesse:
/comprar?src=blog-como-usar-zod-typescript-validacao
]
}
]
}
]
}