Backend
Zod
Schema validation com TypeScript-first para APIs seguras
O que é Zod?
Zod é uma biblioteca de validação de schemas TypeScript-first que permite declarar esquemas de validação e inferir tipos TypeScript automaticamente. É ideal para validar dados de entrada de APIs, formulários e configurações.
Por que utilizamos Zod na IngenioLab?
- Type-safe: Integração perfeita com TypeScript
- Runtime validation: Validação em tempo de execução
- Zero dependencies: Biblioteca leve e sem dependências
- Inferência automática: Gera tipos TypeScript dos schemas
- API intuitiva: Sintaxe declarativa e expressiva
- Parsing seguro: Transforma e valida dados simultaneamente
Instalação
npm install zod# oupnpm add zod# oubun add zod
Conceitos Básicos
1. Schema Simples:
import { z } from 'zod'// Definir schemaconst UserSchema = z.object({id: z.string().uuid(),name: z.string().min(1).max(100),email: z.string().email(),age: z.number().int().min(18).max(120),isActive: z.boolean().default(true)})// Inferir tipo TypeScript automaticamentetype User = z.infer<typeof UserSchema>
2. Validação e Parsing:
// Dados de entradaconst userData = {id: "123e4567-e89b-12d3-a456-426614174000",name: "João Silva",email: "joao@exemplo.com",age: 30}// Validar e parseartry {const validUser = UserSchema.parse(userData)console.log(validUser) // Dados validados e tipados} catch (error) {if (error instanceof z.ZodError) {console.log(error.errors) // Array de erros detalhados}}// Parsing seguro (sem exceção)const result = UserSchema.safeParse(userData)if (result.success) {console.log(result.data) // Dados válidos} else {console.log(result.error) // Erros de validação}
Padrões IngenioLab para APIs
1. Schemas de Request/Response:
// schemas/user.tsimport { z } from 'zod'export const CreateUserSchema = z.object({name: z.string().min(1, "Nome é obrigatório").max(100),email: z.string().email("Email inválido"),password: z.string().min(8, "Senha deve ter pelo menos 8 caracteres"),role: z.enum(['admin', 'user', 'moderator']).default('user')})export const UpdateUserSchema = CreateUserSchema.partial().omit({password: true}).extend({id: z.string().uuid()})export const UserResponseSchema = z.object({id: z.string().uuid(),name: z.string(),email: z.string(),role: z.enum(['admin', 'user', 'moderator']),createdAt: z.date(),updatedAt: z.date()})// Inferir tiposexport type CreateUser = z.infer<typeof CreateUserSchema>export type UpdateUser = z.infer<typeof UpdateUserSchema>export type UserResponse = z.infer<typeof UserResponseSchema>
2. Integração com Fastify:
// routes/users.tsimport { FastifyInstance } from 'fastify'import { CreateUserSchema, UserResponseSchema } from '../schemas/user'export async function userRoutes(fastify: FastifyInstance) {// Schema para documentação automáticaconst createUserOpts = {schema: {body: CreateUserSchema,response: {201: UserResponseSchema}}}fastify.post<{Body: z.infer<typeof CreateUserSchema>}>('/users', createUserOpts, async (request, reply) => {// request.body já está tipado e validadoconst userData = request.body// Lógica de criaçãoconst newUser = await createUser(userData)reply.status(201).send(newUser)})}
3. Validação de Parâmetros:
// schemas/common.tsexport const UuidParamSchema = z.object({id: z.string().uuid("ID deve ser um UUID válido")})export const PaginationSchema = z.object({page: z.coerce.number().int().min(1).default(1),limit: z.coerce.number().int().min(1).max(100).default(10),search: z.string().optional()})// Uso nas rotasfastify.get<{Params: z.infer<typeof UuidParamSchema>Querystring: z.infer<typeof PaginationSchema>}>('/users/:id', {schema: {params: UuidParamSchema,querystring: PaginationSchema,response: {200: UserResponseSchema}}}, async (request, reply) => {const { id } = request.params // string (UUID validado)const { page, limit, search } = request.query // números e string opcional// Buscar usuário com paginação})
Schemas Avançados
1. Validações Customizadas:
const PasswordSchema = z.string().min(8, "Mínimo 8 caracteres").regex(/[A-Z]/, "Deve conter ao menos uma letra maiúscula").regex(/[a-z]/, "Deve conter ao menos uma letra minúscula").regex(/\d/, "Deve conter ao menos um número").regex(/[!@#$%^&*]/, "Deve conter ao menos um caractere especial")const EmailDomainSchema = z.string().email().refine((email) => email.endsWith('@ingeniolab.com'),"Apenas emails @ingeniolab.com são permitidos")
2. Schemas Condicionais:
const UserSchema = z.object({type: z.enum(['individual', 'company']),name: z.string(),// Campos condicionais baseados no tipo}).and(z.discriminatedUnion('type', [z.object({type: z.literal('individual'),cpf: z.string().length(11)}),z.object({type: z.literal('company'),cnpj: z.string().length(14),corporateName: z.string()})]))
3. Transformações:
const ProcessDataSchema = z.object({email: z.string().email().transform(email => email.toLowerCase()),name: z.string().transform(name => name.trim()),birthDate: z.string().transform(date => new Date(date)),tags: z.string().transform(tags => tags.split(',').map(t => t.trim())),metadata: z.string().transform(json => JSON.parse(json))})// Usoconst result = ProcessDataSchema.parse({email: "JOAO@EXEMPLO.COM",name: " João Silva ",birthDate: "1990-05-15",tags: "frontend, react, typescript",metadata: '{"theme": "dark"}'})// result.email = "joao@exemplo.com"// result.name = "João Silva"// result.birthDate = Date object// result.tags = ["frontend", "react", "typescript"]// result.metadata = { theme: "dark" }
Padrões para Diferentes Contextos
1. Schemas de Configuração:
// config/env.tsimport { z } from 'zod'const EnvSchema = z.object({NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),PORT: z.coerce.number().int().min(1000).max(65535).default(3000),DATABASE_URL: z.string().url(),JWT_SECRET: z.string().min(32),REDIS_URL: z.string().url().optional(),LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info')})export const env = EnvSchema.parse(process.env)export type Environment = z.infer<typeof EnvSchema>
2. Schemas para Upload de Arquivos:
const FileUploadSchema = z.object({fieldname: z.string(),originalname: z.string(),encoding: z.string(),mimetype: z.enum(['image/jpeg','image/png','image/webp','application/pdf','text/csv']),size: z.number().max(5 * 1024 * 1024) // 5MB máximo})const MultipleFilesSchema = z.array(FileUploadSchema).max(10)
3. Schemas para WebSocket:
const WebSocketMessageSchema = z.discriminatedUnion('type', [z.object({type: z.literal('chat'),message: z.string().min(1).max(500),room: z.string().uuid()}),z.object({type: z.literal('typing'),room: z.string().uuid(),isTyping: z.boolean()}),z.object({type: z.literal('join'),room: z.string().uuid()})])
Tratamento de Erros
1. Formatação de Erros:
import { ZodError } from 'zod'export function formatZodError(error: ZodError) {return {message: "Dados inválidos",errors: error.errors.map(err => ({field: err.path.join('.'),message: err.message,code: err.code}))}}// Uso em handler de erro do Fastifyfastify.setErrorHandler((error, request, reply) => {if (error instanceof ZodError) {return reply.status(400).send(formatZodError(error))}// Outros erros...})
2. Middleware de Validação:
import { FastifyRequest, FastifyReply } from 'fastify'import { AnyZodObject } from 'zod'export const validate = (schema: AnyZodObject) => {return async (request: FastifyRequest, reply: FastifyReply) => {try {await schema.parseAsync({body: request.body,query: request.query,params: request.params})} catch (error) {if (error instanceof ZodError) {return reply.status(400).send(formatZodError(error))}throw error}}}// Usofastify.register(async function (fastify) {fastify.addHook('preHandler', validate(z.object({body: CreateUserSchema,params: UuidParamSchema})))fastify.post('/users/:id', handler)})
Performance e Otimizações
1. Reutilização de Schemas:
// schemas/base.tsexport const BaseEntitySchema = z.object({id: z.string().uuid(),createdAt: z.date(),updatedAt: z.date()})// schemas/user.tsexport const UserSchema = BaseEntitySchema.extend({name: z.string(),email: z.string().email()})
2. Lazy Evaluation:
const UserSchema = z.lazy(() => z.object({id: z.string(),name: z.string(),posts: z.array(PostSchema) // PostSchema pode referenciar UserSchema}))const PostSchema = z.lazy(() => z.object({id: z.string(),title: z.string(),author: UserSchema}))
3. Parsing Parcial:
// Para campos opcionais em PATCHconst PartialUpdateSchema = UserSchema.partial()// Para campos específicosconst UserSummarySchema = UserSchema.pick({id: true,name: true,email: true})
Testes com Zod
// tests/schemas/user.test.tsimport { describe, it, expect } from 'vitest'import { CreateUserSchema } from '../schemas/user'describe('CreateUserSchema', () => {it('should validate correct user data', () => {const validData = {name: 'João Silva',email: 'joao@exemplo.com',password: 'MinhaSenh@123'}expect(() => CreateUserSchema.parse(validData)).not.toThrow()})it('should reject invalid email', () => {const invalidData = {name: 'João Silva',email: 'email-inválido',password: 'MinhaSenh@123'}expect(() => CreateUserSchema.parse(invalidData)).toThrow()})it('should reject weak password', () => {const result = CreateUserSchema.safeParse({name: 'João Silva',email: 'joao@exemplo.com',password: '123' // Muito fraca})expect(result.success).toBe(false)if (!result.success) {expect(result.error.errors[0].path).toEqual(['password'])}})})