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.