O problema
Um CRUD de tarefa não tem nada de difícil, e é justamente por isso que ele vira bagunça: regra no controller, entidade do banco vazando na resposta, cada erro num formato. Fiz este projeto para treinar a organização, não a funcionalidade.
Backend
Um CRUD feito com disciplina: camada separada, record no lugar da entidade e todo erro no formato da RFC 7807.
RFC 7807um formato para todo erro
Um CRUD de tarefa não tem nada de difícil, e é justamente por isso que ele vira bagunça: regra no controller, entidade do banco vazando na resposta, cada erro num formato. Fiz este projeto para treinar a organização, não a funcionalidade.
Separação domain / repository / service / web. DTOs em records isolam a entidade do contrato HTTP. A regra vive no agregado de domínio. Um GlobalExceptionHandler devolve ProblemDetail (RFC 7807) para todo erro, com paginação e filtro via Pageable.
Copiei do controller. É isto que a API responde.
/api/tarefas201 CreatedEnvio
{
"titulo": "Revisar contrato da API",
"prioridade": "ALTA",
"prazo": "2026-08-15"
}Resposta
{
"id": 7,
"titulo": "Revisar contrato da API",
"descricao": null,
"status": "PENDENTE",
"prioridade": "ALTA",
"prazo": "2026-08-15",
"criadaEm": "2026-07-30T14:02:11.482Z",
"atualizadaEm": "2026-07-30T14:02:11.482Z"
}A entidade do banco não sai pela API. Quem responde é um record.
/api/tarefas/9999404 Not FoundResposta
{
"type": "about:blank",
"title": "Recurso não encontrado",
"status": 404,
"detail": "Tarefa 9999 não encontrada"
}Todo erro sai no mesmo formato, então quem integra escreve um tratamento só.
Records separam o que o banco guarda do que o cliente recebe. Renomear uma coluna para de ser problema de quem consome a API.
Todo erro sai com o mesmo desenho. Quem integra escreve um tratamento e acabou.
O controller só traduz HTTP e passa adiante. A regra dá para testar sem subir servidor.