Como projetar uma API REST moderna com ASP.NET Core

Como projetar uma API REST moderna com ASP.NET Core
Publicado em 13/10/2026 por Admin

Desenvolver uma API REST com ASP.NET Core vai muito além de criar endpoints que recebem requisições HTTP e retornam dados em JSON. Uma API bem projetada precisa oferecer contratos consistentes, segurança, desempenho, facilidade de manutenção e uma arquitetura preparada para evoluir conforme as necessidades da aplicação.

O ecossistema .NET oferece diferentes recursos para construir APIs modernas, incluindo Minimal APIs, Controllers, injeção de dependência, validação de dados, autenticação, autorização, documentação com OpenAPI e integração com ferramentas de observabilidade.

Mas como combinar esses recursos sem transformar um projeto simples em uma arquitetura excessivamente complexa?

Neste artigo, vamos explorar os principais conceitos envolvidos no desenvolvimento de uma API REST moderna com ASP.NET Core, apresentar exemplos práticos em C# e discutir as decisões arquiteturais que fazem diferença em projetos reais.

O que caracteriza uma API REST moderna?

Uma API REST é uma interface que permite a comunicação entre sistemas por meio dos recursos e das operações disponibilizadas através do protocolo HTTP.

Em uma aplicação de gestão de produtos, por exemplo, podemos disponibilizar endpoints para consultar, cadastrar, atualizar e remover produtos.

Uma implementação básica poderia oferecer as seguintes operações:

Método HTTP Endpoint Finalidade
GET /api/v1/products Listar produtos
GET /api/v1/products/10 Consultar um produto específico
POST /api/v1/products Cadastrar um produto
PUT /api/v1/products/10 Substituir os dados de um produto
PATCH /api/v1/products/10 Atualizar parcialmente um produto
DELETE /api/v1/products/10 Remover um produto

Essa estrutura segue uma convenção importante: os endpoints representam recursos, enquanto os métodos HTTP expressam as operações realizadas sobre eles.

Uma API moderna deve considerar outros aspectos além da organização das rotas:

  • Contratos de entrada e saída bem definidos.

  • Códigos HTTP utilizados corretamente.

  • Validação dos dados recebidos.

  • Tratamento consistente de erros.

  • Autenticação e autorização.

  • Documentação acessível para outros desenvolvedores.

  • Testes automatizados.

  • Logs, métricas e rastreamento.

  • Estratégias de versionamento e compatibilidade.

  • Proteção contra requisições abusivas.

Esses elementos ajudam a construir uma interface previsível, facilitando tanto o consumo da API quanto sua evolução ao longo do tempo.

1. Escolha a abordagem adequada: Controllers ou Minimal APIs

O ASP.NET Core oferece duas abordagens principais para desenvolver APIs HTTP: Controllers e Minimal APIs.

As duas permitem criar aplicações completas. A escolha depende do contexto, da organização desejada e das necessidades do projeto.

Quando utilizar Controllers?

Controllers são uma opção interessante para aplicações que precisam de uma estrutura explícita para organizar endpoints, atributos, filtros e comportamentos compartilhados.

Essa abordagem costuma funcionar bem em APIs corporativas com diversos recursos, regras de negócio, equipes de desenvolvimento e padrões estabelecidos.

Um Controller pode ser implementado da seguinte maneira:

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/v1/products")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id:int}")]
    public ActionResult GetById(int id)
    {
        if (id <= 0)
        {
            return BadRequest();
        }

        var product = new ProductResponse(
            id,
            "Teclado mecânico",
            89.90m
        );

        return Ok(product);
    }
}

public record ProductResponse(
    int Id,
    string Name,
    decimal Price
);

Nesse exemplo, o Controller disponibiliza um endpoint para consultar um produto pelo identificador.

O atributo [ApiController] habilita comportamentos específicos para APIs, incluindo respostas automáticas de erro HTTP 400 em determinadas situações de falha na validação do modelo.

Já o atributo [Route] define a rota base do recurso, enquanto [HttpGet("{id:int}")] determina que o método responde a requisições GET com um identificador inteiro.

O tipo ActionResult permite retornar tanto o objeto esperado quanto resultados HTTP alternativos, como BadRequest() e NotFound().

Em um projeto real, a consulta seria delegada a um serviço de aplicação ou componente responsável pelo acesso aos dados, em vez de manter informações fictícias diretamente no Controller.

