--- 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 `-comuns`: ``` -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 // ❌ 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 // 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)