Como usar o Zod para validar e tipar payloads de entrada em APIs
1. Introdução ao Zod e seu papel na validação de APIs
Em APIs modernas, validar payloads de entrada não é apenas uma boa prática — é uma necessidade crítica de segurança e confiabilidade. Dados malformados podem causar desde erros silenciosos até vulnerabilidades graves como injeção de dados ou quebra de regras de negócio.
Zod é uma biblioteca de validação de esquemas baseada em TypeScript que se destaca por sua simplicidade, poder expressivo e integração profunda com o sistema de tipos da linguagem. Diferente de abordagens manuais (que exigem dezenas de if aninhados) ou bibliotecas tradicionais como Joi e Yup, o Zod oferece inferência automática de tipos TypeScript, validações encadeadas e refinamentos customizados em uma API coesa e declarativa.
2. Configuração inicial e primeiros esquemas com Zod
Para começar, instale o Zod em seu projeto Node.js/TypeScript:
npm install zod
Criar seu primeiro esquema é direto. Vamos validar um payload típico de criação de usuário:
import { z } from 'zod';
const createUserSchema = z.object({
name: z.string().min(3, 'Nome deve ter no mínimo 3 caracteres'),
email: z.string().email('Email inválido'),
age: z.number().int().positive('Idade deve ser positiva'),
isActive: z.boolean().default(true)
});
Zod oferece tipos nativos com validações encadeadas: z.string().min().max().email(), z.number().int().positive(), z.boolean(). O método .default() define valores padrão caso o campo seja omitido.
3. Validação avançada de campos: refinamentos e transformações
Para regras de negócio complexas, use .refine(). Por exemplo, validar que a senha contém pelo menos um número:
const passwordSchema = z.string()
.min(8, 'Senha deve ter no mínimo 8 caracteres')
.refine(val => /\d/.test(val), 'Senha deve conter pelo menos um número');
Transformações com .transform() permitem normalizar dados antes da validação final:
const emailSchema = z.string()
.email()
.transform(val => val.toLowerCase().trim());
const cpfSchema = z.string()
.regex(/^\d{3}\.\d{3}\.\d{3}-\d{2}$/, 'CPF inválido')
.transform(val => val.replace(/\D/g, ''));
Validações de padrão com .regex(), .email(), .url() e .uuid() cobrem casos comuns sem necessidade de expressões regulares manuais.
4. Esquemas aninhados, arrays e unions para payloads complexos
Payloads de API frequentemente contêm estruturas aninhadas. Zod lida com isso naturalmente:
const orderSchema = z.object({
id: z.string().uuid(),
customer: z.object({
name: z.string(),
address: z.object({
street: z.string(),
city: z.string(),
zipCode: z.string().regex(/^\d{5}-\d{3}$/)
})
}),
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive(),
price: z.number().positive()
})).min(1, 'Pedido deve ter ao menos um item'),
status: z.enum(['pending', 'shipped', 'delivered', 'cancelled'])
});
Para payloads polimórficos, unions e discriminated unions são essenciais:
const eventSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('user_created'), userId: z.string() }),
z.object({ type: z.literal('order_placed'), orderId: z.string(), amount: z.number() }),
z.object({ type: z.literal('payment_failed'), error: z.string() })
]);
5. Integração com frameworks de API (Express, Fastify, Hono)
Express — middleware de validação simples:
import { Request, Response, NextFunction } from 'express';
function validate(schema: z.ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
try {
req.body = schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({ errors: error.errors });
} else {
next(error);
}
}
};
}
// Uso na rota
app.post('/users', validate(createUserSchema), (req, res) => {
// req.body já está validado e tipado
});
Fastify — suporte nativo a esquemas Zod via plugin:
import fastify from 'fastify';
import { serializerCompiler, validatorCompiler } from 'fastify-zod';
const app = fastify();
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);
app.post('/users', {
schema: {
body: createUserSchema
}
}, async (request, reply) => {
// request.body está validado e tipado automaticamente
});
Hono (Cloudflare Workers) — validação direta no handler:
import { zValidator } from '@hono/zod-validator';
app.post('/users', zValidator('json', createUserSchema), (c) => {
const userData = c.req.valid('json');
// userData tem tipo inferido automaticamente
});
6. Tipagem inferida e segurança de tipos no runtime
O poder real do Zod está na inferência de tipos TypeScript com z.infer:
type CreateUserInput = z.infer<typeof createUserSchema>;
// TypeScript infere: { name: string; email: string; age: number; isActive: boolean }
Isso elimina a necessidade de interfaces duplicadas. O tipo inferido corresponde exatamente à validação — se você adicionar .email(), o tipo continua sendo string, mas o runtime garante que só emails válidos passem. Em controllers, evite any:
async function createUser(data: unknown) {
const validated = createUserSchema.parse(data);
// validated tem tipo CreateUserInput
return await db.user.create({ data: validated });
}
7. Tratamento de erros e mensagens personalizadas
z.parse() lança ZodError se a validação falhar. Para tratamento mais suave, use z.safeParse():
const result = createUserSchema.safeParse(payload);
if (!result.success) {
console.error(result.error.errors);
// Array de objetos com path, message, code
}
// Para uso em API:
if (!result.success) {
return res.status(400).json({
type: 'https://example.com/validation-error',
title: 'Erro de validação',
status: 400,
errors: result.error.errors.map(e => ({
field: e.path.join('.'),
message: e.message
}))
});
}
Mensagens personalizadas por campo melhoram a experiência do desenvolvedor:
const schema = z.object({
email: z.string().email('Forneça um email corporativo válido'),
age: z.number().int('Idade deve ser número inteiro').positive('Idade deve ser positiva')
});
8. Boas práticas e armadilhas comuns
Separação de responsabilidades: mantenha esquemas em arquivos separados (schemas/user.ts) e importe-os nos controllers. Isso facilita testes e reuso.
Evite validação duplicada: se o frontend já valida com Zod (via bibliotecas como @hookform/resolvers), compartilhe o mesmo esquema usando um pacote compartilhado (shared/schemas).
Performance: use safeParse quando a validação é opcional ou você precisa de controle de fluxo. Use parse para cenários onde a falha deve interromper imediatamente. Cache esquemas complexos em variáveis — Zod já é otimizado, mas evitar recriação desnecessária ajuda.
Armadilha comum: não confie apenas na tipagem TypeScript para segurança. Zod valida em runtime, o que é crucial porque dados de entrada (API, formulários) nunca têm tipos garantidos.
Referências
- Documentação oficial do Zod — Guia completo com todos os tipos, métodos e exemplos de uso
- Zod + Express: Validação de payloads em APIs REST — Tutorial prático de integração com Express e tratamento de erros
- Fastify + Zod: Schemas tipados para rotas — Documentação oficial do Fastify sobre validação com esquemas Zod
- Hono + Zod: Validação em Edge Workers — Guia oficial do Hono para validação com Zod em ambientes serverless
- TypeScript Deep Dive: Inferência de tipos com Zod — Explicação detalhada sobre como Zod infere tipos TypeScript a partir de esquemas
- Zod vs Joi vs Yup: Comparativo de bibliotecas de validação — Análise comparativa entre as principais bibliotecas de validação TypeScript