Quando utilizar Minimal APIs?

As Minimal APIs permitem criar endpoints com menos código e configuração.

São particularmente úteis para serviços pequenos, microsserviços, aplicações com endpoints bem delimitados e projetos nos quais uma implementação mais direta favorece a produtividade.

Veja um exemplo equivalente:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet(
    "/api/v1/products/{id:int}",
    (int id) =>
    {
        if (id <= 0)
        {
            return Results.BadRequest();
        }

        var product = new ProductResponse(
            id,
            "Teclado mecânico",
            89.90m
        );

        return Results.Ok(product);
    });

app.Run();

public record ProductResponse(
    int Id,
    string Name,
    decimal Price
);

A estrutura utiliza WebApplication para configurar a aplicação e MapGet para registrar o endpoint.

Apesar da simplicidade, Minimal APIs também oferecem suporte a recursos importantes, como injeção de dependência, autorização, filtros, validação e documentação OpenAPI.

Qual abordagem é melhor?

Para novos projetos, a documentação atual do ASP.NET Core recomenda Minimal APIs como uma opção para construir APIs HTTP com menos código e configuração. Controllers continuam sendo uma alternativa válida, especialmente quando sua estrutura explícita atende melhor às necessidades da aplicação.

A decisão não deve ser baseada apenas na quantidade de linhas de código.

Considere fatores como:

  • Complexidade das regras de negócio.

  • Quantidade de endpoints.

  • Padrões arquiteturais da equipe.

  • Necessidade de filtros e convenções compartilhadas.

  • Familiaridade dos desenvolvedores com cada abordagem.

  • Facilidade de testes e manutenção.

Em uma API corporativa com muitos recursos, Controllers podem proporcionar uma organização familiar. Em um serviço pequeno, Minimal APIs podem reduzir a quantidade de código estrutural.

O importante é escolher uma abordagem e aplicá-la de maneira consistente.

2. Organize o projeto para separar responsabilidades

Um dos problemas mais comuns em APIs que crescem sem planejamento é a concentração de responsabilidades nos Controllers ou nos próprios endpoints.

Quando uma única classe recebe a requisição, valida os dados, executa regras de negócio, consulta o banco, transforma os resultados e trata todas as exceções, a manutenção tende a se tornar mais difícil.

Uma organização mais clara separa as responsabilidades de acordo com o papel de cada componente.

Uma estrutura possível é:

MyApi/
├── Controllers/
│   └── ProductsController.cs
├── Contracts/
│   ├── CreateProductRequest.cs
│   └── ProductResponse.cs
├── Services/
│   ├── IProductService.cs
│   └── ProductService.cs
├── Repositories/
│   ├── IProductRepository.cs
│   └── ProductRepository.cs
├── Domain/
│   └── Product.cs
├── Infrastructure/
│   └── AppDbContext.cs
├── Middleware/
│   └── ExceptionHandlingMiddleware.cs
├── Program.cs
└── appsettings.json

Essa estrutura é apenas uma referência. O projeto pode ser organizado de outras maneiras, dependendo de seu tamanho e das convenções adotadas.

Em aplicações maiores, também pode fazer sentido dividir a solução em projetos separados, como:

  • API: recebe requisições HTTP e configura a aplicação.

  • Application: coordena os casos de uso.

  • Domain: concentra o modelo e as regras de negócio.

  • Infrastructure: implementa o acesso a bancos de dados e serviços externos.

Essa organização é compatível com abordagens como Clean Architecture, mas não é necessário adotar todas essas camadas para qualquer aplicação.

Uma API pequena pode funcionar perfeitamente em um único projeto, desde que as responsabilidades estejam bem delimitadas.

O papel de cada camada

O Controller ou endpoint deve cuidar da interação com HTTP: receber os dados, chamar o componente responsável pelo caso de uso e devolver a resposta apropriada.

O serviço de aplicação coordena a operação solicitada e pode aplicar regras relacionadas ao fluxo do caso de uso.

O domínio representa os conceitos e as regras essenciais do negócio.

O repositório, quando utilizado, abstrai operações de persistência, enquanto a infraestrutura contém as implementações concretas de acesso aos dados.

O objetivo dessa separação não é criar interfaces e classes para tudo. É permitir que cada componente tenha uma responsabilidade clara e possa evoluir sem provocar alterações desnecessárias em outras partes do sistema.

