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 → Código de status HTTP que indica o resultado de uma request. 201 Created = recurso criado com sucesso; 400 Bad Request = payload inválido; 409 Conflict = conflito com o estado atual (ex: duplicata); 500 = bug do servidor. 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.
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:
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)
Cláusula do Drizzle (ou equivalente) que, depois de inserir no banco, devolve o objeto criado já com id e timestamps preenchidos. Sem ela, você precisaria de um SELECT extra.(ou equivalente) está presente: pra você ter o objeto inserido com id e timestamps
Não tem try/catch defensivo envolvendo a chamada inteira
Unique constraint violation: tentou inserir CPF que já existe
Foreign key violation: contador_id aponta pra contador que não existe
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.
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.
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.
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.
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.