Boas práticas para APIs REST

Descubra as melhores práticas para APIs REST, explore a integração de middleware, DI e crie APIs robustas e escaláveis ​​com ASP.NET Core

O ASP.NET Core se tornou o framework preferido de muitos desenvolvedores devido à sua flexibilidade, desempenho e amplo ecossistema. Seja para criar microsserviços, aplicações web ou soluções corporativas, entender as melhores práticas para o design de APIs REST pode diminuir significativamente a manutenção e aumentar o desempenho do seu projeto. Neste artigo, mostrarei as técnicas e dicas que uso para criar APIs escaláveis ​​com facilidade.

Melhores práticas da API RESTful

Aderir aos princípios REST

REST (Representational State Transfer) é mais do que um estilo de programação, é um conjunto de princípios arquitetônicos que levam a APIs escaláveis ​​e sustentáveis.

Abaixo estão os quais considero mais importantes “ilustrando” com exemplos de código.

Api Sem Estado

Cada solicitação deve incluir todas as informações necessárias para executar. Por exemplo, usar tokens para autenticação evita manter sessões do lado do servidor.

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetOrder(int id)
    {
        var order = _orderService.GetOrderById(id);
        return order != null ? Ok(order) : NotFound();
    }
}

URIs baseados em recursos

Use URLs claras e lógicas para representar recursos. Por exemplo:

// URL: /api/products
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetProducts()
    {
        var products = _productService.GetAllProducts();
        return Ok(products);
    }
}

Uso de métodos HTTP padrão

Implemente operações CRUD usando os verbos HTTP apropriados:

// GET, POST, PUT, DELETE methods in the same controller
[ApiController]
[Route("api/[controller]")]
public class CustomersController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetCustomer(int id) 
    { 
        /* ... */ 
    }
    
    [HttpPost]
    public IActionResult CreateCustomer([FromBody] Customer customer) 
    { 
        /* ... */ 
    }
    
    [HttpPut("{id}")]
    public IActionResult UpdateCustomer(int id, [FromBody] Customer customer)
    { 
        /* ... */ 
    }
    
    [HttpDelete("{id}")]
    public IActionResult DeleteCustomer(int id) 
    { 
        /* ... */ 
    }
}

Implementar controle de versão

À medida que sua API evolui, o controle de versão impede incompatibilidad eentre alterações significativas. O ASP.NET Core oferece suporte a diversas estratégias de controle de versão. You can read more at API Versioning with ASP.NET and Swagger UI.

Controle de versão do Path de URL

// URL: /api/v1/products
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetProducts() { /* ... */ }
}

Controle de versão pela string de consulta

// URL: /api/products?api-version=1.0
services.AddApiVersioning(options =>
{
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ApiVersionReader = new QueryStringApiVersionReader("api-version");
});

Controle de versão de cabeçalho

// Use a custom header, e.g., "X-API-Version"
services.AddApiVersioning(options =>
{
    options.ApiVersionReader = new HeaderApiVersionReader("X-API-Version");
});

Garantir a segurança

Segurança É OBRIGATÓRIA para qualquer API! O ASP.NET Core oferece diversos recursos de segurança integrados, como:

Autenticação e Autorização

// Configure JWT authentication in Startup.cs
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = Configuration["Jwt:Issuer"],
                ValidAudience = Configuration["Jwt:Audience"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
            };
        });

Validação de dados

public class Product
{
    public int Id { get; set; }
    
    [Required]
    [StringLength(100)]
    public string Name { get; set; }
    
    [Range(0.01, double.MaxValue)]
    public decimal Price { get; set; }
}

Limitação de taxa

Considere soluções de middleware como AspNetCoreRateLimit para evitar abusos.

// In Startup.cs - Configure rate limiting options
services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimiting"));
services.AddInMemoryRateLimiting();
services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();

Otimização e Desempenho

O desempenho é fundamental para uma boa experiência do usuário. Práticas essenciais incluem cache, programação assíncrona (muito importante) e acesso eficiente aos dados.

Cache

Este é um exemplo simples usando um cache de memória, mas você pode usar um mecanismo mais sofisticado como o Redis.

