Pular para o conteúdo

Builder OS

Builder · OS
L04 · API route honesta
~10 MIN DE LEITURA

Lição 4 de 13: API route honesta

lição 4/13 do Módulo 3
AO FIM, VOCÊ VAI TER
  • API route POST /api/[recurso]s inserindo no banco e retornando 201
  • Tratamento explícito de erros do banco (unique constraint, foreign key) com mensagens em pt-BR
  • Teste E2E rodando: POST válido cria registro; POST inválido retorna 400; conflito retorna 409
  • Commit feat(api): POST /api/[recurso]s with validation + insert

A regra: uma rota faz uma coisa

Uma rota faz uma coisa: aceita um payload, persiste e retorna. Autenticação, envio de email e cálculo de relatório ficam em outra rota ou outra camada. Quando aparece "uma coisa a mais", coloque numa rota separada em vez de fazer o handler crescer.

Na L03 a API route parava em 200 e devolvia o payload validado. Esta lição completa o caminho feliz: payload validado → insert no banco → com o objeto criado. E trata os erros que vão acontecer mesmo com validação na borda: violação de unique constraint, foreign key apontando pra registro que não existe, banco indisponível.

Passo 1 — Insert no banco

Cole no Claude:

prompt · text
Estende a API route `POST /api/s` (da L03) pra inserir o payload validado no banco. Requisitos:

1. Após `safeParse` retornar success, insere no banco usando o ORM do projeto.
2. Retorna 201 com o objeto criado (incluindo id e timestamps gerados pelo banco).
3. Mensagens de erro em pt-BR.
4. **Sem try/catch genérico no caminho feliz.** Use try/catch só pra erros específicos do banco (Passo 2).

Mostre o diff antes de aplicar.
RECURSO[RECURSO]mesmo recurso das lições anteriores (ex: recibo).

Aprove. Confirma:

  1. A função de insert que ele usa existe no ORM/versão do projeto (check anti-alucinação da L01: db.insert(recibos).values(data).returning() é Drizzle válido; db.recibos.create({ data }) é Prisma válido; outras formas, verifica)
  2. (ou equivalente) está presente: pra você ter o objeto inserido com id e timestamps
  3. Não tem try/catch defensivo envolvendo a chamada inteira

Passo 2 — Trate erros específicos do banco

Mesmo com Zod, três erros podem vir:

  1. Unique constraint violation: tentou inserir CPF que já existe
  2. Foreign key violation: contador_id aponta pra contador que não existe
  3. Connection error: banco fora do ar (raro local, comum em produção)
prompt · text
Adiciona tratamento dos 3 erros do banco na API route:

1. Unique constraint (Postgres error code 23505): retorna 409 com `{ error: 'duplicate', field: '[campo-detectado]', message: '[mensagem-em-pt-BR]' }`. Detecta o campo do error message do banco.
2. Foreign key violation (Postgres error code 23503): retorna 400 com `{ error: 'invalid_reference', message: 'O registro referenciado não existe.' }`.
3. Outros erros: retorna 500 com `{ error: 'internal', message: 'Erro ao processar. Tente novamente.' }` e loga o erro no servidor (sem expor pro cliente). **Importante:** antes de logar, faz uma cópia do payload removendo campos sensíveis (CPF, CNPJ, email, telefone). Log poisoning de PII é violação de LGPD silenciosa.

Mostre o diff. Não use try/catch global — use try/catch ao redor da chamada de insert, com switch no error code.

Aprove. Confira:

  • Códigos 23505 e 23503 são códigos Postgres reais. MySQL usa outros (1062 pra duplicate, 1452 pra FK).
  • O try/catch envolve apenas a chamada do banco, não a função inteira.

Passo 3 — Teste E2E

prompt · text
Cria um teste E2E pra essa API route com 3 cenários:

1. POST com payload válido → 201, response body contém o objeto inserido com id.
2. POST com CPF inválido → 400, response body contém `error: 'invalid_payload'`.
3. POST com CPF duplicado (insere o primeiro, depois insere o mesmo de novo) → segundo retorna 409, response body contém `error: 'duplicate', field: 'cliente_cpf'`.

Use o framework de teste já configurado no projeto. Mostra os 3 testes rodando.

Aprove e roda. Os 3 cenários precisam passar.

Passo 4 — Verificação manual com curl

Antes de fazer commit, roda 1 request manual contra o servidor local. Sobe o dev server (npm run dev), e do terminal:

prompt · text
Mostra o comando curl pra criar um recibo de teste na API route. Usa um CPF válido conhecido (gera um, ou usa um de teste — não vaza CPF real). Mostra também a resposta esperada do servidor.

Se você vê 201 + JSON com id, está funcionando. Se vê 500, tem um bug que o teste E2E não pegou: debug agora, antes de fazer commit.

Passo 5 — Faça o commit

prompt · text
Faça commit da API route completa, dos testes E2E e do tratamento de erros. Stage os arquivos relacionados à API route, com mensagem `feat(api): POST /api/[recurso]s with validation + insert` (substitua [recurso] pelo nome real). Mostra os comandos antes de executar.

Build Diary — borda existe mesmo sem API

O CEAP é uma SPA estática consumindo JSON pré-processado (decisão ADR-001: "Static JSON Data Architecture / zero server"). Não tem API route com insert. Mas a borda continua existindo: é o pipeline de ingestão (analysis/01-api-data-collection.ipynb), e ele aplica o mesmo pattern desta lição, só que sobre CSV em vez de payload HTTP.

Trecho real do INVESTIGATION.md:

Detection Logic (Conservative)

MISMATCH = True ONLY IF:
  - NENHUM CNAE (principal OU secundário) corresponde à categoria
  - Valor total > R$ 500 (filtro de ruído)

A primeira linha é validação estrita (regra explícita). A segunda é filtro de ruído (limite mínimo). O resto do código do CEAP confia: se um registro chegou na função de análise, já passou pelas duas regras na borda. É o mesmo princípio da API route: valida na borda, e o interior assume que o dado já chegou limpo.

A borda não precisa ser HTTP. Pode ser leitura de arquivo, fila, webhook ou importação. A regra de validação vale em qualquer ponto onde dado de fora vira dado interno.

Takeaways

  • Uma rota faz uma coisa: aceita payload, persiste, retorna. Trabalho extra vai pra outra rota.
  • Erros do banco têm 3 sabores (unique, FK, conexão), e cada um traduz pra status HTTP específico (409, 400, 500).
  • Try/catch envolve apenas a chamada do banco, não a função inteira, porque catch global esconde bug.
  • Log de erro nunca leva CPF/CNPJ/email/nome: log file vaza fácil em provedor externo e isso é violação silenciosa de LGPD.
  • Transação quando POST toca 2+ tabelas. Idempotência quando clientes externos retentam, obrigatória no M4.

Você terminou quando

A API route faz 3 coisas:

  1. POST válido → 201 com objeto inserido (verificado por teste E2E e curl manual)
  2. POST com payload inválido → 400 (Zod filtra na borda)
  3. POST com duplicata → 409 (banco rejeita; sua route traduz)

E está commitado no GitHub.