Fumadocs + Code Hike
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
# ou
pnpm add zod
# ou
bun add zod

Conceitos Básicos

1. Schema Simples:

import { z } from 'zod'
// Definir schema
const 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 automaticamente
type User = z.infer<typeof UserSchema>

2. Validação e Parsing:

// Dados de entrada
const userData = {
id: "123e4567-e89b-12d3-a456-426614174000",
name: "João Silva",
email: "joao@exemplo.com",
age: 30
}
// Validar e parsear
try {
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.ts
import { 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 tipos
export 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.ts
import { FastifyInstance } from 'fastify'
import { CreateUserSchema, UserResponseSchema } from '../schemas/user'
export async function userRoutes(fastify: FastifyInstance) {
// Schema para documentação automática
const 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 validado
const userData = request.body
// Lógica de criação
const newUser = await createUser(userData)
reply.status(201).send(newUser)
})
}

3. Validação de Parâmetros:

// schemas/common.ts
export 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 rotas
fastify.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))
})
// Uso
const 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.ts
import { 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 Fastify
fastify.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
}
}
}
// Uso
fastify.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.ts
export const BaseEntitySchema = z.object({
id: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
})
// schemas/user.ts
export 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 PATCH
const PartialUpdateSchema = UserSchema.partial()
// Para campos específicos
const UserSummarySchema = UserSchema.pick({
id: true,
name: true,
email: true
})

Testes com Zod

// tests/schemas/user.test.ts
import { 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'])
}
})
})