238 lines
7.2 KiB
Text
238 lines
7.2 KiB
Text
---
|
|
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:
|
|
- global/rules/typescript-sem-any
|
|
- global/skills/criar-schema-zod/SKILL
|
|
---
|
|
|
|
# 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 e Enums Zod
|
|
|
|
```typescript
|
|
// ✅ Composição de schemas via extend/partial
|
|
const zEndereco = z.object({ rua: z.string(), cidade: z.string() })
|
|
const zEmpresaCriar = z.object({ nome: z.string(), endereco: zEndereco })
|
|
const zEmpresaEditar = zEmpresaCriar.partial()
|
|
|
|
// ✅ Enum Zod para valores de domínio
|
|
export const zStatusLicenca = z.enum(["ativa", "vencida", "suspensa", "cancelada"])
|
|
export type tipoStatusLicenca = z.infer<typeof zStatusLicenca>
|
|
```
|
|
|
|
### 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)
|
|
})
|
|
```
|
|
|
|
### Variáveis de ambiente e Frontend
|
|
|
|
```typescript
|
|
// ✅ Validar env no boot da aplicação
|
|
export const ambiente = () => z.object({
|
|
DATABASE_URL: z.string().url(),
|
|
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
|
}).parse(process.env)
|
|
|
|
// ✅ Frontend: usar schema compartilhado antes de enviar
|
|
const salvar = async () => {
|
|
const res = zUsuarioCriar.safeParse(formulario.value)
|
|
if (!res.success) return (erros.value = res.error.flatten().fieldErrors)
|
|
await api.post("/usuarios", res.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: `global/rules/typescript-sem-any` (narrowing com Zod mencionado como alternativa)
|
|
- Skill: `global/skills/criar-schema-zod/SKILL` (guia prático para criar schemas)
|