// Using in-memory caching
public class ProductsController : ControllerBase
{
    private readonly IMemoryCache _cache;
    public ProductsController(IMemoryCache cache) => _cache = cache;
    
    [HttpGet]
    public IActionResult GetProducts()
    {
        var cacheKey = "productList";
        if (!_cache.TryGetValue(cacheKey, out List<Product> products))
        {
            products = _productService.GetAllProducts();
            var cacheEntryOptions = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(5));
            _cache.Set(cacheKey, products, cacheEntryOptions);
        }
        return Ok(products);
    }
}

Programação Assíncrona

Isso é OBRIGATÓRIO para cenários de alta carga porque libera o thread para o pool de threads enquanto aguarda qualquer E/S, como acesso ao banco de dados.

[HttpGet("{id}")]
public async Task<IActionResult> GetProductAsync(int id)
{
    var product = await _productService.GetProductByIdAsync(id);
    return product != null ? Ok(product) : NotFound();
}

Acesso eficiente a dados

O segredo aqui é usar AsNoTracking ao consultar dados que você não atualizará ou excluirá. Este método informa ao Microsoft Entity Framework (EF) para interromper o monitoramento da entidade.

// Using Entity Framework Core with async queries
public async Task<List<Product>> GetAllProductsAsync()
{
    return await _context.Products.AsNoTracking().ToListAsync();
}

Integração de middleware no ASP.NET Core

O middleware é essencial para o pipeline de processamento de solicitações do ASP.NET Core. Veja abaixo exemplos de middleware personalizado e integração de middleware de terceiros.

Construindo Middleware Personalizado

Um middleware personalizado pode ser usado para lidar com exceções, registros e outras ações. Por exemplo, um manipulador de exceções global.

public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;

    public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
    
    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception occurred.");
            context.Response.StatusCode = 500;
            await context.Response.WriteAsync("An unexpected error occurred. Please try again later.");
        }
    }
}

Registrando o middleware no Startup.cs:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseMiddleware<GlobalExceptionMiddleware>();
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

Integração de middleware de terceiros

O ASP.NET Core tem um rico ecossistema de middleware para tarefas como logs, CORS e compactação de resposta.

Registro de eventos (log) com Serilog

Abaixo tem um exemplo simples do uso do Serilog. Veja as postagns Enhance logging in .NET Core Web API e Set up Datalust Seq in a Docker Container para entender o Serilog com mais profundidade.

// In Program.cs
Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Day)
    .CreateLogger();

hostBuilder.UseSerilog();

Configuração CORS

// In Startup.cs, configure CORS policy
public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options =>
    {
        options.AddPolicy("AllowSpecificOrigin",
            builder => builder.WithOrigins("http://example.com")
                              .AllowAnyHeader()
                              .AllowAnyMethod());
    });
    services.AddControllers();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseCors("AllowSpecificOrigin");
    app.UseRouting();
    app.UseEndpoints(endpoints => endpoints.MapControllers());
}

Compressão de Resposta

// In Startup.cs, add response compression
public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseCompression(options =>
    {
        options.EnableForHttps = true;
    });
    services.AddControllers();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseResponseCompression();
    app.UseRouting();
    app.UseEndpoints(endpoints => endpoints.MapControllers());
}

Usando a injeção de dependência (DI)

O contêiner DI integrado do ASP.NET Core simplifica o gerenciamento de serviços, melhora a capacidade de testes e promove uma separação clara das responsabilidades das classes.

Por que usar a injeção de dependência?

O DI ajuda a reduzir o acoplamento rígido injetando dependências em vez de codificá-las. Isso resulta em um código mais sustentável e testável.

Registro de Serviços

Registre serviços no método ConfigureServices com tempos de vida apropriados:

public void ConfigureServices(IServiceCollection services)
{
    // Transient: New instance per use
    services.AddTransient<INotificationService, EmailNotificationService>();
    
    // Scoped: One instance per request
    services.AddScoped<IProductService, ProductService>();
    
    // Singleton: One instance for the entire application lifetime
    services.AddSingleton<ILoggingService, LoggingService>();
    
    services.AddControllers();
}

Injetando serviços em controladores

