5.5 KiB
5.5 KiB
| name | scope | description |
|---|---|---|
| criar-schema-zod | global | Guia operacional para criar, organizar e manter schemas de validação Zod em projetos TypeScript. |
Skill: Criar Schema Zod
Use esta skill ao adicionar validação de dados em qualquer ponto de entrada do sistema — API, formulário, webhook, importação, fila ou variáveis de ambiente.
PASSO 1 — Identificar a Entrada Externa
Determinar qual dado externo precisa de validação:
| Tipo de entrada | Onde validar |
|---|---|
| Corpo de requisição (POST/PUT) | Handler da rota, antes de qualquer lógica |
| Query/path params | Handler da rota |
| Formulário frontend | Antes de chamar a API |
| Dados de fila (BullMQ) | No processador da fila |
| Arquivo importado (CSV/JSON) | No parser, linha a linha |
| Variáveis de ambiente | No boot da aplicação |
| Resposta de API externa | Após receber a resposta |
PASSO 2 — Localizar ou Criar o Schema no Pacote Comuns
Schemas compartilhados vivem no pacote *-comuns:
projeto-comuns/src/tabelas/
├── usuarios/
│ ├── zUsuarioCriar.ts
│ ├── zUsuarioEditar.ts
│ └── index.ts
└── index.ts
2.1. Verificar se já existe
rg "z{NomeDaEntidade}" --include='*.ts' -l
Se já existir → reutilizar ou compor a partir dele (.extend(), .pick(), .partial()).
2.2. Criar o schema
import { z } from "zod"
export const zUsuarioCriar = z.object({
nome: z.string().trim().min(1, "Nome é obrigatório"),
email: z.string().email("E-mail inválido"),
ativo: z.boolean().default(true),
})
2.3. Nomenclatura obrigatória
| Padrão | Uso |
|---|---|
z{Entidade}Criar |
Criação |
z{Entidade}Editar |
Edição (geralmente .partial()) |
z{Entidade}Filtro |
Filtros de listagem |
z{Entidade}Resposta |
Resposta de API |
z{Entidade}Params |
Parâmetros de rota |
z{Modulo}Config |
Configuração |
z{Modulo}Ambiente |
Variáveis de ambiente |
2.4. Colunas e Campos que Podem ser Nulos: Usar .nullish()
Toda coluna do banco de dados relacional ou campo de entidade/registro que puder ser nulo ou omitido DEVE utilizar .nullish() (nunca .optional() ou .nullable() isolados):
// ✅ CORRETO — aceita null (do banco PostgreSQL) e undefined (payload omitido)
export const zPontoCriar = z.object({
nome: z.string().min(1),
descricao: z.string().max(5000).nullish(),
codigo_empreendimento: zUUID.nullish(),
coordenadas: zCoordenadas.nullish(),
})
// ❌ ERRADO — quebra com null retornado pelo Knex/PostgreSQL
descricao: z.string().optional(),
// ❌ ERRADO — quebra quando a chave é omitida no payload (undefined)
descricao: z.string().nullable(),
PASSO 3 — Derivar o Tipo com z.infer<>
export type tipoUsuarioCriar = z.infer<typeof zUsuarioCriar>
Nunca criar um interface ou type manual separado do schema. O tipo sempre deriva do schema.
PASSO 4 — Implementar safeParse no Ponto de Entrada
Backend (Express)
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[0].message })
}
const usuario = await criarUsuario(resultado.data)
return res.json(usuario)
})
Frontend (Vue)
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)
}
Variáveis de ambiente
const zAmbiente = z.object({
DATAbase_url: z.string().url(),
REDIS_HOST: z.string().default("localhost"),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
})
// Aqui o throw é desejável — a app não deve subir com env inválido
export const ambiente = () => zAmbiente.parse(process.env)
PASSO 5 — Compor Schemas Derivados
Quando precisar de variações do mesmo schema, compor em vez de duplicar:
// Schema base
const zProduto = z.object({
nome: z.string().trim().min(1),
preco: z.coerce.number().positive(),
categoria: z.string(),
})
// Edição: todos os campos opcionais
const zProdutoEditar = zProduto.partial()
// Resposta: base + campos gerados
const zProdutoResposta = zProduto.extend({
codigo: z.uuid(),
criadoEm: z.string().datetime(),
})
// Filtro: subset de campos opcionais
const zProdutoFiltro = zProduto.pick({ categoria: true }).partial()
PASSO 6 — Validar Tipagem
npx tsc --noEmit
Zero erros = concluído.
Checklist Final
- O schema está no pacote
*-comuns(quando compartilhado entre front/back)? - O tipo é derivado do schema com
z.infer<>? - A validação usa
safeParse(exceto em boot/middleware)? - A nomenclatura segue o padrão
z{Entidade}{Acao}? - Transformações (trim, coerce, default) estão no schema, não no handler?
- Colunas e campos que podem ser nulos/opcionais utilizam
.nullish()(em vez de.optional()ou.nullable()isolados)? - Schemas derivados usam composição (.partial, .extend, .pick), não duplicação?
tsc --noEmitpassa sem erros?
Referências
- Rule:
global/rules/zod-validacao-padrao - Rule:
global/rules/typescript-sem-any