DevgainsDevgainsDevgains
All articles

Minimal APIs vs Controllers in ASP.NET Core: Which to Use

·11 min read·Updated Jul 24, 2026

Minimal APIs vs Controllers is the first architectural decision you make when you start a new ASP.NET Core service, and the two styles have quietly swapped places over the last few releases. Controllers are the mature, convention-heavy MVC approach that has shipped since ASP.NET Core 1.0; Minimal APIs are the lightweight, top-level route handlers introduced in .NET 6 and steadily grown into a first-class option. Both compile to the same endpoint routing under the hood, both use the same dependency injection container, and both can produce the exact same HTTP responses. This guide explains how each one works, where they genuinely differ, and how to pick the right one without regretting it six months into a project.

This is a supporting page in the Devgains .NET cluster. It assumes you are comfortable with async/await in C# and how services are wired up with dependency injection in ASP.NET Core, since both styles resolve their dependencies from the same container.

Quick answer: Minimal APIs vs Controllers

  • Use Minimal APIs for small-to-medium services, microservices, and focused HTTP endpoints where you want the least ceremony and the fastest cold start. Routes are declared as lambdas mapped directly onto the app.
  • Use Controllers for large applications with many endpoints that benefit from convention-based organization, attribute routing across dozens of actions, model binding edge cases, and the full MVC filter pipeline you already know.
  • They share the same foundation. Endpoint routing, DI, middleware, and IResult/ActionResult responses are common to both, so the choice is about ergonomics and scale, not capability.
  • You can mix them in one project. Minimal APIs and Controllers coexist happily, so you are not locked in.

Start with Minimal APIs unless you have a concrete reason to reach for Controllers — that reason usually appears as an app grows, not on day one.

Why the choice matters

The style you choose shapes how every future endpoint is written, tested, and reviewed. Pick Minimal APIs and your team writes terse, self-contained handlers that are easy to read in isolation but can sprawl if you dump a hundred of them into one file. Pick Controllers and you inherit a predictable folder structure and a rich filter pipeline, at the cost of more boilerplate per action and a heavier request path. Neither is wrong, but switching later means rewriting routing, tests, and sometimes your whole mental model of where request logic lives. Because both run on the same ASP.NET Core service that you deploy as a container, the runtime cost difference is small — the real cost is developer time and consistency.

What are Minimal APIs?

Minimal APIs are route handlers you register directly on the WebApplication with MapGet, MapPost, MapPut, and friends. There is no controller class, no base type, and no attribute routing — the route template and the handler live side by side. Parameters are bound from the route, query string, body, and DI container by convention, and the return value is turned into an HTTP response.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IOrderService, OrderService>();
 
var app = builder.Build();
 
// A GET endpoint: id comes from the route, orders is injected from DI.
app.MapGet("/orders/{id:int}", async (int id, IOrderService orders) =>
{
    var order = await orders.FindAsync(id);
    return order is null ? Results.NotFound() : Results.Ok(order);
});
 
app.Run();

That single MapGet call does what a controller action, an attribute route, and model binding do together — in five lines. The {id:int} route constraint, the injected IOrderService, and the Results.NotFound()/Results.Ok() responses are all first-class. As the app grows you group related routes with MapGroup("/orders") and move handlers into separate methods or classes, so "minimal" does not have to mean "everything in Program.cs."

What are Controllers?

Controllers are classes that derive from ControllerBase (for APIs) or Controller (for views), with public methods called actions that handle requests. Routing is declared with attributes, and the MVC framework discovers controllers by convention at startup.

[ApiController]
[Route("orders")]
public class OrdersController : ControllerBase
{
    private readonly IOrderService _orders;
 
    // Dependencies are injected through the constructor.
    public OrdersController(IOrderService orders) => _orders = orders;
 
    [HttpGet("{id:int}")]
    public async Task<IActionResult> GetById(int id)
    {
        var order = await _orders.FindAsync(id);
        return order is null ? NotFound() : Ok(order);
    }
}

