8.9 KiB
8.9 KiB
| type | name | description | version | scope | priority | trigger | globs | alwaysApply | updatedAt | references | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| rule | zod-validacao-padrao | Estabelece Zod como biblioteca obrigatória de validação em todo o ecossistema Azteca | 1.0 | global | alta | always_on |
|
true | 2026-06-01 |
|
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.
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 comtRPC
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)
// ✅ 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
// ✅ 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
// ❌ 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
// ✅ 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
// ✅ 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
// ✅ 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)
// ✅ 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
// ✅ 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
// ✅ 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)
// ✅ 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)