Após configurar o registro dos serviços, injete-os nos controladores por meio de injeção de construtor:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly ILoggingService _loggingService;
    
    public ProductsController(IProductService productService, ILoggingService loggingService)
    {
        _productService = productService;
        _loggingService = loggingService;
    }
    
    [HttpGet]
    public async Task<IActionResult> GetProducts()
    {
        _loggingService.Log("Fetching all products.");
        var products = await _productService.GetAllProductsAsync();
        return Ok(products);
    }
}

Testando com DI

Os testes unitários tornam-se mais fáceis quando as dependências podem ser simuladas (mocks). Por exemplo, usar o Framework Moq para testes

public class ProductsControllerTests
{
    [Fact]
    public async Task GetProducts_ReturnsOkResult_WithProductList()
    {
        // Arrange
        var mockService = new Mock<IProductService>();
        mockService.Setup(service => service.GetAllProductsAsync())
                   .ReturnsAsync(new List<Product> { new Product { Id = 1, Name = "Test Product", Price = 9.99M } });
        var mockLogger = new Mock<ILoggingService>();
        
        var controller = new ProductsController(mockService.Object, mockLogger.Object);
        
        // Act
        var result = await controller.GetProducts();
        
        // Assert
        var okResult = Assert.IsType<OkObjectResult>(result);
        var products = Assert.IsAssignableFrom<IEnumerable<Product>>(okResult.Value);
        Assert.Single(products);
    }
}

Práticas recomendadas adicionais para design de API escalável

Documentação e Testes

Documentação de API com Swagger

Integre o Swagger para gerar automaticamente documentação de API interativa.

// In Startup.cs - Configure Swagger services
public void ConfigureServices(IServiceCollection services)
{
    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
    });
    services.AddControllers();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    });
    app.UseRouting();
    app.UseEndpoints(endpoints => endpoints.MapControllers());
}

Testes automatizados

Além dos testes unitários, considere os testes de integração usando o TestServer na memória:

public class IntegrationTests
{
    [Fact]
    public async Task GetProducts_EndpointReturnsSuccess()
    {
        // Arrange
        var builder = WebApplication.CreateBuilder();
        builder.Services.AddControllers();
        var app = builder.Build();
        app.MapControllers();
        
        var client = app.GetTestClient();
        
        // Act
        var response = await client.GetAsync("/api/products");
        
        // Assert
        response.EnsureSuccessStatusCode();
    }
}

Monitoramento e Registro

O registro e o monitoramento robustos são cruciais para ambientes de produção

Usando o Application Insights

Leia mais sobre o Application Insights em learn.microsoft.com

// In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddApplicationInsightsTelemetry(Configuration["ApplicationInsights:InstrumentationKey"]);
    services.AddControllers();
}

Exemplo de Log estruturado

Isso é importante para serviços de log como SEQ

// In a service or middleware
_logger.LogInformation("User {UserId} performed action {Action} at {Time}", userId, action, DateTime.UtcNow);

Integração e Implantação Contínuas (CI/CD)

Automatize suas compilações e implantações com pipelines de CI/CD. Veja um exemplo de configuração YAML para GitHub Actions:

name: ASP.NET Core CI/CD

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Setup .NET
        uses: actions/setup-dotnet@v1
        with:
          dotnet-version: '6.0.x'
      - name: Restore dependencies
        run: dotnet restore
      - name: Build
        run: dotnet build --no-restore
      - name: Test
        run: dotnet test --no-build --verbosity normal

Conclusão

Construir APIs RESTful robustas com ASP.NET Core requer atenção aos princípios de design, integração eficaz de middleware e o uso da injeção de dependências para um código modular e testável. Seguindo as melhores práticas para APIs REST discutidas, aderindo às convenções RESTful, integrando middleware de forma eficiente e utilizando DI, você pode criar APIs escaláveis, sustentáveis ​​e de alto desempenho. Além disso, incorporar documentação, testes automatizados e monitoramento garante que sua API permaneça confiável em produção.

Estes exemplos de código expandidos fornecem insights práticos sobre como cada componente funciona no ecossistema ASP.NET Core. Seja iniciando um novo projeto ou aprimorando um existente, estas estratégias ajudarão você a criar APIs que evoluem com as necessidades do seu negócio, mantendo a excelência em desempenho e segurança.

Até mais!