Minimal API or controllers in ASP.NET Core with .NET 10: which one?

A software house in Bergamo starts a new logistics project. Twenty endpoints in month one, eighty by month six, maybe three hundred in two years. The project manager asks a simple question: Minimal API or controllers? The senior developer says "controllers, we have always done it that way". The newcomer has seen a video where a whole API fits in twelve lines and wants Minimal APIs. The debate lasts three meetings and ends with a majority vote nobody remembers six months later.
This guide is meant to close that debate in one meeting. In ASP.NET Core with .NET 10, Minimal APIs and controllers produce the same API with the same real-world performance: the choice is about code organization, the team that will maintain it and the product being built. Below you find what differs under the hood, the same endpoint in both styles, the true weight of performance, how to keep a minimal API tidy as it grows, where validation and filters live, how to test, how to migrate and a rule to decide. If data access is your next doubt, read Dapper or Entity Framework Core too.
Minimal API or controllers: which one for a new .NET 10 project?
For a new .NET 10 project, Minimal APIs are the default choice when the API logic is simple and the team is small; controllers remain the better choice when teams are large, filters are many or existing code is already built on them. Performance does not decide. Organization and habits do.
A Minimal API declares HTTP endpoints directly at application startup, one function per route. A controller is a class that groups several actions, each tied to a verb and a path through attributes. Both give the same result: an HTTP request comes in, a response goes out, and authentication, validation and data access run in between.
The .NET 10 project templates propose Minimal APIs as the default for new web APIs, and the official documentation treats them as the main path. Controllers are not deprecated: ASP.NET Core MVC keeps receiving fixes and features because millions of production applications depend on it. If your team has fewer than five developers, the API has fewer than a hundred endpoints and nobody has MVC code to reuse, start with Minimal APIs organized by feature. In every other case, read the sections below before deciding.
What really differs between Minimal APIs and controllers under the hood?
Both use the same routing, the same dependency container and the same middleware chain. The difference comes after routing: controllers go through a richer machinery (filters, model binding, conventions), Minimal APIs through a leaner one where your function is compiled into a delegate.
A controller is created by the container on every request, parameters pass through a very complete model binding system, filters run (authorization, resource, action, exception, result) and the return value crosses the output formatters. That richness is why controllers stay comfortable in projects with complex cross-cutting rules: every stage of the cycle has an official hook.
In a Minimal API the function you declare is the endpoint. ASP.NET Core analyzes its signature once, builds a delegate and injects parameters directly: services from the container, values from route, body or query. There is no class to instantiate per request and no classic filter cycle, although endpoint filters cover the need to intercept calls.
.NET 10 closed two historic gaps. The first is built-in validation: you call AddValidation() and annotations on your models (Required, Range, StringLength) are checked automatically with a standard error response. The second is native OpenAPI 3.1 support in Microsoft.AspNetCore.OpenApi. If you discarded Minimal APIs "because they do not validate", redo the math.
How do you write the same endpoint in both styles?
The same "read an order by number" endpoint takes about fifteen lines in a controller and five in a Minimal API. The saving is in ceremony, not logic, which stays identical and lives in a dedicated service.
First the controller:
// Controllers/OrdersController.cs: the action lives in a class, the route comes from attributes.
[ApiController]
[Route("api/orders")]
public class OrdersController(IOrderService orders) : ControllerBase
{
[HttpGet("{number:int}")]
public async Task<ActionResult<OrderDto>> Get(int number, CancellationToken ct)
{
var order = await orders.FindAsync(number, ct);
if (order is null)
{
return NotFound();
}
return Ok(order);
}
}Then the same function as a Minimal API:
// Program.cs: the function is the endpoint, the service arrives from the container as a parameter.
app.MapGet("/api/orders/{number:int}",
async (int number, IOrderService orders, CancellationToken ct) =>
await orders.FindAsync(number, ct) is { } order
? TypedResults.Ok(order)
: Results.NotFound());You gain fewer lines and no extra class. You pay in readability once the function grows: a lambda with eight parameters and five branches becomes unreadable fast. The cure is the usual one: move logic into a service and leave only the HTTP-to-domain translation in the endpoint. With TypedResults and a return type like Results<Ok<OrderDto>, NotFound> the compiler knows the possible responses and OpenAPI documentation generates itself. The layering is covered in dependency injection in C# and .NET 10.
Are Minimal APIs faster than controllers?
Minimal APIs are faster in synthetic benchmarks, by microseconds per request, with slightly quicker startup. In a real application the difference disappears inside database time, which is thousands of times larger.
Take a real endpoint answering in 40 milliseconds. The database takes 32, serialization 5, routing and the rest of the framework under 1. Moving from controllers to Minimal APIs may shave a fraction of that millisecond. Moving from an unindexed query to an indexed one removes 30 milliseconds. In the case described in slow queries with Entity Framework Core, a page drops from 4.1 seconds to 38 milliseconds by changing an index and a projection.
Style matters in two cases. Cold start: a serverless function or a container that starts often benefits from a minimal application, especially with ahead-of-time compilation, which Minimal APIs support through a source generator while reflection-based controllers do not. High density: many small instances on one node, where every megabyte at startup has a cost. Otherwise measure first, and look at queries, calls to external services and allocations.
How do you keep a minimal API tidy from twenty to three hundred endpoints?
Use a structure by feature: each area (orders, customers, invoices) has a file with its endpoints, collected with MapGroup and registered by an extension method. The startup file stays at twenty lines.
The most common mistake is leaving everything in Program.cs. It works with ten endpoints, becomes a wall of text with fifty and a merge battlefield with two hundred.
// Features/Orders/OrdersEndpoints.cs: one file per area, one registration method.
public static class OrdersEndpoints
{
public static IEndpointRouteBuilder MapOrders(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/orders")
.RequireAuthorization()
.WithTags("Orders");
group.MapGet("/{number:int}", GetOrder);
group.MapPost("/", CreateOrder);
group.MapDelete("/{number:int}", CancelOrder).RequireAuthorization("Admin");
return app;
}
private static async Task<Results<Ok<OrderDto>, NotFound>> GetOrder(
int number, IOrderService orders, CancellationToken ct) =>
await orders.FindAsync(number, ct) is { } order
? TypedResults.Ok(order)
: TypedResults.NotFound();
}A group shares prefix, authorization, filters and metadata for all its endpoints: a rule like "all billing routes require the Accounting role" is written once. Use named methods instead of anonymous lambdas: stack traces stay readable, tests call the function directly and the registration file becomes an index of routes. When an area has real domain logic, a group is not enough: you need an application layer behind it, as described in Domain-Driven Design in C#.
Where do validation, filters and error handling live?
In controllers, validation and filters are automatic and attribute-driven. In Minimal APIs with .NET 10, validation is enabled with AddValidation() and filters become endpoint filters applied to the group. Error handling works for both through one middleware.
This is where the styles resemble each other most after .NET 10, and where teams most often botch a migration: they lose a check the controller performed silently. A controller with ApiController returns a 400 with details if the model fails its annotations. For complex rules (a field depends on another, a value must be checked against the database) annotations are not enough in either style, and a library such as FluentValidation works with both.
Action filters have an equivalent in endpoint filters, which wrap the call and can read parameters, stop execution or change the result. Controllers offer five filter types with a defined order; Minimal APIs offer one simpler type where order is registration order. For errors, let exceptions bubble up to a global handler that turns them into standard problem details (RFC 9457) with AddProblemDetails() and UseExceptionHandler(): every unexpected error leaves with the same shape and a correlation id to find in the logs.
How do you document and version APIs in .NET 10?
In .NET 10, OpenAPI 3.1 documentation is generated from code with Microsoft.AspNetCore.OpenApi for both styles. Versioning goes in the path (/v1/, /v2/) or in a header, with one rule for the whole API.
Register AddOpenApi() in the services and MapOpenApi() at startup, and the application exposes the JSON document at a known address. Typed results describe possible responses on their own. Versioning matters when you have external clients you cannot force to upgrade: one group per version keeps contracts separate, and the old version is switched off on an announced date. For an internal API with a single application, versioning is usually a cost that does not pay back. Save the OpenAPI document in the repository and compare it at every build: an accidental change, such as a renamed field, stops the pipeline before it reaches consumers.
How do you test Minimal APIs and controllers?
The same way: unit tests on domain logic and services, plus integration tests with WebApplicationFactory, which starts the whole application in memory and calls endpoints like a real client. The style hardly matters.
// Tests/OrdersApiTests.cs: starts the API in memory and calls the endpoint like a client.
public class OrdersApiTests(WebApplicationFactory<Program> factory)
: IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task Missing_order_returns_404()
{
var client = factory.CreateClient();
var response = await client.GetAsync("/api/orders/999999");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}Named static functions let you call the endpoint directly in unit tests and check the typed result without starting anything. In controllers you must build the class and fill the HTTP context. It is a real but small advantage. Always have three tests: authorization per group (no token: 401, wrong role: 403), the shape of an unexpected error, and validation with a deliberately wrong body. In my experience these are the three classes of error that reach production most often after a style migration.
How do you migrate from controllers to Minimal APIs without stopping the team?
Migrate by groups, not all at once: the two styles coexist in the same application. Pick a high-change area, rewrite its endpoints as Minimal APIs behind the same paths, verify with integration tests and delete the controller.
ASP.NET Core accepts both styles in one project: app.MapControllers() and app.MapGroup(...) sit side by side. The technique is simple. The traps are hidden rules: global filters registered in AddControllers, authorization attributes on the class, custom formatters, route conventions, validation error behavior, headers added by a result filter. Each must be converted by hand, and an integration test written before the migration on the same path is your safety net.
Do not migrate for style alone. A stable application with thirty controllers and a team that knows them gains nothing: the cost is six to ten person-days for a mid-size API, the performance gain is negligible and regression risk is not zero. Migrate when you are rewriting the area anyway, when controllers have become two-thousand-line containers, or when the service must start fast in a container. For the framework side, see from .NET Framework to .NET 10.
Which one fits your team and your product?
Choose Minimal APIs if the team is small, the API is young and fast startup matters. Choose controllers if you have several teams, existing MVC code or many filters with a precise order. In a mixed application, use both without embarrassment.

