When .NET 6 shipped Minimal APIs, our first reaction was skepticism — we had years of muscle memory built around Controllers, Startup.cs, and attribute routing. After moving a handful of small internal services over, the appeal is obvious for the right kind of project: less ceremony, faster startup, and a request pipeline you can read top to bottom in one file without hunting through three others.
What Actually Changed
Program.cs now uses top-level statements, so there is no Main method boilerplate. WebApplicationBuilder replaces the old Host.CreateDefaultBuilder plus Startup.ConfigureServices/Configure split — you register services and build middleware in the same file, in order, and that order matters just as much as it always did.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapGet("/api/products", async (IProductRepository repo) =>
Results.Ok(await repo.GetAllAsync()));
app.MapGet("/api/products/{id:int}", async (int id, IProductRepository repo) =>
await repo.GetByIdAsync(id) is Product product
? Results.Ok(product)
: Results.NotFound());
app.MapPost("/api/products", async (Product newProduct, IProductRepository repo) =>
{
await repo.AddAsync(newProduct);
return Results.Created($"/api/products/{newProduct.Id}", newProduct);
});
app.MapDelete("/api/products/{id:int}", async (int id, IProductRepository repo) =>
{
var deleted = await repo.DeleteAsync(id);
return deleted ? Results.NoContent() : Results.NotFound();
});
app.Run();
Model Binding and DI Work Almost the Same
Route parameters, query strings, and the request body still bind the way they do in controllers — a parameter with a matching route token binds from the route, a complex type binds from the JSON body, and [FromServices] (or just the parameter type resolving from DI, as with IProductRepository above) pulls from the container. You can still use [FromQuery], [FromRoute], and [FromBody] explicitly when the framework's inference guesses wrong.
Validation is the one place minimal APIs feel rougher out of the box — there is no automatic ModelState.IsValid the way MVC gives you with data annotations. We either validate manually inside the handler or bring in FluentValidation and call it explicitly before proceeding, which is a few extra lines but keeps handlers honest about what they actually check.
When to Reach for Minimal APIs vs. Controllers
We default to minimal APIs for small services, internal tools, and single-purpose microservices where the whole surface area fits comfortably in one Program.cs or a small set of extension methods grouping related endpoints. We stick with Controllers for anything with a large number of endpoints, heavy use of filters, versioning strategies, or teams that lean on conventions like [ApiController] behaviors (automatic 400 responses on invalid model state, for one) that minimal APIs do not replicate for free.
- Group related endpoints with app.MapGroup("/api/products") to avoid repeating the prefix and to apply shared filters or auth requirements once
- Return typed results (Results.Ok, Results.NotFound, Results.Created) rather than raw objects — it keeps OpenAPI metadata accurate
- Use route constraints like {id:int} to reject malformed input before your handler even runs
- Extract handler bodies into separate static methods once a route file grows past a handful of endpoints, to keep Program.cs readable
Minimal API endpoints do not get the [ApiController] attribute's automatic model-state validation and 400 responses. If you are migrating a controller and relying on that behavior, you need to add it back explicitly — either with manual checks or a validation filter — or you will silently accept requests you used to reject.
Wrapping Up
Minimal APIs are not a replacement for MVC controllers, they are a second tool for a category of service that never needed the full weight of the controller pipeline in the first place. Pick based on the shape of the service, not on which one feels newer.