3. Defina contratos claros para entrada e saída

Uma API possui um contrato que determina quais dados o consumidor pode enviar e quais informações receberá.

Esse contrato deve ser planejado com cuidado.

Um erro frequente é utilizar diretamente a entidade do banco de dados como modelo de entrada e saída da API.

Considere uma entidade de usuário que contém informações como:

public class User
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public string Email { get; set; } = string.Empty;

    public string PasswordHash { get; set; } = string.Empty;

    public bool IsAdmin { get; set; }
}

Se essa entidade for utilizada diretamente como entrada para cadastrar um usuário, o cliente poderá tentar enviar propriedades que não deveria controlar, como IsAdmin ou PasswordHash.

Dependendo de como o código estiver implementado, isso pode provocar vulnerabilidades relacionadas à atribuição indevida de propriedades.

Uma solução é criar contratos específicos para cada operação.

Por exemplo:

using System.ComponentModel.DataAnnotations;

public record CreateUserRequest(
    [Required]
    [StringLength(100, MinimumLength = 2)]
    string Name,

    [Required]
    [EmailAddress]
    [StringLength(254)]
    string Email,

    [Required]
    [StringLength(128, MinimumLength = 12)]
    string Password
);

O contrato determina quais dados são esperados no cadastro.

Observe que o campo Password existe apenas na requisição. Ele não deve ser armazenado em texto puro nem devolvido ao consumidor.

A aplicação deve encaminhar a senha para um mecanismo apropriado de derivação e armazenamento de credenciais, como um serviço especializado de identidade.

O DTO de resposta pode ser diferente:

public record UserResponse(
    int Id,
    string Name,
    string Email
);

Dessa forma, a API controla explicitamente quais informações são disponibilizadas.

Essa separação também facilita a evolução do contrato sem obrigar a entidade de persistência a acompanhar cada alteração da interface HTTP.

4. Utilize corretamente os métodos e códigos HTTP

Uma API REST previsível utiliza os métodos e códigos de status HTTP de acordo com o resultado da operação.

Não basta retornar HTTP 200 em todas as situações e incluir uma mensagem de erro no corpo da resposta.

Os códigos de status fazem parte do contrato e ajudam clientes, ferramentas de monitoramento e intermediários de rede a interpretar o resultado.

Os mais utilizados incluem:

Código Significado Exemplo
200 OK Requisição concluída com sucesso Consulta ou atualização com resposta
201 Created Recurso criado Cadastro de um produto
202 Accepted Requisição aceita para processamento posterior Solicitação enviada para uma fila
204 No Content Operação concluída sem corpo de resposta Exclusão bem-sucedida
400 Bad Request Requisição inválida Dados ou parâmetros incorretos
401 Unauthorized Autenticação ausente ou inválida Token de acesso inválido
403 Forbidden Acesso não permitido Usuário autenticado sem a permissão necessária
404 Not Found Recurso não encontrado Produto inexistente
409 Conflict Conflito com o estado atual do recurso Código de produto duplicado
422 Unprocessable Content Conteúdo compreendido, mas não processável conforme as regras aplicáveis Dados que não satisfazem uma regra de negócio
429 Too Many Requests Limite de requisições excedido Cliente ultrapassa a política de consumo
500 Internal Server Error Falha inesperada no servidor Erro não tratado

A escolha entre HTTP 400 e HTTP 422, por exemplo, depende do contrato definido para a API. O importante é manter um comportamento consistente e documentado.

Exemplo de criação de um recurso

Em um Controller, podemos utilizar CreatedAtAction para informar que o recurso foi criado e disponibilizar sua localização.

[HttpPost]
public ActionResult Create(
    CreateProductRequest request)
{
    var product = new ProductResponse(
        10,
        request.Name,
        request.Price
    );

    return CreatedAtAction(
        nameof(GetById),
        new { id = product.Id },
        product
    );
}

Nesse exemplo, o identificador é fictício e o código não realiza persistência. Em uma implementação real, o serviço de aplicação criaria o produto e retornaria o identificador efetivamente gerado.

O método CreatedAtAction gera uma resposta HTTP 201 Created e inclui a localização do recurso criado quando a rota correspondente pode ser determinada.

Essa abordagem é mais expressiva do que retornar apenas Ok(product), porque comunica corretamente o resultado da operação.

