DEV Community

Anaya Upadhyay
Anaya Upadhyay

Posted on • Originally published at instagram.com

Minimal API vs Controllers in ASP.NET Core: When Each One Actually Wins

Somewhere around .NET 6, the Minimal API question stopped being academic. Teams actually started shipping with it. And then some of those teams quietly started adding Controllers back six months later.

Both choices can be correct. The problem is that the wrong framing "which one is simpler?" or "which one is more modern?" lands you in the wrong answer. Here is the framing that works.


The real question

Both Minimal API and Controllers ship working APIs. What they optimize for is completely different, and ignoring that difference is how you end up refactoring.

Start with three questions:

How complex is your routing logic?
A handful of endpoints vs. a full domain model with dozens of grouped resources. That gap matters.

Do you need the full action filter pipeline?
Controller action filters and DI-bound per-action behavior make unit testing dramatically easier at scale. Minimal API has endpoint filters, they work, but they are not the same abstraction.

Who else touches this codebase?
Controllers have 15 years of conventions. A developer who has never seen your project knows where to look. Minimal API is learnable fast, but it is not pre-loaded.


When Minimal API wins

Microservices with a narrow surface area

One service. A handful of well-defined endpoints. You do not need to carry the full MVC stack for a payment webhook receiver or an internal reporting service. MapGet, MapPost, done.

Internal tooling and prototypes

You want working code in minutes. Controllers impose ceremony that has no return on a one-team service nobody else integrates with. The shorter the expected lifespan, the higher the ceremony cost.

HTTP-native patterns

Webhooks, health checks, simple CRUD. Request in, response out. The Minimal API pipeline maps directly to those problems. No inheritance, no attributes, no discovery conventions.

Small teams that own the full stack

When the team is small and the scope is bounded, conventions are mostly overhead. Minimal API gives that team agency over their own structure without fighting a framework.


What a clean Minimal API endpoint looks like

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /products/{id}
app.MapGet("/products/{id}",
    async (int id, IProductRepository repo) =>
{
    var product = await repo.GetByIdAsync(id);
    return product is null
        ? Results.NotFound()
        : Results.Ok(product);
});

app.Run();
Enter fullscreen mode Exit fullscreen mode

The whole thing fits. DI works. IProductRepository is resolved from the container automatically. Results helpers cover the common response patterns without IActionResult boilerplate.


When Controllers win

Large domain with many grouped resources

ProductsController, OrdersController, UsersController. Controllers namespace your API implicitly and make routing intentions explicit without attribute gymnastics. At 40+ endpoints the ergonomics flip decisively.

Action filters are doing real work

Audit logging, model validation attributes, custom authorization policies scoped per action. The filter pipeline on Controllers is composable in ways that Minimal API endpoint filters currently are not. If your cross-cutting concerns are complex, go where they fit cleanly.

Multiple developers sharing the codebase

This one is underrated. Controllers have 15 years of conventions behind them. A new developer knows where to look without asking anyone. That knowledge is worth something, especially as the team grows.

Versioned APIs with complex binding

Route groups help in Minimal API, but versioned controllers with [ApiVersion] and strongly-typed model binding still carry less ceremony in practice. If you are versioning three major API versions in production, Controllers are the path of least resistance.


The same endpoint as a Controller

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductRepository _repo;

    public ProductsController(IProductRepository repo)
        => _repo = repo;

    // GET api/products/{id}
    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(int id)
    {
        var product = await _repo.GetByIdAsync(id);
        return product is null
            ? NotFound()
            : Ok(product);
    }
}
Enter fullscreen mode Exit fullscreen mode

More structure. More surface area. Worth it when that structure is carrying load.


The side-by-side

Dimension Minimal API Controllers
Boilerplate Minimal Moderate
Testability Good with DI Excellent
Action filters Endpoint filters only Full filter pipeline
Routing Lambda, explicit Attribute + convention
Team ramp-up Fast for new .NET devs Familiar to most devs
Scale target Micro to mid Mid to large domain
Swagger / OAS Full support Full support

Neither row is a clear winner on every dimension. That is the point.


The hybrid pattern. You do not always have to pick

This is the one that most people miss. You can use both in one solution:

// Program.cs
var app = builder.Build();

// Minimal API: health + lightweight probes
app.MapGet("/health", () => Results.Ok());
app.MapGet("/version", () => AppInfo.Version);

// Controllers: complex domain resources
app.MapControllers();

app.Run();
Enter fullscreen mode Exit fullscreen mode

MapControllers() and MapGet() compose cleanly. Route groups and controller routing do not conflict. Use Minimal API for the surface area where it fits, Controllers where the domain needs structure.

This is not a workaround. It is a supported, intended usage pattern.


The decision checklist

Save this for the next project kickoff:

  • New microservice or internal tool → Minimal API
  • Existing codebase already on Controllers → Controllers
  • Need action filter pipeline → Controllers
  • Prototyping or validating an idea → Minimal API
  • Large domain, many grouped routes → Controllers
  • Mixed concerns in one solution → Hybrid

What actually goes wrong

The most common failure mode is not choosing the wrong approach. It is choosing one approach and never questioning it as the project grows. A project that starts as a three-endpoint Minimal API and quietly becomes a 60-endpoint domain model has a debt problem.

Pay attention to the signals:

  • Duplicated per-endpoint middleware logic → Controllers
  • Handler functions growing past 30 lines → consider Controllers
  • Difficulty testing endpoints in isolation → Controllers
  • Ceremony cost outweighing value → reconsider Minimal API The framework does not enforce a migration path. You have to notice it yourself.

The full carousel version of this breakdown (with code examples for each scenario) is on Instagram at @thesharpfuture.

Top comments (0)