Pular para o conteúdo

Builder OS

Builder · OS
L03 · Zod nas bordas
~12 MIN DE LEITURA

Lição 3 de 13: Zod nas bordas

lição 3/13 do Módulo 3
AO FIM, VOCÊ VAI TER
  • Função validateCpf (ou validateCnpj) em um helper de lib/
  • Schema Zod definindo a forma do payload da sua API
  • Pelo menos 1 teste rodando que confirma CPF inválido é rejeitado
  • Commit feat(validation): zod schema for [recurso] + cpf validator

Como o CEAP filtrou R$ 1.5 milhão de ruído na borda

O CEAP cruza CNAE (atividade econômica registrada na Receita) com categoria de despesa pra detectar fornecedores cobrando por serviço fora do escopo declarado. A primeira versão do mapeamento gerou centenas de falsos positivos. Trecho real de INVESTIGATION.md:

v3.1 Mapping Update. Added to Category 1 (Office Maintenance) to remove false positives:

  • 84 — Public Administration (municipalities rent office space to deputies)
  • 94 — Associations/Unions (rent space to deputies)
  • 85 — Education (schools/universities rent space)

False Positives Successfully Removed:

  • Municipalities (CNAE 84) — R$ 445k removed
  • Associations/unions (CNAE 94) — R$ 590k removed
  • Education institutions (CNAE 85) — R$ 105k removed

A validação na (filtrar CNAEs legítimos antes de classificar como suspeito) economizou R$ 1,5 milhão em falsos positivos. Sem essa camada, a análise sinalizaria prefeituras como "fraude" e queimaria a credibilidade com os auditores do TCU.

A lógica é a mesma da validação que você vai criar: regra explícita no ponto de entrada, erro específico quando não entra, resto do sistema confia que o que passa é válido. No CEAP a regra de entrada classifica fraude; no seu produto ela valida o CPF do cliente.

Por que validar na borda

Toda entrada externa vem de três lugares: formulário do usuário, API route que recebe POST/PUT, ou resposta de API de terceiro (Receita, Stripe, CEP). Sem porta de entrada, o código confia que tudo está OK e passa pro banco. Quando o banco rejeita, o erro estoura no fundo da stack: 20 minutos rastreando de onde veio.

Com validação na borda, o erro estoura no primeiro ponto: a API rejeita o request com erro claro antes de qualquer outra coisa rodar. Você sabe onde o problema é (sempre na borda), o que foi rejeitado (qual campo), e não precisa de checagem defensiva em cada função interna.

Pra produto brasileiro, isso vira ainda mais crítico: CPF/CNPJ inválido no banco volta como problema 6 meses depois, quando você tenta emitir nota fiscal pra esse cliente. Melhor pegar na hora.

Passo 1 — Crie a função de validação de CPF/CNPJ

Validar CPF é verificar o dígito verificador (fórmula da Receita Federal que detecta digitação errada ou número inventado), não só conferir tamanho.

Cole no Claude:

prompt · text
Crie um helper de validação de CPF/CNPJ em `lib/validators/`. Requisitos:

1. Função `validateCpf(cpf: string): boolean` — aceita com ou sem máscara, retorna true se válido.
2. Função `validateCnpj(cnpj: string): boolean` — mesma coisa pra CNPJ.
3. Função `cleanDocument(doc: string): string` — remove pontuação, retorna só dígitos.
4. Testes em arquivo `*.test.ts` ao lado: CPF válido conhecido, CPF inválido óbvio ("11111111111"), CPF com máscara, vazio, com letra. Idem CNPJ.

Mostre o diff antes de criar. Use TypeScript strict.

Antes de aprovar, aplica o check 3 do /work (L01): a implementação existe ou foi inventada?

prompt · text
A regra do dígito verificador que você usou — é a regra oficial da Receita Federal? Confirma com referência (link ou descrição do algoritmo) ou marca como "implementação padrão, verificar contra fonte oficial antes de produção".

