APIs REST - Aplicando boas práticas
    Neste artigo vamos apresentar boas práticas que podemos usar na criação de APIs REST na
    plataforma .NET.

Uma API REST - Representational State Transfer -  é um estilo arquitetural para sistemas de software distribuídos, especialmente na web que define um conjunto de princípios para projetar serviços web que são flexíveis, escaláveis e interoperáveis.


Criar uma API que apenas "funciona" é simples. O verdadeiro desafio é projetar serviços que sejam escaláveis, fáceis de manter, seguros e intuitivos para quem os consome.

Neste artigo, vamos direto ao ponto: o que define uma API REST de alta qualidade e como aplicar essas boas práticas na prática com ASP.NET Core / Minimal APIs.

Existem três princípios para o design da API REST:

1- Sem Estado: Cada solicitação do cliente ao servidor deve conter todas as informações necessárias para compreender e processar a solicitação.

2- Uso de cache: As respostas devem ser definidas como armazenáveis em cache ou não armazenáveis em cache para melhorar o desempenho do lado do cliente.

3- Interface Uniforme: Uma forma padronizada de comunicação entre cliente e servidor, aumentando a simplicidade e a dissociação.

Uma API REST deve ser orientada a recursos, o que significa que os recursos (como clientes, pedidos, produtos) devem ser claramente identificáveis e acessíveis através de URIs significativas. As URIs devem ser intuitivas e fornecer uma maneira fácil de identificar e manipular os recursos.

Os métodos HTTP (GET, POST, PUT, DELETE, etc.) devem ser utilizados de acordo com as operações que estão sendo realizadas nos recursos. Por exemplo, GET para recuperar dados, POST para criar novos recursos, PUT/PATCH para atualizar recursos existentes e DELETE para excluir recursos.

Criando uma API REST

Para criar uma API REST na plataforma .NET, você pode usar o framework ASP.NET Core, que oferece suporte nativo para o desenvolvimento de APIs RESTful. Aqui está um exemplo básico de como criar uma API REST utilizando o ASP.NET Core:

Passo 1: Crie um novo projeto ASP.NET Core Web API.
Passo 2: Defina seus controladores.

Crie controladores para lidar com as solicitações HTTP. Um controlador é uma classe que herda de ControllerBase e contém métodos para manipular diferentes tipos de solicitações HTTP (GET, POST, PUT, DELETE).

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
}

O conteúdo do controlador vai depender geralmente do domínio da sua aplicação.

Vejamos a seguir algumas práticas recomendadas para criar APIs REST.

1. Design de Endpoints Orientado a Recursos

Endpoints devem focar em recursos (substantivos), e não em ações (verbos). O verbo já é fornecido pelo método HTTP.

❌ Como NÃO fazer (Gambiarras comuns)
GET /getTodosLivros 
POST /criarNovoLivro 
POST /deletarLivro?id=5

O principal problema com essas rotas é que elas foram projetadas usando verbos RPC (Remote Procedure Call) em vez do estilo RESTful.

Em uma API REST, a URL deve identificar o recurso (o "substantivo"), enquanto a ação que você deseja realizar é definida pelo método HTTP (o "verbo").

✅ Como FAZER (Padrão RESTful)
GET    /api/v1/livros          # Obter lista de livros
GET    /api/v1/livros/{id}     # Obter um livro específico
POST   /api/v1/livros          # Criar um novo livro
PUT    /api/v1/livros/{id}     # Atualizar o livro inteiro
PATCH  /api/v1/livros/{id}     # Atualização parcial do livro
DELETE /api/v1/livros/{id}     # Remover um livro 

Dica de Ouro: Mantenha os nomes no plural (/livros, /clientes, /pedidos) e preserve a hierarquia para recursos relacionados (ex: /api/v1/clientes/{id}/pedidos).

2. Métodos HTTP e Status Codes Adequados

Um erro clássico em APIs fracas é retornar 200 OK para tudo, mesmo quando ocorreu uma falha, ou ignorar os códigos de status semânticos.
Método Finalidade Sucesso Típico    
GET Ler dados 200 OK    
POST Criar recursos 201 Created (header location)    
PUT/PATCH Atualizar 200 OK ou 204 No Content    
DELETE Deletar/Remover 204 No Content    