The [ApiController] attribute opts into helpful conventions — automatic 400 responses for invalid models, binding source inference, and ProblemDetails error bodies. Constructor injection pulls in services, and every action shares that controller's dependencies. For an app with dozens of related endpoints, this structure keeps things organized: one controller per resource, one action per operation, all discoverable by convention.

Architecture: the same routing underneath

The most important thing to understand is that both styles compile down to the same endpoint routing system. When a request arrives, ASP.NET Core matches it against a route table, selects an endpoint, runs the middleware pipeline, and invokes your handler — whether that handler is a lambda or a controller action. Dependency injection, the middleware pipeline, authentication, and response writing are identical. Controllers add an extra layer on top: the MVC action invoker and its filter pipeline (authorization, resource, action, exception, and result filters). Minimal APIs skip that layer and use lighter-weight endpoint filters instead.

That shared foundation is why the decision is rarely about what is possible. Both can do versioning, authentication, OpenAPI/Swagger generation, and model validation. The difference is how much framework machinery sits between the request and your code, and how that machinery is configured.

Step-by-step: adding validation to each

A common worry is that Minimal APIs lack the filter pipeline. Here is the same cross-cutting concern — validating input — in both styles.

Controllers lean on the [ApiController] convention plus data annotations:

public record CreateOrder([Required] string Sku, [Range(1, 100)] int Quantity);
 
[HttpPost]
public async Task<IActionResult> Create(CreateOrder input)
{
    // [ApiController] auto-returns 400 with ProblemDetails if the model is invalid,
    // so this method only runs on valid input.
    var id = await _orders.CreateAsync(input.Sku, input.Quantity);
    return CreatedAtAction(nameof(GetById), new { id }, null);
}

Minimal APIs use an endpoint filter to run the same check before the handler:

app.MapPost("/orders", async (CreateOrder input, IOrderService orders) =>
{
    var id = await orders.CreateAsync(input.Sku, input.Quantity);
    return Results.Created($"/orders/{id}", null);
})
.AddEndpointFilter(async (ctx, next) =>
{
    var input = ctx.GetArgument<CreateOrder>(0);
    if (string.IsNullOrWhiteSpace(input.Sku) || input.Quantity is < 1 or > 100)
        return Results.ValidationProblem(new Dictionary<string, string[]>
        {
            ["input"] = ["Sku is required and Quantity must be 1–100."]
        });
    return await next(ctx);   // run the handler only if validation passes
});

Both return an RFC 7807 ProblemDetails body on failure. The controller version is more concise for annotation-based rules; the Minimal API version is explicit and easy to unit test as a plain function. For richer validation, many teams add FluentValidation to either style.

Minimal APIs vs Controllers comparison

DimensionMinimal APIsControllers
BoilerplateVery low — route + lambdaHigher — class, attributes, actions
RoutingInline MapGet/MapPost, MapGroupAttribute routing ([Route], [HttpGet])
Dependency injectionHandler parametersConstructor injection
Cross-cutting concernsEndpoint filtersFull MVC filter pipeline
Startup / cold startSlightly faster, less reflectionSlightly slower (controller discovery)
Organization at scaleManual — use MapGroup and filesConvention-based, familiar folders
Model bindingConvention + explicit [FromBody] etc.Rich binding, value providers
Best forMicroservices, focused APIs, edge functionsLarge apps, many endpoints, MVC teams

When should you use Minimal APIs vs Controllers?

  • Choose Minimal APIs when the service is small or you are building a microservice, when cold start matters (serverless, containers that scale to zero), or when you want request logic to read top-to-bottom without hunting through controller classes.
  • Choose Controllers when the application is large with many related endpoints, when your team already thinks in MVC conventions, when you rely on the full filter pipeline, or when you need model-binding features that Minimal APIs expose less directly.
  • Choose either for typical CRUD APIs — both are production-ready. Consistency within a codebase matters more than the marginal differences, so pick one as the default and only mix when a specific endpoint clearly benefits.

