Skip to main content

5 Things I Wish I Knew Sooner About ASP.NET Core Minimal APIs

· 2 min read

Minimal APIs in ASP.NET Core have come a long way since their introduction in .NET 6. After using them in several projects, here are five things I wish I'd known from the start.

1. Endpoint Filters Are Your Friend

If you're coming from controllers, you probably miss action filters. Endpoint filters are the Minimal API equivalent and they're incredibly useful for cross-cutting concerns like validation and logging.

app.MapPost("/api/items", async (CreateItemRequest request, IItemService service) =>
{
var item = await service.CreateAsync(request);
return Results.Created($"/api/items/{item.Id}", item);
})
.AddEndpointFilter(async (context, next) =>
{
var request = context.GetArgument<CreateItemRequest>(0);
if (string.IsNullOrWhiteSpace(request.Name))
{
return Results.ValidationProblem(
new Dictionary<string, string[]>
{
["Name"] = ["Name is required."]
});
}
return await next(context);
});

2. Route Groups Keep Things Organized

Once you have more than a handful of endpoints, MapGroup is essential for keeping your Program.cs clean:

var items = app.MapGroup("/api/items")
.RequireAuthorization();

items.MapGet("/", GetAllItems);
items.MapGet("/{id}", GetItemById);
items.MapPost("/", CreateItem);
items.MapPut("/{id}", UpdateItem);
items.MapDelete("/{id}", DeleteItem);

3. TypedResults Improve OpenAPI Generation

Using TypedResults instead of Results gives you better OpenAPI/Swagger documentation out of the box:

static async Task<Results<Ok<Item>, NotFound>> GetItemById(
int id,
IItemService service)
{
var item = await service.GetByIdAsync(id);
return item is not null
? TypedResults.Ok(item)
: TypedResults.NotFound();
}

4. Extension Methods for Endpoint Mapping

Move your endpoint definitions into static extension methods to keep Program.cs focused on app configuration:

public static class ItemEndpoints
{
public static IEndpointRouteBuilder MapItemEndpoints(
this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/items");
group.MapGet("/", GetAll);
group.MapGet("/{id}", GetById);
return app;
}

private static async Task<Ok<List<Item>>> GetAll(IItemService service)
{
var items = await service.GetAllAsync();
return TypedResults.Ok(items);
}

private static async Task<Results<Ok<Item>, NotFound>> GetById(
int id, IItemService service)
{
var item = await service.GetByIdAsync(id);
return item is not null
? TypedResults.Ok(item)
: TypedResults.NotFound();
}
}

Then in Program.cs it's just:

app.MapItemEndpoints();

5. Binding from Multiple Sources

Minimal APIs can bind parameters from route values, query strings, headers, and the request body all at once. The [AsParameters] attribute is great for grouping related parameters:

app.MapGet("/api/items", async ([AsParameters] ItemQuery query, IItemService service) =>
{
var items = await service.SearchAsync(query);
return TypedResults.Ok(items);
});

public record ItemQuery(
string? Search,
int Page = 1,
int PageSize = 20,
string SortBy = "Name");

These patterns have made my Minimal API projects significantly more maintainable. If you're still organizing everything in Program.cs, give endpoint extension methods and route groups a try.