5. Implemente validação em diferentes níveis

Validar os dados recebidos é uma das primeiras medidas para tornar uma API mais confiável.

Entretanto, é importante distinguir três responsabilidades:

  1. Validação estrutural dos dados.

  2. Validação das regras de negócio.

  3. Validação de permissões e segurança.

A primeira verifica se os dados possuem o formato esperado.

A segunda avalia se a operação é permitida pelas regras do domínio.

A terceira determina se o usuário ou sistema que está fazendo a requisição possui autorização para executar a operação.

Essas validações são complementares.

Validação estrutural

Em uma aplicação baseada em Controllers, podemos utilizar Data Annotations:

using System.ComponentModel.DataAnnotations;

public record CreateProductRequest(
    [Required]
    [StringLength(120, MinimumLength = 3)]
    string Name,

    [Range(typeof(decimal), "0.01", "999999999")]
    decimal Price
);

Com o atributo [ApiController], falhas de validação do modelo normalmente resultam em uma resposta HTTP 400 automática.

Isso evita a necessidade de repetir verificações básicas em cada método do Controller.

Nas Minimal APIs, o comportamento de validação depende da versão do ASP.NET Core e da forma como a aplicação foi configurada. Portanto, não se deve presumir que qualquer contrato com Data Annotations produzirá automaticamente o mesmo comportamento em todas as versões e abordagens.

Validação de regras de negócio

Imagine que uma loja não permita cadastrar dois produtos com o mesmo código interno.

Essa regra não deve ficar limitada à validação do formato da requisição.

A aplicação precisa consultar o estado relevante do sistema e verificar se o código já existe.

Mesmo assim, uma consulta prévia não substitui uma restrição de unicidade no banco de dados: duas requisições concorrentes poderiam passar pela verificação simultaneamente.

Por isso, regras de integridade críticas devem ser protegidas também na persistência, com tratamento adequado dos conflitos.

A validação deve acontecer no local apropriado, considerando as características da regra.

6. Centralize o tratamento de erros

Uma API profissional não deve expor mensagens internas, detalhes de conexão, caminhos de arquivos ou stack traces para os consumidores.

Também não é recomendável espalhar blocos try/catch idênticos por todos os endpoints.

Uma estratégia mais consistente é utilizar um mecanismo centralizado de tratamento de exceções e padronizar o formato das respostas de erro.

O ASP.NET Core oferece suporte a Problem Details, um formato padronizado para representar problemas HTTP.

Um exemplo de resposta seria:

{
  "type": "https://api.exemplo.com/problems/product-not-found",
  "title": "Produto não encontrado",
  "status": 404,
  "detail": "Não foi encontrado um produto com o identificador informado.",
  "instance": "/api/v1/products/123"
}

Os campos type, title, status, detail e instance ajudam a identificar e descrever o problema.

O conteúdo de detail deve ser pensado para o consumidor da API, sem revelar informações internas sensíveis.

Podemos configurar o tratamento de exceções e o suporte a Problem Details no Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddProblemDetails();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler();
}

app.UseHttpsRedirection();

app.MapControllers();

app.Run();

Essa configuração é um ponto de partida. Para utilizá-la em produção, a aplicação deve garantir que o tratamento de exceções esteja corretamente configurado e que os erros sejam registrados nos logs.

Também é importante distinguir erros esperados de falhas inesperadas.

Um produto inexistente, por exemplo, pode ser tratado como um resultado HTTP 404. Já uma falha inesperada de conexão com o banco de dados deve ser registrada e tratada sem expor detalhes técnicos ao cliente.

Nem toda exceção deve ser convertida indiscriminadamente em HTTP 400. Isso esconderia a diferença entre uma requisição inválida e uma falha interna.

7. Utilize injeção de dependência

A injeção de dependência é um recurso central do ASP.NET Core e permite fornecer aos componentes as dependências necessárias sem obrigá-los a criar suas próprias implementações.

Em vez de instanciar diretamente um serviço dentro de um Controller, podemos registrar o serviço no contêiner da aplicação.

Considere a seguinte interface:

public interface IProductService
{
    Task GetByIdAsync(
        int id,
        CancellationToken cancellationToken);
}

Uma implementação poderia utilizar um repositório para consultar os dados:

public class ProductService : IProductService
{
    private readonly IProductRepository _repository;

