Minimal API en .NET : ce que c’est, et pourquoi j’ai lâché les controllers
Les Minimal API ASP.NET Core expliquées par la pratique : route groups, endpoint filters, TypedResults, OpenAPI natif, organisation du code et cas où garder les controllers.
Pendant des années, une API .NET, c’était forcément des controllers. Un dossier Controllers, une classe qui hérite de ControllerBase, des attributs [HttpGet] partout, et un constructeur qui injecte dix services dont chaque action n’en utilise que deux.
Quand les Minimal API sont arrivées avec .NET 6, je les ai d’abord rangées dans la case « jouet pour démos et microservices ». Aujourd’hui, c’est ce que j’utilise par défaut, y compris sur un ERP qui compte plus de 250 endpoints. Je t’explique ce qui m’a fait changer d’avis, et comment éviter le piège classique.
C’est quoi, une Minimal API ?
C’est une façon de déclarer un endpoint sans controller : une route, une fonction, et c’est tout.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options => options.UseNpgsql(connectionString));
var app = builder.Build();
app.MapGet("/api/payment-terms", async (AppDbContext db, CancellationToken ct) =>
await db.PaymentTerms.ToListAsync(ct));
app.Run();Les paramètres de la fonction sont résolus automatiquement : le DbContext vient du conteneur DI, le CancellationToken de la requête HTTP, et un paramètre comme Guid id vient de la route. Pas d’attribut, pas de classe de base.
Ce qui m’a fait changer d’avis
Chaque endpoint ne reçoit que ce dont il a besoin. Fini le constructeur de controller qui injecte tout le monde pour que chaque action pioche dedans. Les dépendances arrivent en paramètres de la fonction, endpoint par endpoint.
Une feature tient dans un fichier. C’est ce qui colle le mieux avec une architecture en vertical slices : la route, la request, le handler et la réponse au même endroit. Quand tu bosses sur « créer une condition de paiement », tu ouvres un fichier, pas quatre.
C’est plus léger. Pas de pipeline MVC complet derrière chaque appel : moins d’allocations, un démarrage plus rapide, et c’est compatible avec la compilation Native AOT depuis .NET 8. Sur une API classique, ce n’est pas ce qui change ta vie, mais ça ne fait pas de mal.
Le routing est explicite. Tu lis la route là où l’endpoint est déclaré. Pas de convention de nommage de controller à deviner.
Les outils qui manquaient au début, et qui sont là maintenant
En .NET 6, il manquait des briques pour faire une vraie API. Elles sont arrivées version après version.
Les route groups (.NET 7) factorisent un préfixe, des tags OpenAPI et l’autorisation :
var paymentTerms = app.MapGroup("/api/payment-terms")
.WithTags("PaymentTerms")
.RequireAuthorization();
paymentTerms.MapGet("/{id:guid}", async Task<Results<Ok<PaymentTermResponse>, NotFound>> (
Guid id, AppDbContext db, CancellationToken ct) =>
{
var paymentTerm = await db.PaymentTerms.FindAsync([id], ct);
return paymentTerm is null ? TypedResults.NotFound() : TypedResults.Ok(paymentTerm.ToResponse());
});Tu remarques le type de retour Results<Ok<...>, NotFound> ? C’est TypedResults : les réponses possibles sont dans la signature, donc vérifiées à la compilation et documentées automatiquement dans OpenAPI.
Les endpoint filters (.NET 7) remplacent les action filters de MVC :
paymentTerms.MapPost("/", CreatePaymentTerm)
.AddEndpointFilter(async (context, next) =>
{
// S'exécute avant le handler, comme un action filter MVC
var request = context.GetArgument<CreatePaymentTermRequest>(0);
if (string.IsNullOrWhiteSpace(request.Name))
return TypedResults.ValidationProblem(new Dictionary<string, string[]>
{
["name"] = ["Le nom est obligatoire."],
});
return await next(context);
});[AsParameters] (.NET 7) regroupe les paramètres d’un endpoint dans un seul objet : route, query, body et services dans un record, au lieu d’une signature de dix paramètres.
L’OpenAPI natif (.NET 9) : builder.Services.AddOpenApi() et app.MapOpenApi(), sans Swashbuckle. Ajoute Scalar par-dessus pour une interface de doc agréable (app.MapScalarApiReference()).
La validation intégrée (.NET 10) : avec builder.Services.AddValidation(), les DataAnnotations de tes requests sont vérifiées automatiquement, et une request invalide renvoie un 400 propre sans écrire de filtre.
Le piège : le Program.cs de 2 000 lignes
Tous les exemples de Minimal API mettent les endpoints dans Program.cs. Pour une démo, c’est parfait. Pour un vrai projet, c’est la catastrophe : au bout de trente endpoints, ton Program.cs devient illisible et tout le monde se marche dessus dans les merges.
La solution classique, c’est de donner à chaque endpoint sa propre classe, et de les découvrir automatiquement au démarrage :
public interface IEndpoint
{
void Map(IEndpointRouteBuilder app);
}
public sealed class GetPaymentTerm : IEndpoint
{
public void Map(IEndpointRouteBuilder app) =>
app.MapGet("/api/payment-terms/{id:guid}", Handle).WithTags("PaymentTerms");
private static async Task<Results<Ok<PaymentTermResponse>, NotFound>> Handle(
Guid id, AppDbContext db, CancellationToken ct)
{
var paymentTerm = await db.PaymentTerms.FindAsync([id], ct);
return paymentTerm is null ? TypedResults.NotFound() : TypedResults.Ok(paymentTerm.ToResponse());
}
}Un scan d’assembly au démarrage appelle Map sur chaque implémentation, et ton Program.cs redevient court.
Ça marche bien, mais au bout de quelques projets, je me suis rendu compte que j’écrivais toujours le même code : le scan, le préfixe api, les tags, les métadonnées de réponse (201 sur un POST, 404 sur un GET…), et l’envoi de la request vers MediatR ou Wolverine. C’est exactement ce qui m’a poussé à écrire QuickApi, une petite bibliothèque qui fait tout ça pour moi. Je la présente dans un article dédié.
Quand garder les controllers
Les Minimal API ne sont pas obligatoires, et je ne migrerais pas tout pour le principe.
- Tu as une grosse base de code MVC qui marche. La réécrire n’apporte pas grand-chose. Les deux styles cohabitent très bien dans la même appli : commence les nouvelles features en Minimal API.
- Tu dépends beaucoup de l’écosystème MVC : model binders maison, filtres MVC complexes, OData. Tout n’a pas d’équivalent direct.
- Ton équipe est très à l’aise avec les controllers et le projet ne souffre pas de leur cérémonie. Le meilleur style, c’est aussi celui que l’équipe maîtrise.
Le mot de la fin
Les Minimal API ne sont plus un jouet. Avec les route groups, les endpoint filters, TypedResults, l’OpenAPI natif et la validation intégrée, elles couvrent tout ce dont une vraie API a besoin, avec beaucoup moins de cérémonie que les controllers. La seule vraie discipline à avoir, c’est de sortir les endpoints de Program.cs dès le départ.
Si tu hésites à franchir le pas sur ton projet, ou si tu veux un avis sur l’organisation de ton API, écris-moi.