| Situation | Recommended choice | Reason |
|---|---|---|
| New API, team up to 5 people | Minimal API | Less ceremony, structure by feature |
| New API, several teams on one product | Controllers or Minimal with folder rules | Conventions known to all, ordered filters |
| Existing MVC application to extend | Controllers | Consistency, no migration cost |
| Containerized microservice, fast startup | Minimal API | Faster startup, AOT possible |
| Serverless function | Minimal API | Contained cold start |
| Many filters with precise order | Controllers | Five filter types with defined order |
The criterion that outweighs the rest is the team that will maintain the code in two years, not the one writing it today. Mixing is legitimate if the rule is written down (new area: minimal, existing area: controllers) and the location of global filters is documented. Bring three facts to the meeting: team size, amount of existing MVC code, runtime context. Apply the table, write the rule on one page in the repository and close the discussion. Getting it wrong costs six to ten person-days of migration, about 3,600-6,000 euro at 600 euro a day, and ten mediocre choices made in a hurry cost more than one wrong choice discussed at length.
The Bergamo team closed the debate in one meeting. Minimal APIs with a feature structure for new areas, controllers left where they are, one page of conventions in the repository and one integration test per route group. No rewrite, no lost quarter, and the newcomer wrote his first endpoint in an afternoon.
If you want to learn to make decisions like this with the right method and to design APIs that survive years of growth, in the C# course by Sviluppatore Migliore we work on real projects with my direct feedback, in recorded lessons and live sessions that fit around your job. Bring your API: we look at it together.
Does your company need custom software or AI in the team?
We design and build custom software, modernize legacy systems and train your developers to work with AI. Tell us the problem: you get a concrete answer within one working day.
Frequently asked questions
For a new project with a small team and simple logic, choose Minimal APIs, which .NET 10 proposes as the default. Controllers remain the better choice with several teams, many filters or existing MVC code. Real-world performance is practically the same, so team organization and habits decide.
No. ASP.NET Core MVC still receives fixes and features, and millions of production applications depend on it. Microsoft promotes Minimal APIs for new projects but has not planned to remove controllers. The two styles can coexist in the same application.
In synthetic benchmarks yes, by microseconds per request, with faster startup and ahead-of-time compilation support. In a real application the difference vanishes inside database time, which is thousands of times larger. Measure queries and external calls before style.
Yes. With .NET 10 you register AddValidation() in the services and annotations on models (Required, Range, StringLength) are checked automatically with a standard error response. For complex rules use a library such as FluentValidation, which works with both styles.
Yes. ASP.NET Core accepts MapControllers and MapGroup at the same startup. It helps migrate one area at a time, as long as you write the rule (new area minimal, existing area controllers) and document where global filters live.
With a structure by feature: one file per area with its endpoints collected by MapGroup and registered by an extension method. Functions have names and logic lives in a service. Program.cs stays around twenty lines even with three hundred endpoints.
For a mid-size API it takes six to ten person-days, about 3,600-6,000 euro at a reference rate of 600 euro a day. It pays off only if you are rewriting the area anyway or the controllers have become unmanageable; for fashion alone, spend the budget elsewhere.