    public ProductService(IProductRepository repository)
    {
        _repository = repository;
    }

    public async Task GetByIdAsync(
        int id,
        CancellationToken cancellationToken)
    {
        var product = await _repository.GetByIdAsync(
            id,
            cancellationToken);

        if (product is null)
        {
            return null;
        }

        return new ProductResponse(
            product.Id,
            product.Name,
            product.Price
        );
    }
}

O exemplo pressupõe que a interface IProductRepository e o método GetByIdAsync estejam definidos no projeto.

O serviço consulta o repositório e transforma o resultado em um contrato de resposta.

No Program.cs, podemos registrar as implementações:

builder.Services.AddScoped();
builder.Services.AddScoped();

O tempo de vida Scoped é apropriado para muitos serviços relacionados a operações por requisição, incluindo cenários comuns com Entity Framework Core.

Isso não significa que todos os componentes devam ser registrados como Scoped. O tempo de vida correto depende das dependências e do comportamento de cada serviço.

Uma boa utilização da injeção de dependência reduz o acoplamento e facilita a substituição de implementações durante os testes.

Também é importante evitar abstrações desnecessárias. Uma interface só é útil quando contribui para a organização, substituição, testabilidade ou evolução do sistema.

8. Escolha a estratégia de acesso a dados

Uma API REST pode utilizar diferentes tecnologias de persistência, incluindo Entity Framework Core, Dapper ou consultas SQL executadas diretamente.

A escolha deve considerar a complexidade das consultas, o modelo de dados, os requisitos de desempenho e as necessidades de manutenção.

Entity Framework Core

O Entity Framework Core oferece um ORM que permite trabalhar com entidades e consultas utilizando LINQ.

É uma boa opção quando o projeto precisa de recursos como rastreamento de alterações, mapeamento de relacionamentos, integração com transações e suporte a migrations.

Exemplo:

public async Task GetByIdAsync(
    int id,
    CancellationToken cancellationToken)
{
    return await _dbContext.Products
        .AsNoTracking()
        .FirstOrDefaultAsync(
            product => product.Id == id,
            cancellationToken);
}

O método utiliza AsNoTracking() porque a consulta apresenta um cenário de leitura no qual o rastreamento das alterações da entidade não é necessário.

Isso pode reduzir o trabalho do Entity Framework Core em consultas apropriadas, embora o ganho efetivo dependa do contexto e do volume de dados.

Dapper

O Dapper é uma alternativa leve para executar consultas SQL e mapear resultados para objetos .NET.

Pode ser interessante quando o desenvolvedor precisa controlar explicitamente o SQL ou trabalha com consultas específicas que se beneficiam dessa abordagem.

Exemplo:

const string sql = """
    SELECT Id, Name, Price
    FROM Products
    WHERE Id = @Id
    """;

var product = await connection.QuerySingleOrDefaultAsync(
    sql,
    new { Id = id });

O exemplo pressupõe uma conexão configurada e aberta, além de uma classe Product compatível com o resultado da consulta.

Os parâmetros utilizados pelo Dapper ajudam a evitar a concatenação insegura de valores na instrução SQL.

Qual é a melhor escolha?

Não existe uma resposta universal.

O Entity Framework Core oferece uma experiência integrada de ORM e pode acelerar o desenvolvimento de aplicações com modelos de domínio e operações de persistência variadas.

O Dapper permite escrever e controlar SQL de maneira direta, sendo útil em cenários nos quais essa característica é importante.

Também é possível utilizar as duas tecnologias no mesmo sistema, desde que exista uma justificativa clara e uma divisão de responsabilidades bem definida.

O mais importante é medir o desempenho quando necessário e evitar escolher uma ferramenta apenas por popularidade.

9. Projete endpoints com paginação e filtros

Um erro comum em APIs é disponibilizar um endpoint que retorna todos os registros de uma tabela sem qualquer limitação.

Em uma base pequena, isso pode funcionar. Conforme o volume de dados cresce, a consulta pode aumentar o consumo de memória, o tempo de resposta e a quantidade de dados transferidos pela rede.

Uma abordagem mais adequada é permitir paginação e filtros.

Por exemplo:

GET /api/v1/products?page=1&pageSize=20&search=teclado

Nesse contrato, o cliente solicita a primeira página, com até 20 registros, filtrando os resultados pelo termo informado.