Exemplo ASP.NET Core :
[ApiController]
[Route("api/v1/[controller]")]
public class LivrosController : ControllerBase
{
    private readonly ILivroService _livroService;
    public LivrosController(ILivroService livroService) => _livroService = livroService;
    [HttpGet("{id:int}")]
    public async Task<IActionResult> GetById(int id)
    {
        var livro = await _livroService.ObterPorIdAsync(id);
        if (livro is null)
            return NotFound(); // 404 Not Found
        return Ok(livro); // 200 OK
    }
    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CriarLivroDto dto)
    {
        var livroCriado = await _livroService.CriarAsync(dto);
        
        // Retorna 201 Created + Header Location para GET /api/v1/livros/{id}
        return CreatedAtAction(nameof(GetById), new { id = livroCriado.Id }, livroCriado);
    }
} 

3. Entendendo a Idempotência: PUT vs. POST

A diferença fundamental entre PUT e POST se resume à idempotência:

POST (Não Idempotente): Executar a mesma requisição 3 vezes pode criar 3 registros duplicados no banco de dados.

PUT (Idempotente): Executar a mesma requisição 1 ou 100 vezes resultará exatamente no mesmo estado final do recurso

Imagine que o usuário clicou no botão "Confirmar Compra" no site.

a. Exemplo com POST (Não Idempotente)

No POST, o cliente não sabe o ID do recurso antes de criá-lo. Ele envia os dados para a coleção e o servidor gera um novo ID a cada chamada.
POST /api/v1/pedidos
Content-Type: application/json
{
  "clienteId": 1024,
  "valorTotal": 150.00,
  "itens": ["Livro C#", "Caneca"]
}

1ª chamada: O servidor cria o pedido ID 101 no banco de dados.

O problema da rede: A conexão do usuário oscila bem na hora de receber a resposta 201 Created. O usuário se assusta e clica em "Confirmar Compra" mais 2 vezes.

2ª chamada: O servidor recebe o POST e cria o pedido ID 102.
3ª chamada: O servidor recebe o POST e cria o pedido ID 103.

Resultado: O cliente comprou 3 vezes o mesmo item e teve 3 cobranças no cartão. A chamada repetida alterou o estado final do sistema (gerou duplicidade).

b. Exemplo com PUT (Idempotente)

No PUT, a operação serve para substituir ou definir exatamente o estado do recurso no caminho especificado (ou criar se ele já souber o ID exato).

Cenário A: Atualizando um recurso existente

Você quer alterar a quantidade de um item do pedido ID 101.
PUT /api/v1/pedidos/101
Content-Type: application/json
{
  "clienteId": 1024,
  "valorTotal": 200.00,
  "status": "AguardandoPagamento"
}

1ª chamada: O servidor pega o pedido 101 e substitui seus dados. status vira "AguardandoPagamento".
2ª chamada (repetida por erro de rede): O servidor pega o pedido 101 e substitui novamente pelos mesmos dados.
100ª chamada: O resultado no banco continua sendo exatamente o mesmo.

Cenário B: Criando com um ID fornecido pelo Cliente (GUID/UUID)

Para evitar o problema de cobrança dupla no checkout usando PUT, o front-end gera um ID único (GUID) antes de enviar:
PUT /api/v1/pedidos/d3b07384-d113-4601-a79b-2a2d4b967d2e
Content-Type: application/json

{
"clienteId": 1024,
"valorTotal": 150.00
}

1ª chamada: O pedido d3b0... não existe, então o servidor o cria.
2ª chamada: O pedido d3b0... já existe, então o servidor apenas atualiza/sobscreve ele com o mesmo conteúdo.

Resultado: Não importa quantas vezes o cliente clique no botão ou quantas retransmissões de rede aconteçam, existirá apenas 1 pedido no banco de dados.

Resumo Comparativo :
 POST /api/v1/pedidos       --->  Servidor cria ID 1
 POST /api/v1/pedidos       --->  Servidor cria ID 2  (Estado mudou! Regs. duplicados)

 PUT /api/v1/pedidos/101    --->  Define dados do ID 101
 PUT /api/v1/pedidos/101    --->  Define os MESMOS dados do ID 101 (Estado final idêntico)

4. Padronização no Tratamento de Erros (RFC 7807)

Tratar erros de forma adequada em uma API REST na plataforma .NET é crucial para fornecer uma experiência de usuário robusta e amigável. Aqui estão algumas recomendações para o tratamento de erros em uma API REST usando ASP.NET Core:

