Validação de Dados com Zod
Validar dados de entrada é essencial para a segurança e integridade da sua API. Nesta aula, vamos aprender a usar Zod, uma biblioteca de validação schema-first que oferece inferência de tipos TypeScript automática.
Por que Zod?
typescript
// Problema: TypeScript não valida em runtimeinterface CreateUserDTO { name: string; email: string; age: number;} // Isso compila, mas em runtime req.body pode ter qualquer coisa!const data: CreateUserDTO = req.body;Com Zod, validamos em runtime E obtemos tipos TypeScript:
typescript
import { z } from 'zod'; const createUserSchema = z.object({ name: z.string(), email: z.string().email(), age: z.number().int().positive(),}); // Tipo inferido automaticamente!type CreateUserDTO = z.infer<typeof createUserSchema>; // Validação em runtimeconst data = createUserSchema.parse(req.body); // Throw se inválidoInstalação
bash
npm install zodSchemas Básicos
Tipos Primitivos
typescript
import { z } from 'zod'; // Stringconst nameSchema = z.string(); // Númeroconst ageSchema = z.number(); // Booleanconst activeSchema = z.boolean(); // Dateconst dateSchema = z.date(); // Null e Undefinedconst nullSchema = z.null();const undefinedSchema = z.undefined(); // Enumconst roleSchema = z.enum(['user', 'admin', 'moderator']); // Union (ou)const idSchema = z.union([z.string(), z.number()]);// ouconst idSchema2 = z.string().or(z.number());Validações de String
typescript
const stringSchema = z.string() .min(2, 'Mínimo 2 caracteres') .max(100, 'Máximo 100 caracteres') .email('Email inválido') .url('URL inválida') .uuid('UUID inválido') .regex(/^[A-Z]/, 'Deve começar com maiúscula') .trim() .toLowerCase(); // Específicosz.string().email();z.string().url();z.string().uuid();z.string().cuid();z.string().datetime();z.string().ip();Validações de Número
typescript
const numberSchema = z.number() .int('Deve ser inteiro') .positive('Deve ser positivo') .negative('Deve ser negativo') .min(0, 'Mínimo 0') .max(100, 'Máximo 100') .multipleOf(5, 'Deve ser múltiplo de 5'); // Específicosz.number().int();z.number().positive();z.number().nonnegative();z.number().finite();Objects
typescript
const userSchema = z.object({ id: z.string().uuid(), name: z.string().min(2).max(100), email: z.string().email(), age: z.number().int().min(0).max(150).optional(), role: z.enum(['user', 'admin']).default('user'), tags: z.array(z.string()).default([]), address: z.object({ street: z.string(), city: z.string(), zipCode: z.string(), }).optional(),}); type User = z.infer<typeof userSchema>;// {// id: string;// name: string;// email: string;// age?: number;// role: 'user' | 'admin';// tags: string[];// address?: { street: string; city: string; zipCode: string };// }Arrays
typescript
const tagsSchema = z.array(z.string()) .min(1, 'Pelo menos uma tag') .max(10, 'Máximo 10 tags') .nonempty('Array não pode ser vazio'); const uniqueTagsSchema = z.array(z.string()).refine( (tags) => new Set(tags).size === tags.length, 'Tags devem ser únicas');Schemas para nossa API
Schema de Usuário
typescript
// src/schemas/user.schema.tsimport { z } from 'zod'; export const createUserSchema = z.object({ name: z.string() .min(2, 'Nome deve ter pelo menos 2 caracteres') .max(100, 'Nome deve ter no máximo 100 caracteres') .trim(), email: z.string() .email('Email inválido') .toLowerCase() .trim(), password: z.string() .min(8, 'Senha deve ter pelo menos 8 caracteres') .regex( /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, 'Senha deve conter maiúscula, minúscula e número' ), role: z.enum(['user', 'admin']).optional().default('user'),}); export const updateUserSchema = z.object({ name: z.string().min(2).max(100).trim().optional(), email: z.string().email().toLowerCase().trim().optional(), password: z.string().min(8).optional(), role: z.enum(['user', 'admin']).optional(),}).refine( data => Object.keys(data).length > 0, 'Pelo menos um campo deve ser fornecido'); export const userIdSchema = z.object({ id: z.string().uuid('ID inválido'),}); // Tipos inferidosexport type CreateUserDTO = z.infer<typeof createUserSchema>;export type UpdateUserDTO = z.infer<typeof updateUserSchema>;Schema de Produto
typescript
// src/schemas/product.schema.tsimport { z } from 'zod'; export const createProductSchema = z.object({ name: z.string().min(2).max(200).trim(), description: z.string().max(5000).optional(), price: z.number() .positive('Preço deve ser positivo') .multipleOf(0.01, 'Máximo 2 casas decimais'), stock: z.number().int().nonnegative().default(0), categoryId: z.string().uuid(), images: z.array(z.string().url()) .max(10, 'Máximo 10 imagens') .default([]), tags: z.array(z.string().max(50)) .max(20, 'Máximo 20 tags') .default([]), active: z.boolean().default(true),}); export const productQuerySchema = 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(), category: z.string().uuid().optional(), minPrice: z.coerce.number().positive().optional(), maxPrice: z.coerce.number().positive().optional(), sortBy: z.enum(['name', 'price', 'createdAt']).default('createdAt'), sortOrder: z.enum(['asc', 'desc']).default('desc'),}); export type CreateProductDTO = z.infer<typeof createProductSchema>;export type ProductQuery = z.infer<typeof productQuerySchema>;Middleware de Validação
Crie um middleware genérico para validar qualquer schema:
typescript
// src/middlewares/validate.middleware.tsimport { Request, Response, NextFunction } from 'express';import { ZodSchema, ZodError } from 'zod'; interface ValidateOptions { body?: ZodSchema; query?: ZodSchema; params?: ZodSchema;} export function validate(schemas: ValidateOptions) { return async (req: Request, res: Response, next: NextFunction) => { try { if (schemas.body) { req.body = schemas.body.parse(req.body); } if (schemas.query) { req.query = schemas.query.parse(req.query); } if (schemas.params) { req.params = schemas.params.parse(req.params); } next(); } catch (error) { if (error instanceof ZodError) { const errors = error.errors.map(err => ({ field: err.path.join('.'), message: err.message, })); return res.status(400).json({ success: false, message: 'Dados inválidos', errors, }); } next(error); } };}Usando nas Rotas
typescript
// src/routes/user.routes.tsimport { Router } from 'express';import { validate } from '../middlewares/validate.middleware';import { createUserSchema, updateUserSchema, userIdSchema} from '../schemas/user.schema'; const router = Router(); router.post('/', validate({ body: createUserSchema }), userController.create); router.put('/:id', validate({ params: userIdSchema, body: updateUserSchema }), userController.update); router.get('/:id', validate({ params: userIdSchema }), userController.findById); export default router;Transformações e Refinamentos
Transformações
typescript
const priceSchema = z.string() .transform(val => parseFloat(val)) .pipe(z.number().positive()); // "19.99" → 19.99Refinamentos Customizados
typescript
const passwordConfirmSchema = z.object({ password: z.string().min(8), confirmPassword: z.string(),}).refine( data => data.password === data.confirmPassword, { message: 'Senhas não conferem', path: ['confirmPassword'], }); // Validação assíncronaconst uniqueEmailSchema = z.string().email().refine( async (email) => { const user = await userRepository.findByEmail(email); return !user; }, 'Email já cadastrado');Preprocess
typescript
// Converte string vazia para undefinedconst optionalString = z.preprocess( val => (val === '' ? undefined : val), z.string().optional()); // Converte string para Dateconst dateSchema = z.preprocess( val => (typeof val === 'string' ? new Date(val) : val), z.date());Mensagens de Erro Customizadas
typescript
const userSchema = z.object({ name: z.string({ required_error: 'Nome é obrigatório', invalid_type_error: 'Nome deve ser texto', }).min(2, { message: 'Nome muito curto' }), email: z.string({ required_error: 'Email é obrigatório', }).email({ message: 'Formato de email inválido' }), age: z.number({ required_error: 'Idade é obrigatória', invalid_type_error: 'Idade deve ser um número', }),});Dicas de Performance
typescript
// Reutilize schemas compiladosconst schema = z.string().email(); // ❌ Não crie schemas dentro de funçõesfunction validate(email: string) { return z.string().email().parse(email); // Cria novo schema toda vez} // ✅ Defina schemas uma vez e reutilizeconst emailSchema = z.string().email();function validate(email: string) { return emailSchema.parse(email);}Resumo
Nesta aula aprendemos:
- ✅ Por que usar Zod para validação
- ✅ Schemas para tipos primitivos, objetos e arrays
- ✅ Criar schemas complexos para nossa API
- ✅ Middleware de validação genérico
- ✅ Transformações e refinamentos
- ✅ Mensagens de erro customizadas
Na próxima aula, vamos aprender Tratamento de Erros Profissional! 🚨