.agent/global/rules/zod-validacao-padrao.md
2026-06-18 11:59:57 -03:00

315 lines
8.9 KiB
Markdown

---
type: rule
name: zod-validacao-padrao
description: Estabelece Zod como biblioteca obrigatória de validação em todo o ecossistema Azteca
version: "1.0"
scope: global
priority: alta
trigger: always_on
globs:
- "**/*.ts"
- "**/*.tsx"
- "**/*.vue"
alwaysApply: true
updatedAt: "2026-06-01"
references:
- .agent/rules/typescript-sem-any.global.md
- .agent/skills/criar-schema-zod.global/SKILL.md
---
# Regra: Zod como Validador Padrão
## Contexto
Todo projeto do ecossistema Azteca que receba dados externos — API, formulário, webhook, importação, fila, variáveis de ambiente — **deve** validar esses dados com [Zod](https://zod.dev).
Zod é a única biblioteca de validação autorizada porque:
- Inferência nativa de tipos TypeScript (`z.infer<>`) — elimina duplicação entre schema e tipo
- Bundle pequeno (~13KB gzipped) — viável no frontend
- Compartilhável entre back e front via pacotes `*-comuns`
- Ecossistema maduro: `zod-to-json-schema`, `@anatine/zod-nestjs`, integração com `tRPC`
---
## Regra
### 1. Toda entrada externa deve ser validada com Zod
Entradas externas incluem:
- **Corpo de requisição** (API REST, webhook, GraphQL)
- **Query params e path params**
- **Formulários de frontend**
- **Dados de fila** (BullMQ, Redis, etc.)
- **Arquivos importados** (CSV, JSON, Excel)
- **Variáveis de ambiente**
- **Respostas de APIs externas** (quando a resposta não é confiável)
### 2. Schemas devem residir no pacote `*-comuns`
Para que front e back compartilhem os mesmos schemas, eles devem residir no pacote `<modulo>-comuns`:
```
<modulo>-comuns/src/
├── tabelas/
│ ├── usuarios/
│ │ ├── zUsuarioCriar.ts
│ │ ├── zUsuarioEditar.ts
│ │ └── index.ts
│ ├── empresas/
│ │ ├── zEmpresaCriar.ts
│ │ └── index.ts
│ └── index.ts
```
### 3. Tipos devem ser derivados dos schemas (nunca o inverso)
```typescript
// ✅ CORRETO — tipo derivado do schema
export const zUsuarioCriar = z.object({
nome: z.string().min(1),
email: z.string().email(),
ativo: z.boolean().default(true),
})
export type tipoUsuarioCriar = z.infer<typeof zUsuarioCriar>
// ❌ ERRADO — tipo manual separado do schema
interface UsuarioCriar {
nome: string
email: string
ativo?: boolean
}
const zUsuarioCriar = z.object({ ... }) // duplicação!
```
### 4. Nomenclatura obrigatória de schemas
| Padrão | Uso | Exemplo |
|--------|-----|---------|
| `z{Entidade}Criar` | Criação de registro | `zUsuarioCriar` |
| `z{Entidade}Editar` | Edição de registro | `zUsuarioEditar` |
| `z{Entidade}Filtro` | Filtros de listagem | `zUsuarioFiltro` |
| `z{Entidade}Resposta` | Resposta de API | `zUsuarioResposta` |
| `z{Entidade}Params` | Parâmetros de rota | `zUsuarioParams` |
| `z{Modulo}Config` | Configuração do módulo | `zGeeConfig` |
| `z{Modulo}Ambiente` | Variáveis de ambiente | `zBackAmbiente` |
Prefixo `z` minúsculo + nome em `PascalCase`.
### 5. Usar `safeParse` em vez de `parse` para erros controlados
```typescript
// ✅ CORRETO — erro controlado
const resultado = zUsuarioCriar.safeParse(dados)
if (!resultado.success) {
return respostaComuns.erro(resultado.error.issues[0].message)
}
const usuario = resultado.data // tipado automaticamente
// ❌ ERRADO — throw não controlado
const usuario = zUsuarioCriar.parse(dados) // lança ZodError
```
A exceção é em contextos onde o throw é desejável (middleware global, início de pipeline).
### 6. Nunca usar validação manual quando Zod resolve
```typescript
// ❌ ERRADO — validação manual
if (!dados.nome || typeof dados.nome !== "string") {
return erro("Nome é obrigatório")
}
if (!dados.email || !dados.email.includes("@")) {
return erro("Email inválido")
}
// ✅ CORRETO — Zod faz tudo
const resultado = zUsuarioCriar.safeParse(dados)
if (!resultado.success) {
return erro(resultado.error.issues[0].message)
}
```
### 7. Transformações e defaults pertencem ao schema
```typescript
// ✅ CORRETO — transformação no schema
export const zProdutoCriar = z.object({
nome: z.string().trim().min(1),
preco: z.coerce.number().positive(),
ativo: z.boolean().default(true),
codigo: z.string().toUpperCase().optional(),
tags: z.array(z.string()).default([]),
})
// ❌ ERRADO — transformação manual após validação
const produto = zProdutoCriar.parse(dados)
produto.nome = produto.nome.trim()
produto.codigo = produto.codigo?.toUpperCase()
```
### 8. Composição de schemas em vez de duplicação
```typescript
// ✅ CORRETO — composição
const zEndereco = z.object({
rua: z.string(),
cidade: z.string(),
uf: z.string().length(2),
})
const zEmpresaCriar = z.object({
nome: z.string(),
endereco: zEndereco,
})
const zEmpresaEditar = zEmpresaCriar.partial() // todos opcionais
const zEmpresaResposta = zEmpresaCriar.extend({
codigo: z.string().uuid(),
criadoEm: z.string().datetime(),
})
// ❌ ERRADO — copiar campos entre schemas
const zEmpresaEditar = z.object({
nome: z.string().optional(), // duplicou!
endereco: z.object({ // duplicou!
rua: z.string().optional(),
cidade: z.string().optional(),
}),
})
```
### 9. Enums Zod para valores de domínio
```typescript
// ✅ CORRETO — enum Zod quando os valores vêm de dados externos
export const zStatusLicenca = z.enum(["ativa", "vencida", "suspensa", "cancelada"])
export type tipoStatusLicenca = z.infer<typeof zStatusLicenca>
// Usar em outros schemas
const zLicencaCriar = z.object({
status: zStatusLicenca,
// ...
})
// ✅ TAMBÉM CORRETO — TypeScript enum quando já existe no projeto
// e o schema referencia o enum
import { StatusLicenca } from "./enums"
const zLicencaCriar = z.object({
status: z.nativeEnum(StatusLicenca),
})
```
### 10. Bibliotecas proibidas para validação
| ❌ Proibido | Motivo |
|-------------|--------|
| `joi` | Não infere tipos TS, bundle pesado |
| `yup` | Inferência fraca, API menos ergonômica |
| `class-validator` | Decorators, não compartilhável com frontend |
| `io-ts` | Verboso, curva de aprendizado alta |
| `ajv` (direto) | JSON Schema — usar `zod-to-json-schema` se precisar de JSON Schema |
| Validação manual com `if/typeof` | Quando Zod resolve o mesmo caso |
---
## Exemplos
### API backend (Express)
```typescript
// ✅ Correto
import { zUsuarioCriar } from "projeto-comuns/tabelas/usuarios"
app.post("/usuarios", async (req, res) => {
const resultado = zUsuarioCriar.safeParse(req.body)
if (!resultado.success) {
return res.status(400).json({ erro: resultado.error.issues })
}
const usuario = await criarUsuario(resultado.data)
return res.json(usuario)
})
```
### Webhook com fila
```typescript
// ✅ Correto — valida antes de enfileirar
const zPayloadWebhook = z.object({
tabela: z.enum(["empreendedores", "empreendimentos", "usuarios"]),
dados: z.array(z.unknown()).min(1),
})
app.post("/webhook/:tabela", async (req, res) => {
const resultado = zPayloadWebhook.safeParse({
tabela: req.params.tabela,
dados: req.body,
})
if (!resultado.success) {
return res.status(400).json({ erro: "Payload inválido" })
}
await fila.add("sync", resultado.data)
return res.json({ ok: true })
})
```
### Variáveis de ambiente
```typescript
// ✅ Correto — validar no boot da aplicação
const zAmbiente = z.object({
DATABASE_URL: z.string().url(),
REDIS_HOST: z.string().default("localhost"),
REDIS_PORT: z.coerce.number().default(6379),
BASE_URL: z.string().url(),
SESSAO_BACK: z.string().uuid(),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
})
export const ambiente = () => zAmbiente.parse(process.env)
```
### Frontend (formulário Vue)
```typescript
// ✅ Correto — schema compartilhado validando antes de enviar
import { zUsuarioCriar } from "projeto-comuns/tabelas/usuarios"
const salvar = async () => {
const resultado = zUsuarioCriar.safeParse(formulario.value)
if (!resultado.success) {
erros.value = resultado.error.flatten().fieldErrors
return
}
await api.post("/usuarios", resultado.data)
}
```
---
## Impacto
Sem esta regra:
- Cada desenvolvedor escolhe uma estratégia diferente de validação (if/typeof, joi, yup, etc.)
- Tipos e schemas ficam dessincronizados — mudança no schema não reflete no tipo e vice-versa
- Validações manuais são incompletas e inconsistentes entre endpoints
- Código de validação é duplicado entre front e back
- Dados inválidos passam despercebidos e corrompem a base
Com esta regra:
- Uma única fonte de verdade: o schema Zod
- Tipos TypeScript sempre sincronizados via `z.infer<>`
- Validação consistente em todos os pontos de entrada
- Reutilização entre front, back e workers via pacote `*-comuns`
- Erros de validação estruturados e internacionalizáveis
---
## Referências
- Rule: `.agent/rules/typescript-sem-any.global.md` (narrowing com Zod mencionado como alternativa)
- Skill: `.agent/skills/criar-schema-zod.global/SKILL.md` (guia prático para criar schemas)