Se cita a regra (multiplicação por 10, 9, 8, ... e mod 11), OK. Se admite "implementação comum mas não verificada", também OK: você sabe o que tem e pode validar contra um CPF real conhecido nos testes.

Aprove. Roda:

prompt · text
Roda o teste da validação de CPF/CNPJ. Mostra o output.

Testes precisam passar. Se falhar, Claude corrige. Não avance antes do verde.

Passo 2 — Crie o Zod schema do recurso

Você vai escrever um objeto que descreve "essa request precisa ter os campos X, Y, Z; X é string com regra Y; Y é número positivo; Z é opcional." O Zod transforma essa descrição em uma função que recebe dados crus e devolve "ok" ou "inválido + por quê."

Cole no Claude:

prompt · text
Crie um Zod schema em `lib/schemas/` para o payload de criação de . Requisitos:

1. Importe `z` de `zod` e `validateCpf` (ou `validateCnpj`) do helper do Passo 1.
2. Defina `createSchema` com os campos da tabela (consulte o schema da L02).
3. Campos de documento usam `.refine(validateCpf, { message: 'CPF inválido' })` — mensagem em pt-BR.
4. Valores monetários: aceita como número de centavos (`z.number().int().positive()`); rejeita float.
5. Exporte o tipo TypeScript inferido: `export type CreateInput = z.infer<typeof createSchema>`.

Mostre o diff. Inclua 1 teste: schema rejeita CPF inválido com mensagem específica.
RECURSO[RECURSO]nome do recurso (ex: recibo, cliente). Singular minúsculo.

Aprove. Roda:

prompt · text
Roda o teste do Zod schema. Confirma que CPF inválido é rejeitado com mensagem "CPF inválido".

Passo 3 — Conecte o schema à API route

Cole no Claude:

prompt · text
Crie (ou atualize) a API route `POST /api/s` (plural), com este shape:

1. Importa `createSchema` do schema do Passo 2.
2. Lê o body do request.
3. Valida com `createSchema.safeParse(body)` — sem throw.
4. Se `!success`: retorna 400 com `{ error: 'invalid_payload', issues: error.issues }`.
5. Se `success`: por enquanto, retorna 200 com o payload validado (insert no banco vem na L04).
6. Sem try/catch defensivo no caminho feliz — Zod já filtrou.

Mostre o diff. Inclua 1 teste E2E: POST com CPF inválido retorna 400; CPF válido retorna 200.
RECURSO[RECURSO]mesmo recurso do Passo 2 (ex: recibo).

Aprove. Roda:

prompt · text
Roda o teste da API route. Mostra o output dos dois cenários (CPF inválido = 400, CPF válido = 200).

Quando os dois passam, a borda está blindada. Qualquer request daqui pra frente é garantidamente válido, e o resto do código pode confiar.

Passo 4 — Faça o commit

prompt · text
Faça commit dos três arquivos criados (validador, Zod schema, API route + testes). Stage os arquivos, com mensagem `feat(validation): zod schema for [recurso] + cpf validator`. Mostre os comandos antes de executar.

Aprove git add e git commit. Confira no GitHub.

Takeaways

  • Valide na borda, onde dado externo entra. Cada borda fica blindada; o resto do código pode confiar.
  • Use .refine() do Zod pra encaixar validação custom (CPF/CNPJ) dentro do schema. Erro vem com path do campo + mensagem em pt-BR.
  • (sem throw) é o default em API route, evita try/catch defensivo. parse lança exceção; só use quando você quer que o erro suba.
  • Validação assíncrona (consulta o banco) é exceção, não default. Pra duplicata, use constraint do banco + 409 na L04, não .refine async.

Você terminou quando

Quatro coisas:

  1. Função validateCpf (ou validateCnpj) com testes verdes
  2. Zod schema do recurso com .refine usando a função do item 1
  3. API route validando o payload com 400 pra inválido / 200 pra válido (insert vem na L04)
  4. Tudo commitado no GitHub