Minimal APIs vs Controllers ā When to Choose Which
Honest comparison of Minimal APIs and MVC Controllers: performance, testability, organization at scale, team familiarity, and the framework signals for choosing one over the other in new and existing .NET projects.
The Core Difference
MVC Controllers:
Endpoint declared as class method
Route: [HttpGet("{id}")] attribute
Auth: [Authorize(Policy = "...")] attribute
DI: constructor injection
Filters: [ServiceFilter(typeof(MyFilter))] attribute
Framework: action invoker pipeline, ModelState, IActionResult
Minimal APIs:
Endpoint declared as delegate (lambda or method reference)
Route: app.MapGet("{id}", handler)
Auth: .RequireAuthorization("...")
DI: parameter injection
Filters: .AddEndpointFilter()
Framework: lighter pipeline, IResult Performance
Minimal API overhead per request: ~10-20% less than controllers
Why: No action invoker, no ControllerBase allocation, lighter pipeline
In practice:
If your API processes 1,000 req/sec:
20% savings = 200 req/sec improvement ā significant
If your API processes 10 req/sec:
20% savings = 2 req/sec improvement ā irrelevant
Recommendation: choose Minimal API for high-throughput APIs where
every millisecond matters. Choose controllers when productivity
and familiarity matter more than peak throughput.Testability
// Controller action test ā requires full controller instantiation
public class PatientControllerTests
{
[Fact]
public async Task GetPatient_should_return_ok()
{
var controller = new PatientController(
Substitute.For<IGetPatientHandler>());
controller.ControllerContext = new ControllerContext
{
HttpContext = new DefaultHttpContext()
};
var result = await controller.GetPatient(Guid.NewGuid(), CancellationToken.None);
result.Should().BeOfType<OkObjectResult>();
}
}
// Minimal API handler test ā just call the delegate
[Fact]
public async Task GetPatient_handler_should_return_not_found_for_unknown_id()
{
var handler = Substitute.For<IGetPatientHandler>();
handler.Handle(Arg.Any<GetPatientQuery>(), Arg.Any<CancellationToken>())
.Returns(Result.Failure<PatientDto>(PatientErrors.NotFound));
// Call the handler function directly ā no HTTP context needed
var result = await PatientEndpoints.GetPatient(
Guid.NewGuid(), handler, CancellationToken.None);
result.Should().BeOfType<NotFound>();
}
// Both work ā WebApplicationFactory tests are equivalent for bothOrganization at Scale
Minimal APIs ā organizing 50+ endpoints:
ā MapGroup for shared prefix and auth
ā Extension methods (MapPatients, MapPrescriptions) split across files
ā No built-in class-level organization
ā Requires discipline to avoid Program.cs bloat
Controllers ā organizing 50+ endpoints:
ā Each controller is a class ā natural grouping
ā [ApiController] + [Route("api/[controller]")] gives consistent URLs
ā Familiar to every .NET developer
ā Heavier ā ControllerBase, ModelState, ActionResult hierarchy
ā Attributes scattered through the fileSide-by-Side Comparison
Feature Minimal API Controller
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Performance Faster Slightly slower
Boilerplate Less More (class, constructor)
Discoverability Requires discipline Easy (class browser)
Routing Fluent Attributes
Auth .RequireAuthorization [Authorize]
Filters .AddEndpointFilter [ServiceFilter]
Team onboarding Newer pattern Universal familiarity
Complex binding Manual ModelState auto-binding
File organization Extension methods Classes
OpenAPI WithOpenApi() Automatic from attributesDecision Framework
Choose Minimal APIs when:
ā Performance is a top concern (microservices, high-throughput)
ā Starting a new project with a team familiar with Minimal APIs
ā Building a small-to-medium API (under 50 endpoints) where flat organization works
ā You want a clean architecture without MVC's weight
Choose Controllers when:
ā Team is large with developers of varying .NET experience
ā Existing MVC codebase ā consistency matters more than performance
ā Complex model binding with ModelState is heavily used
ā Large number of endpoints where class-based organization is clearer
ā You are adding a few endpoints to an existing MVC project
Both are fine:
Mixing both in one project is supported and common during migration
Controllers for legacy endpoints; Minimal APIs for new featuresMigration Pattern
// Migrate a controller endpoint to Minimal API incrementally
// Step 1: Controller still exists
[ApiController]
[Route("api/[controller]")]
public class PatientsController : ControllerBase
{
[HttpGet("{id}")]
public async Task<IActionResult> Get(Guid id, [FromServices] IGetPatientHandler handler)
=> (await handler.Handle(new GetPatientQuery(id), HttpContext.RequestAborted))
.Match<IActionResult>(dto => Ok(dto), err => err.ToActionResult());
}
// Step 2: Add equivalent Minimal API (test both work)
app.MapGet("/api/patients/{id:guid}", async (
Guid id, IGetPatientHandler handler, CancellationToken ct) =>
{
var result = await handler.Handle(new GetPatientQuery(id), ct);
return result.Match(Results.Ok, err => err.ToProblemResult());
}).RequireAuthorization();
// Step 3: Remove the controller action
// Step 4: Remove the controller class when all endpoints are migratedRed Flag / Green Answer
Red Flag: "We are building a new project and choosing between controllers and Minimal APIs based on what we find in online tutorials."
Choose based on team context, not tutorials. If the team knows controllers well and the API is growing large, controllers provide familiar structure. If you are building a high-performance service or the team is comfortable with the pattern, Minimal APIs reduce overhead.
Green Answer:
Evaluate: team familiarity, API size, performance requirements, existing codebase consistency. Document the decision. Both are production-quality choices ā the criteria matter, not a universal "Minimal APIs are better."
Key Takeaway
Minimal APIs are faster and lighter with less boilerplate. Controllers provide class-based organization familiar to all .NET developers. For new projects, Minimal APIs with
MapGroupand extension methods is the direction Microsoft is investing in. For existing codebases or large teams, controllers remain a solid choice. The difference matters at scale ā choose based on your actual context, not framework hype.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.