Uma resposta poderia seguir este formato:

{
  "items": [
    {
      "id": 10,
      "name": "Teclado mecânico",
      "price": 89.90
    }
  ],
  "page": 1,
  "pageSize": 20,
  "totalItems": 145,
  "totalPages": 8
}

O exemplo é ilustrativo: os valores representam uma resposta possível para uma consulta hipotética.

Uma implementação real deve aplicar os filtros e a paginação diretamente na consulta sempre que possível, evitando carregar todos os registros na memória antes de selecionar a página.

Também é importante validar os parâmetros recebidos.

Por exemplo, page deve ser positivo, e pageSize deve ter um limite máximo definido pela API.

Para conjuntos de dados muito grandes ou que mudam frequentemente, a paginação baseada em cursor pode ser mais adequada do que a paginação por número de página.

A escolha depende dos requisitos de ordenação, consistência e navegação do cliente.

10. Pense em segurança desde o início

A segurança não deve ser adicionada apenas quando a API estiver pronta para produção.

Uma aplicação que manipula dados pessoais, informações financeiras ou operações administrativas precisa estabelecer controles desde o desenho inicial.

Entre as medidas mais importantes estão:

Utilize HTTPS

O HTTPS protege a comunicação entre o cliente e a API, ajudando a preservar a confidencialidade e a integridade dos dados transmitidos.

Em produção, configure corretamente os certificados e o encaminhamento de HTTPS quando a aplicação estiver atrás de um proxy reverso ou balanceador de carga.

Implemente autenticação e autorização

Autenticação é o processo de verificar a identidade de quem faz a requisição.

Autorização determina quais operações essa identidade pode executar.

Em APIs consumidas por aplicações externas, OAuth 2.0 e OpenID Connect são padrões relevantes, e tokens de acesso podem ser utilizados conforme o fluxo de autenticação adotado.

O ASP.NET Core oferece mecanismos para validar tokens e aplicar políticas de autorização.

Não basta verificar se o usuário está autenticado. Também é necessário verificar se ele pode acessar o recurso específico solicitado.

Por exemplo, um usuário autenticado não deve conseguir consultar os dados privados de outro usuário apenas alterando o identificador na URL.

Proteja os dados de entrada

Não confie nos valores enviados pelo cliente.

Valide formatos, limites e regras de negócio. Utilize consultas parametrizadas e evite construir SQL por concatenação de strings.

Também não exponha entidades internas que contenham senhas, tokens, segredos ou outras informações sensíveis.

Gerencie segredos corretamente

Senhas de banco de dados, chaves de API e segredos de autenticação não devem ser publicados no repositório de código.

Durante o desenvolvimento, mecanismos locais de configuração podem ser utilizados. Em produção, prefira um serviço de gerenciamento de segredos adequado à infraestrutura.

Aplique limitação de requisições

Rate limiting ajuda a controlar a quantidade de requisições processadas por determinado período, reduzindo o impacto de clientes abusivos e de padrões de consumo inesperados.

O ASP.NET Core disponibiliza middleware para configurar políticas de limitação.

Os limites devem ser definidos conforme a capacidade do serviço, o perfil dos consumidores e a criticidade das operações.

Rate limiting não substitui autenticação, autorização nem uma estratégia completa de proteção contra ataques distribuídos.

11. Documente a API com OpenAPI

Uma API pode estar tecnicamente correta e ainda ser difícil de consumir quando seus contratos não estão documentados.

O OpenAPI descreve a interface HTTP de uma aplicação em um formato estruturado que pode ser utilizado por ferramentas de documentação, geração de clientes e testes.

Uma boa documentação deve informar:

  • Endpoints disponíveis.

  • Métodos HTTP aceitos.

  • Parâmetros de rota e consulta.

  • Formatos dos corpos das requisições.

  • Estruturas das respostas.

  • Códigos HTTP possíveis.

  • Requisitos de autenticação.

  • Regras de validação relevantes.

No ASP.NET Core, o suporte a OpenAPI permite gerar a descrição da API a partir da aplicação.

Um exemplo de configuração básica para Controllers é:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.MapControllers();

app.Run();

Esse exemplo utiliza os serviços OpenAPI disponibilizados pelo ASP.NET Core. A configuração exata pode variar conforme a versão utilizada e as ferramentas adicionais instaladas.

