Pular para o conteúdoPedro Farbo
Lição 6 / 2540 min

Validação de Dados com Zod

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álido

Instalação

bash
npm install zod

Schemas 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.99

Refinamentos 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! 🚨

Gostou do conteúdo? Sua contribuição ajuda a manter tudo online e gratuito!

PIX:0737160d-e98f-4a65-8392-5dba70e7ff3e