Padronização de respostas de erro: Defina um formato consistente para as respostas de erro retornadas pela sua API. Isso pode incluir informações como código de erro, mensagem de erro e talvez detalhes adicionais sobre a causa do erro. Use formatos comuns como JSON para tornar as respostas de erro fáceis de serem consumidas pelos clientes.

Utilização de códigos de status apropriados: Retorne códigos de status HTTP adequados para indicar o resultado da solicitação. Por exemplo, use o código de status 404 Not Found para recursos não encontrados, 400 Bad Request para solicitações malformadas e 500 Internal Server Error para erros do servidor.

Log de erros: Registre detalhes sobre os erros que ocorrem na sua API, incluindo informações como a pilha de chamadas, para que você possa diagnosticar e corrigir problemas rapidamente. Use bibliotecas de log populares, como Serilog ou NLog, para facilitar o registro de informações de erro.

Tratamento global de exceções: Implemente um middleware de tratamento de exceções global para capturar e lidar com exceções não tratadas em toda a sua aplicação. Isso garante que você possa interceptar e responder a todas as exceções de forma consistente.

Mensagens de erro amigáveis: Forneça mensagens de erro significativas e amigáveis para os usuários da sua API. Isso pode ajudar os desenvolvedores a entender e corrigir problemas mais facilmente.

Documentação de erros: Documente os possíveis erros que podem ocorrer ao usar sua API, incluindo as condições que levam a cada erro e como os desenvolvedores podem lidar com eles.

Testes de erro: Realize testes extensivos para garantir que sua API lide adequadamente com uma variedade de cenários de erro, incluindo solicitações inválidas, falhas de validação e problemas de integração.

Nunca retorne mensagens de erro genéricas como {"erro": "ocorreu uma falha"} ou exceções brutas contendo StackTrace em produção. No .NET 8+, utilize o padrão nativo Problem Details (RFC 7807).

Resposta de Erro Padronizada (JSON)
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",

  "title": "Um ou mais erros de validação ocorreram.",
  "status": 400,
  "errors": {
    "Preco": ["O preço deve ser maior que zero."]
  }
} 

Como habilitar globalmente no
Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

// Habilita a exibição padronizada de detalhes de problemas
builder.Services.AddProblemDetails(); 
var app = builder.Build();
// Intercepta exceções não tratadas e converte para ProblemDetails
app.UseExceptionHandler(); 
app.UseStatusCodePages();
app.MapControllers();
app.Run(); 

5. Estratégias de Versionamento


Apis evoluem. Para evitar quebrar contratos com clientes antigos, adote versionamento desde o primeiro dia. O método baseado na URL é o mais legível e recomendado para a maioria dos cenários.

O versionamento de uma API REST é o processo de gerenciar e manter diferentes versões da mesma API. À medida que uma API é desenvolvida e evolui ao longo do tempo, podem surgir alterações que impactam a compatibilidade com versões anteriores.

O versionamento ajuda a lidar com essas mudanças, permitindo que os clientes continuem a usar versões mais antigas da API enquanto as atualizações são feitas.

No contexto do ASP.NET Core, existem várias abordagens para o versionamento de APIs REST. Algumas das abordagens comuns incluem:

Versionamento baseado em URL:

Nessa abordagem, a versão da API é incluída na URL. Por exemplo, /api/v1/products e /api/v2/products indicam diferentes versões da rota de produtos. O ASP.NET Core oferece suporte nativo para esse tipo de versionamento.

Versionamento baseado em cabeçalho:

Aqui a versão da API é especificada em um cabeçalho HTTP, como Accept ou Custom-Version. O ASP.NET Core pode ser configurado para extrair a versão da solicitação a partir desses cabeçalhos.

Versionamento baseado em parâmetros de consulta:

Nessa abordagem, a versão da API é especificada como um parâmetro de consulta na URL. Por exemplo, /api/products?version=1 e /api/products?version=2.

Embora menos comum, o ASP.NET Core pode ser configurado para suportar essa abordagem.
// Exemplo utilizando a biblioteca Asp.Versioning.Mvc
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class ProdutosV1Controller : ControllerBase { /* ... */ }
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class ProdutosV2Controller : ControllerBase { /* ... */ } 

6. Checklist de Segurança para APIs REST no .NET

Garantir a segurança de uma API REST é essencial para proteger os dados e os recursos da aplicação. Aqui estão algumas recomendações de segurança importantes para uma API REST:

Autenticação: Exija autenticação para acessar a API. Isso pode ser feito usando tokens de acesso, como JSON Web Tokens (JWT), OAuth 2.0, ou autenticação baseada em tokens. Certifique-se de usar um método seguro de armazenamento e transmissão de credenciais de autenticação.

Autorização: Implemente controle de acesso baseado em papéis (role-based access control - RBAC) para determinar quais usuários têm permissão para acessar quais recursos da API. Isso garante que apenas usuários autorizados possam realizar operações específicas na API.

Validação de entrada: Valide e sanitize todos os dados de entrada recebidos pela API para prevenir ataques de injeção, como SQL injection e cross-site scripting (XSS). Use técnicas como validação de tipo, validação de comprimento máximo, escape de caracteres especiais, entre outros.

Proteção contra ataques de CSRF e XSS: Implemente medidas de segurança para proteger contra ataques de falsificação de solicitação entre sites (Cross-Site Request Forgery - CSRF) e ataques de script entre sites (Cross-Site Scripting - XSS), como usar tokens anti-CSRF (CSRF tokens) e sanitizar a entrada de dados.

Criptografia: Utilize criptografia para proteger os dados transmitidos entre o cliente e o servidor. Isso pode ser feito usando HTTPS (SSL/TLS) para criptografar a comunicação e garantir a integridade dos dados.

Limitação de taxa (Rate limiting): Implemente limites de taxa para evitar ataques de negação de serviço (DoS) e de força bruta, limitando o número de solicitações que um cliente pode fazer dentro de um determinado período de tempo.
// Habilitando Rate Limiting no Program.cs (.NET 7+)
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("FixedPolicy", opt =>
    {
        opt.PermitLimit = 100;
        opt.Window = TimeSpan.FromMinutes(1);
    });
}); 

Monitoramento e logging: Implemente logs de auditoria para registrar todas as atividades na API, incluindo solicitações, respostas, erros e acessos não autorizados. Monitore regularmente os logs para identificar possíveis ameaças à segurança e anomalias de tráfego.

Atualizações e patches: Mantenha a API atualizada com as últimas correções de segurança e patches de software. Certifique-se de que todos os componentes da infraestrutura, como frameworks, bibliotecas e sistemas operacionais, estejam atualizados para mitigar vulnerabilidades conhecidas.

Segurança do servidor: Implemente medidas de segurança em nível de servidor, como firewalls, configurações de segurança do sistema operacional, proteção contra malware e outras técnicas de hardening para proteger contra ataques externos.

Testes de segurança: Realize regularmente testes de segurança, como testes de penetração e avaliações de vulnerabilidade, para identificar e corrigir possíveis brechas de segurança na API antes que se tornem exploitàveis.

HTTPS Sempre: Redirecione todo tráfego HTTP para HTTPS (app.UseHttpsRedirection()).

7. Documentação Viva com OpenAPI / Swagger