O documento OpenAPI pode ser disponibilizado por um endpoint da aplicação. Uma interface visual para explorar e testar os endpoints, como Swagger UI, pode exigir configuração ou dependências adicionais.

Em produção, avalie cuidadosamente se a documentação deve ser pública ou restrita a usuários autorizados.

Uma API interna pode precisar de documentação acessível apenas à equipe, enquanto uma API pública pode disponibilizar sua especificação para desenvolvedores externos.

12. Utilize versionamento quando necessário

As APIs evoluem.

Novos campos são adicionados, regras de negócio mudam e funcionalidades antigas podem deixar de atender às necessidades do produto.

O desafio é introduzir essas mudanças sem quebrar os consumidores existentes.

Uma estratégia comum é utilizar versões explícitas nas rotas:

/api/v1/products
/api/v2/products

Esse modelo facilita identificar qual contrato está sendo utilizado.

Entretanto, versionar não significa que toda alteração exige uma nova versão.

Adicionar um campo opcional a uma resposta, por exemplo, pode ser compatível com muitos consumidores, embora clientes que rejeitem propriedades desconhecidas possam se comportar de maneira diferente.

Alterar o significado de um campo existente, remover uma propriedade ou mudar o formato de uma resposta pode provocar incompatibilidades.

Antes de criar uma nova versão, avalie:

  • O impacto sobre os consumidores atuais.

  • A possibilidade de manter compatibilidade.

  • A necessidade de migração dos clientes.

  • O período de suporte à versão anterior.

  • O processo de comunicação de alterações.

A estratégia de versionamento deve ser acompanhada de documentação e planejamento para descontinuar versões antigas de maneira previsível.

13. Prepare a API para observabilidade

Uma API em produção precisa oferecer informações suficientes para que a equipe consiga identificar falhas e compreender o comportamento do sistema.

A observabilidade costuma envolver três pilares complementares:

  • Logs: registram eventos relevantes.

  • Métricas: apresentam indicadores agregados de comportamento e desempenho.

  • Rastreamento distribuído: permite acompanhar uma operação através de diferentes componentes e serviços.

O ASP.NET Core oferece integração com mecanismos de logging e pode ser utilizado com OpenTelemetry para coletar métricas e traces.

Uma configuração básica de logging pode utilizar o ILogger:

public class ProductService : IProductService
{
    private readonly ILogger _logger;

    public ProductService(
        ILogger logger)
    {
        _logger = logger;
    }

    public async Task GetByIdAsync(
        int id,
        CancellationToken cancellationToken)
    {
        _logger.LogInformation(
            "Consultando produto {ProductId}",
            id
        );

        // A consulta real ao repositório seria executada aqui.

        await Task.CompletedTask;

        return null;
    }
}

O método demonstra como registrar um evento com um identificador estruturado.

Ele não implementa uma consulta real e retorna null apenas para manter o exemplo focado no logging.

Em uma aplicação real, os logs devem ser úteis para diagnóstico, mas não devem incluir senhas, tokens de acesso, dados pessoais desnecessários ou outros segredos.

Também vale acompanhar indicadores como:

  • Taxa de requisições.

  • Latência dos endpoints.

  • Frequência de respostas HTTP 5xx.

  • Taxa de erros de dependências externas.

  • Saturação de CPU e memória.

  • Tempo de execução de consultas ao banco.

Esses indicadores ajudam a detectar problemas antes que eles se transformem em indisponibilidade prolongada.

14. Utilize operações assíncronas e CancellationToken

O modelo assíncrono do C# é especialmente importante em APIs que realizam operações de entrada e saída, como consultas ao banco de dados e chamadas a serviços externos.

Utilizar async e await permite que a aplicação não mantenha uma thread bloqueada desnecessariamente enquanto aguarda uma operação de I/O.

Um exemplo de assinatura adequada é:

[HttpGet("{id:int}")]
public async Task> GetById(
    int id,
    CancellationToken cancellationToken)
{
    var product = await _productService.GetByIdAsync(
        id,
        cancellationToken);

    if (product is null)
    {
        return NotFound();
    }

    return Ok(product);
}

Nesse exemplo, o Controller delega a consulta ao serviço e propaga o CancellationToken.

O cancelamento pode permitir que operações compatíveis interrompam trabalho quando a requisição deixa de ser necessária.

