.NET ASP.NET Core Minimal APIs Web API Arquitetura

Minimal APIs vs Controllers no .NET: comparação honesta para escolher o certo

Análise técnica definitiva entre Minimal APIs e Controllers MVC no .NET: performance, organização, testabilidade.

N
Neryx Digital Architects
29 de dezembro de 2025
11 min de leitura
190 profissionais leram
Categoria: Arquitetura Público: Times de engenharia e produto Etapa: Aprendizado

A chegada das Minimal APIs no .NET 6 gerou um falso dilema: "Devo migrar tudo para Minimal APIs?". A resposta real é que ambas as abordagens têm casos de uso legítimos — e misturá-las no mesmo projeto é perfeitamente válido. Este artigo faz a comparação técnica honesta para você decidir com critério.

O que são — sem enrolação

Controllers MVC existem desde o ASP.NET Core 1.0. São classes que herdam de ControllerBase, decoradas com atributos ([ApiController], [Route], [HttpGet]), com model binding automático, filtros de ação, validação de ModelState integrada e muito scaffolding do framework.

Minimal APIs são lambdas ou métodos registrados diretamente no pipeline de roteamento, sem infraestrutura de controller. O máximo de conveniência com o mínimo de abstração.

// Controller — abordagem clássica
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id:guid}")]
    public async Task<ActionResult<ProductDto>> GetById(Guid id,
        [FromServices] IProductService service)
    {
        var product = await service.GetByIdAsync(id);
        if (product is null) return NotFound();
        return Ok(product);
    }
}

// Minimal API — abordagem moderna
app.MapGet("/api/products/{id:guid}", async (Guid id, IProductService service) =>
{
    var product = await service.GetByIdAsync(id);
    return product is null ? Results.NotFound() : Results.Ok(product);
});

Performance: a diferença real

Minimal APIs são mais rápidas. Isso é um fato mensurável nos benchmarks do TechEmpower. A razão: eliminam camadas intermediárias do pipeline MVC (filtros de ação, model binders via reflection, ActionResult boxing).

Mas o ganho de performance é relevante para você? Apenas se:

  • Sua API processa centenas de milhares de requisições por segundo
  • Você tem endpoints de hot path com latência < 1ms como requisito
  • Está construindo algo como um proxy ou gateway de alta throughput

Para APIs de negócio comuns (CRUD, integrações, SaaS), o gargalo estará no banco de dados ou em chamadas externas — não no framework de roteamento. A diferença de performance entre Controllers e Minimal APIs é irrelevante nesses cenários.

Organização de código: o ponto mais crítico

O maior argumento contra Minimal APIs não é técnico — é organizacional. Um Program.cs com 50 endpoints é insuportável.

Solução: extensões de rota por feature (o padrão que funciona)

// src/Features/Products/ProductsEndpoints.cs
public static class ProductsEndpoints
{
    public static IEndpointRouteBuilder MapProductsEndpoints(
        this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/products")
            .WithTags("Products")
            .RequireAuthorization()
            .WithOpenApi();

        group.MapGet("/", GetAll);
        group.MapGet("/{id:guid}", GetById);
        group.MapPost("/", Create);
        group.MapPut("/{id:guid}", Update);
        group.MapDelete("/{id:guid}", Delete);

        return app;
    }

    private static async Task<IResult> GetAll(
        [AsParameters] PaginationParams pagination,
        IProductService service,
        CancellationToken ct)
    {
        var result = await service.GetAllAsync(pagination, ct);
        return Results.Ok(result);
    }

    private static async Task<IResult> GetById(
        Guid id,
        IProductService service,
        CancellationToken ct)
    {
        var product = await service.GetByIdAsync(id, ct);
        return product is null
            ? Results.NotFound(new { Message = $"Produto {id} não encontrado" })
            : Results.Ok(product);
    }