Best practices

  • Pick a default and stay consistent. A codebase that mixes both without a rule is harder to navigate than one that commits to a style.
  • Group Minimal API routes with MapGroup. Share a prefix, auth policy, and filters across related endpoints instead of repeating them.
  • Move handlers out of Program.cs. Once you have more than a handful of routes, put handlers in static methods or dedicated classes so the file stays readable.
  • Await your async work. Both styles are async-first; return Task/Task<IResult> and await I/O so request threads are freed — see async/await in C#.
  • Register services with the right lifetime. Handlers and actions both resolve from the same container, so the service-lifetime rules apply identically.
  • Generate OpenAPI for both. Add WithOpenApi()/WithName() on Minimal API routes so Swagger documents them as clearly as controller actions.

Common mistakes

  • Dumping every route into Program.cs. Minimal does not mean unstructured — a 600-line startup file is a design smell, not a feature.
  • Rewriting an MVC app to Minimal APIs "for performance." The runtime gain is small; you rarely earn back the rewrite cost. Reserve Minimal APIs for new services.
  • Forgetting [ApiController] on API controllers. Without it you lose automatic 400 responses and binding-source inference, and start hand-writing validation.
  • Assuming Minimal APIs can't do filters. Endpoint filters and MapGroup-level filters cover most cross-cutting needs.
  • Mixing styles at random. Two ways to write the same endpoint in one repo doubles the surface a new contributor has to learn.

Takeaways

  • Minimal APIs and Controllers run on the same endpoint routing, DI, and middleware — the choice is about ergonomics and scale, not capability.
  • Minimal APIs win on low boilerplate and cold start; Controllers win on convention-based organization and the mature MVC filter pipeline.
  • Cross-cutting concerns like validation exist in both: [ApiController] conventions for Controllers, endpoint filters for Minimal APIs.
  • Default to Minimal APIs for new small-to-medium services; reach for Controllers as an app grows large and convention pays off.
  • Consistency beats cleverness — commit to one style per codebase and mix only with a reason.

FAQ

What is the difference between Minimal APIs and Controllers in ASP.NET Core? Minimal APIs register route handlers as lambdas directly on the app (app.MapGet(...)) with almost no boilerplate, while Controllers are classes with action methods and attribute routing. Both use the same underlying endpoint routing, dependency injection, and middleware, so they can produce identical responses.

Are Minimal APIs faster than Controllers? Slightly, mainly at startup and cold start because they skip controller discovery and the MVC action invoker. For steady-state throughput the difference is small; do not rewrite a working controller app chasing it.

Can I use Minimal APIs and Controllers together? Yes. Call both builder.Services.AddControllers() / app.MapControllers() and your app.MapGet handlers in the same project. They coexist without conflict, which makes incremental adoption easy.

Do Minimal APIs support dependency injection? Yes. Any handler parameter that matches a registered service is injected from the same container Controllers use, following the same Singleton/Scoped/Transient lifetime rules.

Should I use Minimal APIs for a large application? You can, but you must impose your own structure with MapGroup and separate handler files. Controllers give you that organization by convention, which many large teams prefer.

Conclusion

Minimal APIs vs Controllers is less a battle than a spectrum: the same ASP.NET Core routing engine sits underneath both, so you are really choosing how much ceremony you want between an HTTP request and your C#. New services benefit from the low-friction, fast-starting Minimal API style; large, endpoint-heavy applications often prefer the convention and filter pipeline that Controllers provide. Whichever you pick, register services with the right lifetimes, keep handlers small and testable, and be consistent. From here the .NET cluster goes deeper into building fast services — browse the full .NET category and continue with the ASP.NET Core middleware pipeline that both styles share, dependency injection lifetimes, and EF Core performance.

References

11 min read

Read next