TL;DR: Minimal APIs are significantly faster and require far less boilerplate than traditional MVC Controllers. Use Route Groups to organize endpoints and Endpoint Filters to handle validation. For complex, massive enterprise apps with hundreds of routes, Controllers still have their place.
For years, building an API in ASP.NET meant creating a Controller class, inheriting from ControllerBase, adding [ApiController] attributes, and relying heavily on Reflection.
With the introduction of Minimal APIs, .NET fundamentally shifted how we build HTTP endpoints. They are leaner, require far less boilerplate, and most importantly: they are insanely fast.
Why Minimal APIs?
Minimal APIs cut out the heavy MVC pipeline. By leveraging C# lambdas and minimal abstraction, they use less memory and deliver higher throughput.
❌ The Old Way (Controllers)
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService userService)
{
_userService = userService;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
var user = await _userService.GetUserAsync(id);
return user is not null ? Ok(user) : NotFound();
}
}
✅ The New Way (Minimal APIs)
// Program.cs
var app = builder.Build();
app.MapGet("/api/users/{id}", async (int id, IUserService userService) =>
{
var user = await userService.GetUserAsync(id);
return user is not null ? Results.Ok(user) : Results.NotFound();
});
app.Run();
Look how clean that is! The dependencies (IUserService) are injected directly into the delegate automatically.
Organizing Minimal APIs with Route Groups
The biggest criticism of Minimal APIs early on was that Program.cs would turn into a 1,000-line mess. Route Groups solve this completely.
// Define a group for all /api/products endpoints
var productsApi = app.MapGroup("/api/products")
.RequireAuthorization() // Applies to ALL endpoints in this group!
.WithOpenApi();
// Map specific endpoints to the group
productsApi.MapGet("/", GetAllProducts);
productsApi.MapGet("/{id}", GetProductById);
productsApi.MapPost("/", CreateProduct);
// The actual handler functions
static async Task<IResult> GetProductById(int id, IProductService service)
{
return Results.Ok(await service.GetAsync(id));
}
Data Validation with Endpoint Filters
What about validation? In Controllers, we used Action Filters. In Minimal APIs, we use Endpoint Filters.
Here is how you can write a reusable filter to validate incoming DTOs using FluentValidation:
public class ValidationFilter<T> : IEndpointFilter where T : class
{
private readonly IValidator<T> _validator;
public ValidationFilter(IValidator<T> validator)
{
_validator = validator;
}
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var argument = context.Arguments.OfType<T>().FirstOrDefault();
if (argument == null) return Results.BadRequest();
var validationResult = await _validator.ValidateAsync(argument);
if (!validationResult.IsValid)
{
return Results.ValidationProblem(validationResult.ToDictionary());
}
return await next(context);
}
}
You can seamlessly attach this to your Minimal API:
app.MapPost("/api/users", CreateUser)
.AddEndpointFilter<ValidationFilter<CreateUserDto>>();
Summary
Minimal APIs represent the modern era of .NET. They provide Node.js/Express levels of simplicity combined with the legendary performance and strong typing of C#. Start using them for your next microservice!