    private static async Task<IResult> Create(
        CreateProductRequest request,
        IValidator<CreateProductRequest> validator,
        IProductService service,
        CancellationToken ct)
    {
        var validation = await validator.ValidateAsync(request, ct);
        if (!validation.IsValid)
            return Results.ValidationProblem(validation.ToDictionary());

        var id = await service.CreateAsync(request, ct);
        return Results.CreatedAtRoute("GetProductById", new { id }, new { id });
    }

    private static async Task<IResult> Update(
        Guid id,
        UpdateProductRequest request,
        IProductService service,
        CancellationToken ct)
    {
        var success = await service.UpdateAsync(id, request, ct);
        return success ? Results.NoContent() : Results.NotFound();
    }

    private static async Task<IResult> Delete(
        Guid id,
        IProductService service,
        CancellationToken ct)
    {
        var success = await service.DeleteAsync(id, ct);
        return success ? Results.NoContent() : Results.NotFound();
    }
}

// Program.cs — limpo e simples
app.MapProductsEndpoints();
app.MapOrdersEndpoints();
app.MapCustomersEndpoints();

Validação: o ponto de atrito das Minimal APIs

Controllers com [ApiController] têm validação de ModelState automática. Minimal APIs não. Você tem três opções:

Opção 1: FluentValidation via endpoint filter (recomendado)

// Filtro de validação genérico
public class ValidationFilter<T> : IEndpointFilter where T : class
{
    private readonly IValidator<T> _validator;

    public ValidationFilter(IValidator<T> validator)
        => _validator = validator;

    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        // Encontra o argumento do tipo T na lista de argumentos do endpoint
        var argument = context.Arguments
            .OfType<T>()
            .FirstOrDefault();

        if (argument is not null)
        {
            var result = await _validator.ValidateAsync(argument);
            if (!result.IsValid)
                return Results.ValidationProblem(result.ToDictionary());
        }

        return await next(context);
    }
}

// Extensão para facilitar o uso
public static class EndpointRouteBuilderExtensions
{
    public static RouteHandlerBuilder WithValidation<T>(
        this RouteHandlerBuilder builder) where T : class
    {
        return builder.AddEndpointFilter<ValidationFilter<T>>();
    }
}

// Uso no endpoint
group.MapPost("/", Create)
    .WithValidation<CreateProductRequest>();

Opção 2: .NET 8 — Microsoft.AspNetCore.Http.Validations (nativo)

// No .NET 8+, você pode usar DataAnnotations de forma nativa
app.MapPost("/api/products", async (
    [FromBody] CreateProductRequest request,
    IProductService service) =>
{
    // Sem setup adicional — .NET 8 valida automaticamente via IEndpointMetadataProvider
    var id = await service.CreateAsync(request);
    return Results.Created($"/api/products/{id}", new { id });
})
.WithParameterValidation(); // Extensão que habilita validação automática

Testabilidade: empate técnico com abordagens diferentes

Ambas as abordagens são igualmente testáveis via WebApplicationFactory. A diferença está nos testes unitários:

// Teste de integração — igual para ambas
public class ProductsApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public ProductsApiTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetById_ExistingProduct_Returns200()
    {
        var response = await _client.GetAsync("/api/products/valid-id");
        response.StatusCode.Should().Be(HttpStatusCode.OK);
    }
}

// Teste unitário de Minimal API — teste o handler direto
[Fact]
public async Task GetById_NonExistentProduct_ReturnsNotFound()
{
    var serviceMock = new Mock<IProductService>();
    serviceMock.Setup(s => s.GetByIdAsync(It.IsAny<Guid>(), default))
        .ReturnsAsync((ProductDto?)null);

    // Invoca o handler diretamente (não precisa do HTTP stack completo)
    var result = await ProductsEndpoints.GetById(Guid.NewGuid(), serviceMock.Object, default);

    result.Should().BeOfType<NotFound>();
}

Filtros e middleware: Controllers ganham aqui

Controllers têm um sistema de filtros maduro: IActionFilter, IExceptionFilter, IAuthorizationFilter, com ordem de execução previsível e suporte a DI. Minimal APIs têm endpoint filters, mas são mais limitados.

