Nossa API já persiste dados num banco e usa injeção de dependência — mas ela ainda é ingênua diante de uma verdade dura do mundo real: os dados que chegam pela rede não são confiáveis. Um programa cliente, na internet, pode enviar um produto com nome vazio, um preço negativo, um texto onde se espera um número, ou um pedido completamente malformado. Uma API profissional antecipa esses casos e responde a eles com clareza, em vez de quebrar ou, pior, aceitar lixo e corrompê-lo no banco. Esta aula, que fecha o assunto das APIs, torna a nossa robusta: aprenderemos a validar os dados que chegam, a tratar erros devolvendo respostas apropriadas, e as boas práticas que separam uma API de brinquedo de uma pronta para produção. Você reencontrará a filosofia do "falhe claramente" da fase de robustez — agora aplicada na fronteira entre o seu sistema e o mundo inteiro.
O princípio: nunca confie na entrada
Este é o mantra de toda programação de fronteira, e você já o encontrou lá na aula sobre entrada e conversão, quando aprendeu TryParse para não confiar no que o usuário digita. Numa API, o princípio é ainda mais crítico, porque a entrada vem de qualquer programa na internet — bem-intencionado ou não, correto ou com defeito. Toda informação que chega de fora deve ser validada antes de ser usada ou salva. A pergunta a fazer para cada campo é sempre a mesma: o que acontece se este valor vier vazio, nulo, negativo, gigante, ou simplesmente errado? Uma API robusta responde a cada um desses casos com um erro claro, e nunca com uma quebra nem com dados inválidos gravados no banco.
Validação e o código 400
Quando a entrada é inválida, a resposta correta é o código 400 (Pedido Inválido), acompanhado de uma mensagem que explica o que está errado — para que o cliente possa corrigir. Vejamos a rota de criação com validação:
app.MapPost("/produtos", async (Produto novo, LojaContext db) =>
{
// VALIDAÇÃO: checa a entrada ANTES de tocar no banco.
if (string.IsNullOrWhiteSpace(novo.Nome))
return Results.BadRequest("O nome é obrigatório."); // 400 com explicação
if (novo.Preco <= 0)
return Results.BadRequest("O preço deve ser positivo.");
if (novo.Nome.Length > 100)
return Results.BadRequest("O nome não pode ter mais de 100 caracteres.");
// Só chega aqui se a entrada for VÁLIDA:
db.Produtos.Add(novo);
await db.SaveChangesAsync();
return Results.Created($"/produtos/{novo.Id}", novo);
});
Note o fluxo, que é a lição central: validar primeiro, agir depois. Se o nome é vazio, a rota retorna 400 com uma mensagem clara e nunca chega a salvar — o banco é protegido de dados inválidos. Cada retorno de erro diz ao cliente exatamente o que corrigir. Isso é o "falhe claramente" da fase de robustez aplicado à fronteira: em vez de deixar um nome vazio corromper o sistema silenciosamente (ou estourar uma exceção obscura lá no fundo do banco), a API o rejeita na porta de entrada, com uma explicação útil. A ordem importa: as verificações vêm antes de qualquer operação no banco, funcionando como um "porteiro" que barra o que não deve entrar.
Validação declarativa: as anotações
Escrever if de validação em cada rota funciona, mas repete-se. O C# oferece uma forma mais declarativa: as anotações de dados — atributos que você coloca nas properties da classe, declarando as regras, e que o sistema verifica por você:
using System.ComponentModel.DataAnnotations;
class Produto
{
public int Id { get; set; }
[Required(ErrorMessage = "O nome é obrigatório.")]
[StringLength(100, ErrorMessage = "Máximo de 100 caracteres.")]
public string Nome { get; set; } = "";
[Range(0.01, 100000, ErrorMessage = "O preço deve ser positivo.")]
public decimal Preco { get; set; }
}
Os atributos [Required], [StringLength] e [Range] declaram as regras junto do dado — a validação vira parte da descrição do modelo, não código espalhado pelas rotas. É a diferença entre validação imperativa (você escreve os if, passo a passo) e declarativa (você descreve as regras e o sistema as aplica). A segunda é mais limpa e menos sujeita a esquecimentos: as regras ficam num só lugar, visíveis, junto da property que restringem. Cada abordagem tem seu valor — os if dão controle total para regras complexas; as anotações são concisas para regras comuns —, e você usará ambas conforme o caso.
Tratando erros inesperados: o código 500
Erros de validação (culpa do cliente, que enviou algo inválido) merecem o código 400. Mas e os erros inesperados dentro do seu próprio código — uma exceção não prevista, o banco de dados fora do ar? Esses são o código 500 (Erro Interno do Servidor), que diz "algo deu errado do meu lado". Aqui há uma regra de ouro, herdada da fase de robustez: nunca vaze detalhes internos — a mensagem crua de uma exceção, informações técnicas do sistema — para o cliente. Isso confunde quem consome a API e pode revelar informações sensíveis sobre o seu sistema a quem não deveria vê-las. Em vez disso, você registra o erro internamente (num log) e devolve ao cliente uma mensagem genérica e educada.
O ASP.NET Core oferece um mecanismo para capturar qualquer exceção não tratada e convertê-la numa resposta 500 limpa, de forma centralizada:
// Captura QUALQUER exceção não tratada e devolve um 500 genérico e limpo.
app.UseExceptionHandler(handler =>
{
handler.Run(async contexto =>
{
contexto.Response.StatusCode = 500;
// Mensagem genérica ao cliente; o detalhe real vai para o log, não para a resposta.
await contexto.Response.WriteAsJsonAsync(new { erro = "Ocorreu um erro interno." });
});
});
Assim, se uma exceção inesperada escapar de qualquer rota, em vez de o cliente receber uma mensagem técnica assustadora, ele recebe um JSON educado e genérico, enquanto você, nos logs do servidor, tem o detalhe completo para investigar. É o equilíbrio entre "falhar claramente para você" (o log detalhado, que ajuda a depurar) e "falhar discretamente para o cliente" (a mensagem genérica, que não expõe nem confunde).
Boas práticas de API, reunidas
Encerro com um punhado de práticas que elevam a qualidade de uma API — no espírito de honestidade do curso, são convenções amplamente adotadas, não leis absolutas, mas cada uma resolve um problema real:
- Valide sempre a entrada. Nunca salve dados não validados; a fronteira é o lugar de barrar o que não presta. É a primeira e mais importante linha de defesa. - Retorne os códigos de status corretos. 200/201/204 para sucessos conforme o caso, 400 para entrada inválida, 404 para não encontrado, 500 para erro interno. A consistência torna a API previsível e fácil de consumir. - Não vaze detalhes internos em erros. Mensagens genéricas ao cliente, detalhes nos logs. - Seja consistente na nomenclatura dos endereços. Endereços no plural e em minúsculas (/produtos, não /Produto), verbos usados conforme seu significado. - Mensagens de erro claras. Quando rejeitar algo, diga por quê, para que o cliente possa corrigir.
Essas práticas não são burocracia; cada uma nasce de um problema concreto que aparece quando uma API encontra o mundo real, com seus clientes variados e seus dados imprevisíveis. Uma API que as segue é uma API em que outros programadores confiam e que é agradável de usar.
O princípio inegociável de qualquer sistema que recebe dados de fora ficou estabelecido: nunca confie na entrada. Toda informação que chega pela rede precisa ser verificada antes de ser usada, e uma requisição malformada deve receber uma recusa clara, com o código apropriado, em vez de provocar uma falha genérica.
A validação declarativa por anotações reduziu bastante o trabalho manual, e o tratamento centralizado de erros inesperados garantiu que nenhuma exceção escape na forma de detalhes internos expostos ao cliente. O conjunto de boas práticas reunido aqui é o que separa uma API que funciona em demonstração de uma que aguenta uso real.
Fontes e leituras recomendadas
- Tratamento de erros no ASP.NET Core — o mecanismo de tratamento de exceções e respostas de erro; a base desta aula.
- Validação de modelo — as anotações de dados (
[Required],[Range]) e a validação declarativa. - Respostas de erro padronizadas — o formato recomendado de respostas de erro para APIs.
- Códigos de status HTTP — a lista dos códigos e seus significados, para escolher o certo.
- Boas práticas de design de API — o guia de convenções que tornam uma API robusta e agradável.
Exercícios
Exercício 1
Adicione validação à rota POST /produtos: rejeite com 400 (e uma mensagem clara) nomes vazios e preços menores ou iguais a zero. Teste enviando um nome vazio e confirme que recebe 400, e que o produto não é criado no banco.
Ver resposta
✓ Resposta: A rota com validação retorna Results.BadRequest("O nome é obrigatório.") quando string.IsNullOrWhiteSpace(novo.Nome), e outra mensagem para preço menor ou igual a zero, antes de db.Produtos.Add(...). Enviando um nome vazio via POST, a resposta é 400 com a mensagem, e como o return acontece antes do SaveChangesAsync, nada é gravado — confirmável por um GET seguinte que não mostra o produto. A validação protege o banco na fronteira.
Exercício 2
Reescreva a validação do exercício anterior de forma declarativa, usando anotações ([Required], [Range]) na classe Produto. Explique a diferença entre essa abordagem e os if de validação, e cite uma vantagem de cada.
Ver resposta
✓ Resposta: Com anotações:
class Produto
{
public int Id { get; set; }
[Required(ErrorMessage = "O nome é obrigatório.")]
public string Nome { get; set; } = "";
[Range(0.01, 100000, ErrorMessage = "O preço deve ser positivo.")]
public decimal Preco { get; set; }
}
A diferença: a abordagem declarativa (anotações) coloca as regras junto do dado, como parte da descrição do modelo, e o sistema as aplica automaticamente — menos código repetido e menos risco de esquecer uma checagem em alguma rota. A imperativa (if) dá controle explícito e é flexível para regras complexas que não cabem num atributo simples, mas espalha e repete a lógica. Vantagem da declarativa: concisão e consistência (as regras num só lugar); vantagem da imperativa: controle total para lógica de validação elaborada.
Exercício 3
Adicione o mecanismo de tratamento global de exceções à sua API. Depois, force uma exceção proposital numa rota (por exemplo, lançando uma com throw) e confirme que o cliente recebe um 500 com mensagem genérica, e não o detalhe técnico da exceção.
Ver resposta
✓ Resposta: Após adicionar app.UseExceptionHandler(...) que retorna 500 com { erro = "Ocorreu um erro interno." }, ao lançar uma exceção proposital numa rota (throw new Exception("teste");), o cliente recebe o JSON genérico com status 500, e não o detalhe técnico da exceção. O detalhe real ficaria disponível nos logs do servidor. Isso confirma que o mecanismo captura exceções não tratadas e devolve uma resposta limpa e segura.
Exercício 4
Explique a diferença entre quando retornar 400 e quando retornar 500. Dê um exemplo concreto de cada, deixando claro de quem é a "culpa" (do cliente ou do servidor) em cada caso.
Ver resposta
✓ Resposta: Retorna-se 400 quando a culpa é do cliente: ele enviou um pedido inválido (nome vazio, preço negativo, dado faltando) — o problema está na entrada, e o cliente pode corrigi-la. Retorna-se 500 quando a culpa é do servidor: algo deu errado no seu código ou infraestrutura (uma exceção não prevista, o banco fora do ar) — o cliente não fez nada de errado e não pode corrigir. Exemplo de 400: um POST com preço negativo. Exemplo de 500: uma consulta que falha porque o banco de dados está indisponível. A distinção comunica ao cliente se ele deve ajustar o pedido (400) ou apenas tentar de novo mais tarde (500).
Exercício 5
Sem código: a aula recomenda "nunca vazar detalhes internos em erros" para o cliente. Explique os dois motivos pelos quais devolver a mensagem crua de uma exceção ao cliente é uma má prática, e o que você deveria fazer com esse detalhe em vez de enviá-lo.
Ver resposta
✓ Resposta: Devolver a mensagem crua de uma exceção ao cliente é má prática por dois motivos. Primeiro, segurança: a mensagem técnica pode revelar informações internas sensíveis sobre o sistema — estrutura do banco, caminhos de arquivos, detalhes de configuração —, que poderiam ser exploradas por alguém mal-intencionado. Segundo, clareza: uma mensagem técnica é confusa e inútil para quem consome a API, que não entende o jargão interno e não sabe o que fazer com ele. Em vez de enviar esse detalhe ao cliente, você deve registrá-lo internamente (num log do servidor), onde você e sua equipe têm acesso a ele para investigar e corrigir o problema, e devolver ao cliente apenas uma mensagem genérica e educada (como "Ocorreu um erro interno"). Assim, você preserva a informação necessária para depurar sem expô-la nem confundir quem usa a API.