Para aproveitar esse comportamento, o token deve ser encaminhado às operações subsequentes que ofereçam suporte a cancelamento, incluindo consultas assíncronas ao banco.

É importante observar que async não torna automaticamente uma consulta mais rápida. Seu principal benefício, nesse cenário, está na utilização mais eficiente dos recursos durante operações de I/O.

Também não é recomendável utilizar Task.Run indiscriminadamente para envolver consultas assíncronas de banco de dados. Isso não transforma uma operação bloqueante em uma operação de I/O verdadeiramente assíncrona.

15. Escreva testes automatizados para os endpoints

Uma API que funciona durante o desenvolvimento ainda pode apresentar problemas quando recebe diferentes tipos de entrada ou quando suas dependências se comportam de maneira inesperada.

Os testes automatizados ajudam a identificar esses problemas antes da publicação.

Uma estratégia de testes pode incluir:

Testes unitários

Verificam componentes isolados, como serviços de aplicação e regras de negócio.

Por exemplo, um teste pode confirmar que o serviço rejeita um produto com preço inválido.

Testes de integração

Verificam a interação entre componentes, como endpoints, persistência, validação e autenticação.

Podem ser implementados utilizando ferramentas do ecossistema .NET, como Microsoft.AspNetCore.Mvc.Testing, com o recurso WebApplicationFactory.

Testes de contrato

Verificam se a API continua respeitando os contratos esperados pelos consumidores.

Podem ajudar a detectar alterações incompatíveis em formatos de resposta, códigos HTTP e regras de integração.

Uma boa suíte de testes deve considerar, pelo menos:

  • Requisições válidas.

  • Parâmetros inválidos.

  • Recursos inexistentes.

  • Usuários sem permissão.

  • Falhas de dependências.

  • Operações concorrentes relevantes.

  • Regressões em regras de negócio.

Os testes devem refletir os riscos reais da aplicação. Não é necessário testar cada linha de código de maneira isolada, mas é importante proteger os comportamentos críticos.

16. Evite complexidade arquitetural desnecessária

Ao estudar arquitetura de software, é fácil concluir que uma API moderna precisa obrigatoriamente utilizar Clean Architecture, CQRS, MediatR, DDD, vários projetos e múltiplas interfaces.

Essas abordagens podem ser úteis, mas não são requisitos universais.

Uma aplicação com poucos endpoints e regras simples pode funcionar muito bem com uma estrutura enxuta, utilizando Controllers ou Minimal APIs, serviços bem delimitados e uma camada de persistência.

Já um sistema corporativo com muitos módulos, regras de negócio complexas, integrações externas e equipes independentes pode se beneficiar de uma separação arquitetural mais rigorosa.

A arquitetura deve resolver problemas concretos, como:

  • Acoplamento excessivo.

  • Dificuldade de testar regras de negócio.

  • Alterações que afetam módulos não relacionados.

  • Regras duplicadas.

  • Complexidade na integração entre componentes.

  • Dificuldade de evoluir o sistema.

Se uma abstração não melhora a clareza, a testabilidade ou a capacidade de evolução, talvez ela não seja necessária naquele momento.

O objetivo é construir uma API que seja simples de entender e suficientemente estruturada para acompanhar o crescimento do projeto.

Conclusão

Projetar uma API REST moderna com ASP.NET Core exige decisões que vão além da implementação dos endpoints.

A escolha entre Controllers e Minimal APIs, a separação de responsabilidades, a definição de contratos, a utilização correta dos códigos HTTP e a validação dos dados formam a base de uma interface confiável.

A partir daí, segurança, documentação OpenAPI, observabilidade, testes automatizados e operações assíncronas ajudam a preparar a aplicação para ambientes reais de produção.

Também é importante escolher ferramentas e padrões arquiteturais conforme as necessidades do projeto, evitando tanto a concentração excessiva de responsabilidades quanto a criação de camadas desnecessárias.

Uma API bem projetada não é necessariamente a que utiliza mais tecnologias ou possui a arquitetura mais elaborada. É aquela que oferece contratos previsíveis, protege os dados, pode ser testada e permite que a equipe implemente mudanças sem comprometer continuamente o funcionamento do sistema.

Para quem desenvolve com C# e .NET, dominar esses fundamentos é um passo importante para construir serviços mais seguros, sustentáveis e preparados para evoluir.