// Controller — filtro de ação global
public class LogActionFilter : IActionFilter
{
    private readonly ILogger<LogActionFilter> _logger;
    private Stopwatch _stopwatch;

    public LogActionFilter(ILogger<LogActionFilter> logger)
        => _logger = logger;

    public void OnActionExecuting(ActionExecutingContext context)
    {
        _stopwatch = Stopwatch.StartNew();
        _logger.LogInformation(">> {Action}", context.ActionDescriptor.DisplayName);
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        _stopwatch.Stop();
        _logger.LogInformation("<< {Action} em {Ms}ms",
            context.ActionDescriptor.DisplayName,
            _stopwatch.ElapsedMilliseconds);
    }
}

// Minimal API — endpoint filter (funciona, mas mais verboso para casos complexos)
app.MapGet("/api/products", GetAll)
    .AddEndpointFilter(async (context, next) =>
    {
        var sw = Stopwatch.StartNew();
        var result = await next(context);
        sw.Stop();
        // ... log
        return result;
    });

Quando usar cada um — a decisão prática

Critério Minimal APIs Controllers
Performance extrema / alta throughput ✅ Melhor ⚠️ Overhead do MVC
APIs de negócio com muitas regras ⚠️ Requer estrutura manual ✅ Estrutura pronta
Time que conhece ASP.NET MVC ⚠️ Curva de aprendizado ✅ Familiar
Microserviços pequenos e focados ✅ Ideal ⚠️ Overhead desnecessário
CRUD simples, poucas regras ✅ Conciso ⚠️ Muito boilerplate
Filtros e cross-cutting concerns complexos ⚠️ Mais trabalhoso ✅ Sistema de filtros maduro
OpenAPI / Swagger automático ✅ .NET 9 melhorou muito ✅ Suporte completo
Versioning de API ⚠️ Suporte básico ✅ Maduro com Asp.Versioning

A abordagem híbrida — a melhor das duas

Projetos maiores em .NET frequentemente se beneficiam de usar as duas abordagens no mesmo projeto:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Adiciona suporte a ambas
builder.Services.AddControllers();           // Para endpoints complexos
// Minimal APIs não precisam de registro aqui

var app = builder.Build();

// Controllers — rotas complexas com muitos filtros, versionamento, etc.
app.MapControllers();

// Minimal APIs — endpoints simples, webhooks, health, probes
app.MapGet("/health", () => Results.Ok(new { Status = "Healthy" }));
app.MapGet("/metrics", async (IMetricsService metrics) =>
    Results.Ok(await metrics.GetSnapshotAsync()));

// Feature-based Minimal APIs — microserviços internos
app.MapNotificationsEndpoints();  // endpoints simples de notificação
app.MapWebhooksEndpoints();       // recebe e repassa webhooks externos

Conclusão objetiva

Não existe "melhor" universal. Existe o que resolve seu problema com o menor custo de manutenção:

  • Novo microserviço pequeno e focado? Minimal APIs com extensões por feature.
  • API de negócio grande com time estabelecido em .NET MVC? Controllers — não mude o que funciona.
  • Projeto legado evoluindo para .NET 8? Mantenha Controllers, adicione Minimal APIs para novos endpoints simples.
  • Alta throughput / proxy / gateway? Minimal APIs sem hesitar.

A escolha da abordagem importa menos do que a consistência dentro do projeto. Um time dividido entre as duas abordagens sem convenção definida é o pior cenário possível.


Precisa definir a arquitetura certa para sua API .NET? Na Neryx fazemos essa análise como parte da consultoria técnica. Fale com a gente.

Precisa desenhar a próxima fase com menos retrabalho?

Fazemos discovery técnico para mapear riscos, arquitetura-alvo e sequência de execução antes de investir pesado.

Solicitar Discovery

Newsletter

Receba artigos como este no seu e-mail

Conteúdo técnico sobre arquitetura de software, .NET, IA e gestão de produto. Sem spam.