Uma API sem documentação é uma API inútil para quem precisa integrá-la. Utilize o Swagger para documentar endpoints, parâmetros, esquemas de DTOs e respostas de erro de forma automática.
/// <summary>
/// Obtém os detalhes de um livro pelo seu identificador.
/// </summary>
/// <param name="id">ID do livro</param>
/// <response code="200">Retorna o livro solicitado</response>
/// <response code="404">Se o livro não for encontrado</response>
[HttpGet("{id}")]
[ProducesResponseType(typeof(LivroDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetById(int id) { /* ... */ }

Para garantir que sua API REST na plataforma .NET seja bem compreendida e facilmente adotada pelos desenvolvedores, é essencial fornecer uma documentação clara e abrangente. Aqui estão algumas recomendações para criar uma boa documentação para sua API REST:

Descrição geral da API: Forneça uma visão geral da API, incluindo seu propósito, funcionalidades principais e os recursos que ela expõe. Isso ajuda os desenvolvedores a entenderem o propósito e o escopo da API.

Guia de início rápido: Forneça um guia passo a passo para os desenvolvedores começarem a usar sua API rapidamente. Isso pode incluir instruções para configuração, autenticação, exemplos de solicitações e respostas, etc.

Descrição dos recursos: Documente todos os recursos da API, incluindo detalhes sobre os endpoints disponíveis, os parâmetros de solicitação aceitos, os formatos de resposta esperados e os códigos de status retornados.

Exemplos de uso: Inclua exemplos de solicitações e respostas para cada endpoint da
API. Isso ajuda os desenvolvedores a entenderem como interagir com a API e a formatar corretamente as solicitações e respostas.

Modelos de dados: Documente os modelos de dados usados pela API, incluindo os campos e tipos de dados aceitos em cada solicitação e resposta. Isso ajuda os desenvolvedores a entenderem a estrutura dos dados manipulados pela API.

Autenticação e autorização: Descreva os métodos de autenticação e autorização suportados pela API, incluindo como obter e usar tokens de acesso ou chaves de API. Forneça exemplos de como autenticar solicitações na API.

Limitações e políticas de uso: Documente quaisquer limitações ou políticas de uso da API, como limites de taxa, quotas de acesso ou restrições de uso. Isso ajuda os desenvolvedores a entenderem as restrições impostas pela API.

Referência da API: Forneça uma referência completa da API, incluindo uma lista de todos os endpoints, parâmetros de solicitação e respostas suportadas, e os códigos de status retornados. Isso ajuda os desenvolvedores a consultar rapidamente a documentação para encontrar informações sobre endpoints específicos.

Ferramentas de interação com a API: Se possível, forneça ferramentas de interação com a API, como clientes HTTP ou SDKs para diferentes linguagens de programação. Isso facilita a integração e o desenvolvimento de aplicativos que consomem a API.

Manutenção da documentação: Mantenha a documentação atualizada com as últimas alterações na API. Isso inclui atualizar exemplos, modelos de dados e referências de endpoint sempre que a API for atualizada ou alterada.

8. Testando a sua API

Quando se trata de testar uma API REST em .NET, é importante adotar boas práticas de teste de unidade para garantir que sua API esteja funcionando conforme o esperado e que seja robusta o suficiente para lidar com diferentes cenários. Aqui estão algumas recomendações e boas práticas para testar APIs REST no .NET:

Separação de camadas: Estruture sua aplicação de forma que a lógica de negócios, a camada de acesso a dados e a lógica de apresentação estejam separadas. Isso permite que você teste cada camada isoladamente, facilitando a escrita e a execução de testes de unidade.

Testes unitários: Escreva testes unitários para cada componente da sua API REST, incluindo controladores, serviços e classes utilitárias. Isso ajuda a garantir que cada parte da sua API esteja funcionando corretamente de forma isolada.

Mocks e stubs: Use mocks e stubs para isolar as dependências externas da sua API durante os testes. Isso permite que você simule o comportamento de objetos externos, como bancos de dados ou serviços web, tornando os testes mais previsíveis e independentes.

Testes de integração: Além dos testes unitários, escreva testes de integração para validar a interação entre os diferentes componentes da sua API. Isso pode incluir testes de integração de ponta a ponta para simular cenários reais de uso da API.

Testes automatizados: Automatize a execução dos seus testes de unidade e integração para garantir que eles sejam executados regularmente como parte do processo de desenvolvimento. Isso ajuda a identificar problemas o mais cedo possível e a evitar regressões.

Dados de teste: Use dados de teste realistas e representativos ao escrever seus testes de unidade e integração. Isso ajuda a garantir que seus testes cubram uma variedade de cenários de uso da API e identifiquem potenciais problemas antes de serem implantados em produção.

Testes de desempenho: Além dos testes funcionais, considere realizar testes de desempenho para avaliar o desempenho da sua API REST em condições de carga e estresse. Isso pode ajudar a identificar gargalos de desempenho e otimizar a sua API conforme necessário.

Testes de segurança: Verifique se sua API REST está protegida contra vulnerabilidades de segurança com testes de segurança regulares. Isso pode incluir testes de segurança automatizados, revisões de código e auditorias de segurança.

Conclusão:

Projetar boas APIs REST na plataforma .NET exige equilíbrio entre seguir a convenção arquitetural e manter o código limpo e idiomático. Ao adotar endpoints semânticos, status codes corretos, respostas de erro padronizadas com ProblemDetails e segurança desde o início, suas APIs ganham em robustez, escalabilidade e facilidade de integração.

E estamos conversados ...

"Bendito seja o Deus e Pai de nosso Senhor Jesus Cristo, o Pai das misericórdias e o Deus de toda a consolação"
2 Coríntios 1:3

Referências:


José Carlos Macoratti