Introduction#
.NET 6 introduced Minimal APIs as an alternative to the traditional Controller-based approach. Minimal APIs reduce ceremony and compile faster but sacrifice some structure. This post compares both approaches and provides guidance on when each is appropriate.
Minimal APIs#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| // Program.cs — a complete API
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
// Route group with shared prefix and middleware
var orders = app.MapGroup("/api/v1/orders")
.RequireAuthorization()
.WithTags("Orders");
orders.MapGet("/", async (IOrderService svc, CancellationToken ct) =>
Results.Ok(await svc.GetAllAsync(ct)));
orders.MapGet("/{id:int}", async (int id, IOrderService svc, CancellationToken ct) =>
{
var order = await svc.GetByIdAsync(id, ct);
return order is null ? Results.NotFound() : Results.Ok(order);
});
orders.MapPost("/", async (
CreateOrderRequest request,
IOrderService svc,
CancellationToken ct) =>
{
var order = await svc.CreateAsync(request, ct);
return Results.Created($"/api/v1/orders/{order.Id}", order);
}).WithValidator<CreateOrderRequest>(); // FluentValidation integration
orders.MapDelete("/{id:int}", async (int id, IOrderService svc, CancellationToken ct) =>
{
await svc.DeleteAsync(id, ct);
return Results.NoContent();
}).RequireAuthorization("admin-policy");
app.Run();
|
Controller-Based APIs#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| // OrdersController.cs
[ApiController]
[Route("api/v1/[controller]")]
[Authorize]
public class OrdersController : ControllerBase
{
private readonly IOrderService _svc;
public OrdersController(IOrderService svc) => _svc = svc;
[HttpGet]
public async Task<IActionResult> GetAll(CancellationToken ct) =>
Ok(await _svc.GetAllAsync(ct));
[HttpGet("{id:int}")]
[ProducesResponseType<Order>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetById(int id, CancellationToken ct)
{
var order = await _svc.GetByIdAsync(id, ct);
return order is null ? NotFound() : Ok(order);
}
[HttpPost]
public async Task<IActionResult> Create(
[FromBody] CreateOrderRequest request,
CancellationToken ct)
{
var order = await _svc.CreateAsync(request, ct);
return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
}
[HttpDelete("{id:int}")]
[Authorize(Policy = "admin-policy")]
public async Task<IActionResult> Delete(int id, CancellationToken ct)
{
await _svc.DeleteAsync(id, ct);
return NoContent();
}
}
|
Organizing Minimal APIs for Larger Projects#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| // IEndpoint pattern: keeps endpoints organized without controllers
public interface IEndpoint
{
void MapEndpoints(IEndpointRouteBuilder app);
}
// OrderEndpoints.cs
public class OrderEndpoints : IEndpoint
{
public void MapEndpoints(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/v1/orders")
.RequireAuthorization()
.WithTags("Orders");
group.MapGet("/", GetAll);
group.MapGet("/{id:int}", GetById);
group.MapPost("/", Create);
}
static async Task<IResult> GetAll(IOrderService svc, CancellationToken ct) =>
Results.Ok(await svc.GetAllAsync(ct));
static async Task<IResult> GetById(int id, IOrderService svc, CancellationToken ct)
{
var order = await svc.GetByIdAsync(id, ct);
return order is null ? Results.NotFound() : Results.Ok(order);
}
static async Task<IResult> Create(
CreateOrderRequest request,
IOrderService svc,
CancellationToken ct)
{
var order = await svc.CreateAsync(request, ct);
return Results.Created($"/api/v1/orders/{order.Id}", order);
}
}
// Program.cs: auto-register all IEndpoint implementations
var endpoints = typeof(Program).Assembly
.GetTypes()
.Where(t => t.IsAssignableTo(typeof(IEndpoint)) && !t.IsAbstract)
.Select(Activator.CreateInstance)
.Cast<IEndpoint>();
foreach (var endpoint in endpoints)
endpoint.MapEndpoints(app);
|
Filters and Middleware#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // Minimal API endpoint filter (validation, logging)
app.MapPost("/api/v1/orders", CreateOrder)
.AddEndpointFilter(async (context, next) =>
{
var request = context.GetArgument<CreateOrderRequest>(0);
if (request.Total <= 0)
return Results.ValidationProblem(new Dictionary<string, string[]>
{
["Total"] = ["Total must be positive"]
});
return await next(context);
});
// Controller: action filter
public class ValidationFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
context.Result = new UnprocessableEntityObjectResult(
context.ModelState);
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
|
Decision Guide#
| Factor |
Minimal APIs |
Controllers |
| Code volume |
Less boilerplate |
More structure |
| Compile time |
Faster |
Slower |
| API versioning |
Manual grouping |
Built-in ApiVersion |
| Large teams |
Harder to structure |
Easier conventions |
| Existing codebase |
May not mix well |
Standard approach |
| Action filters |
Endpoint filters |
Full filter pipeline |
| OpenAPI |
Full support |
Full support |
Use Minimal APIs for: new microservices, small APIs, teams comfortable with functional style.
Use Controllers for: large APIs with many endpoints, teams with existing conventions, when you need the full filter pipeline.
Conclusion#
Minimal APIs reduce boilerplate and are the direction ASP.NET Core is moving. For new projects, they work well with the IEndpoint pattern for organization. Controllers remain valid for large, complex APIs where the additional structure pays for itself. Both have full OpenAPI/Swagger support and equivalent performance.