--- 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 `-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().nullish(), 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. Colunas e Registros que Podem ser Nulos Devem Usar `.nullish()` Em schemas de tabelas, entidades e contratos de dados que mapeiam colunas do banco de dados ou campos de formulário/API que podem ser nulos ou omitidos, **DEVE-SE obrigatoriamente usar `.nullish()`** (nunca `.optional()` ou `.nullable()` isolados). **Por que `.nullish()` é obrigatório?** - **Bancos relacionais (PostgreSQL/Knex)**: colunas nulas retornam `null`. - **Formulários e payloads parciais**: frequentemente omitem o campo (`undefined`) ou enviam `null` ao limpar. - `.optional()` aceita apenas `undefined` — **falha** com `Expected string, received null` ao receber dados do banco ou payload com `null`. - `.nullable()` aceita apenas `null` — **falha** com `Required` ao omitir o campo em atualizações parciais (`undefined`). - `.nullish()` aceita ambos (`T | null | undefined`), garantindo interoperabilidade total entre banco, API e frontend sem erros de runtime. ```typescript // ✅ CORRETO — coluna/registro opcional ou nulo com .nullish() export const zPontosBase = z.object({ id: z.string().max(64).nullish(), nome: z.string().min(1), descricao: z.string().max(5000).nullish(), codigo_empreendimento: zUUID.nullish(), coordenadas: zCoordenadas.nullish(), }) // ❌ ERRADO — quebra ao ler do banco quando a coluna for NULL descricao: z.string().optional() // ❌ ERRADO — quebra se o campo for omitido no payload (undefined) descricao: z.string().nullable() ``` ### 9. 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 // ✅ Enums de Tabelas e Colunas derivados dos Schemas (no *-comuns) export const zNomeTabelas = zTabelas.keyof() export const enumTabelas = zNomeTabelas.enum // { pontos: "pontos", registros: "registros", ... } export const enumColunasPontos = zPontosBase.keyof().enum ``` > 🚨 **Uso Obrigatório Universal:** Se existe enum ou zEnum disponível no pacote `*-comuns` (como enums de tabelas, colunas, status, tipos, categorias, eventos, etc. — quaisquer que sejam as variáveis do módulo), é **estritamente proibido usar strings literais**. Toda query de banco, manipulador de fila, serviço e tela DEVE consumir obrigatoriamente os enums canônicos exportados pelo `*-comuns`. ### 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)