Aspnet Minimal API
ASP.NET Minimal APIs for building lightweight HTTP endpoints in .NET
You are an expert in ASP.NET Minimal APIs for building lightweight, high-performance HTTP services in .NET.
## Key Points
- **Use route groups** to share prefixes, filters, and metadata across related endpoints.
- **Return `TypedResults`** instead of `Results` so that OpenAPI metadata is generated automatically.
- **Extract endpoint definitions** into static classes or extension methods once `Program.cs` grows beyond ~50 lines.
- **Use endpoint filters** for cross-cutting concerns like validation, logging, or authorization checks specific to an endpoint.
- **Register services in DI** and accept them as handler parameters rather than resolving them manually.
- **Leverage `AsParameters`** for handlers with many parameters to group them into a single record or class.
- **Forgetting `AddEndpointsApiExplorer()`** — without it, Swagger/OpenAPI will not discover minimal API endpoints.
- **Blocking on async code** — always use `async`/`await`; never call `.Result` or `.Wait()` on tasks inside handlers.
- **Not constraining route parameters** — use constraints like `{id:int}` or `{slug:regex(^[a-z-]+$)}` to avoid ambiguous routing.
- **Putting all endpoints in `Program.cs`** — this works for small APIs but becomes unmanageable quickly; use `MapGroup` and extension methods early.
- **Ignoring `Results.Problem` for errors** — return RFC 7807 problem details instead of bare status codes so clients get structured error information.skilldb get csharp-dotnet-skills/Aspnet Minimal APIFull skill: 209 linesASP.NET Minimal APIs — C#/.NET
You are an expert in ASP.NET Minimal APIs for building lightweight, high-performance HTTP services in .NET.
Core Philosophy
Overview
Minimal APIs were introduced in .NET 6 as a streamlined way to build HTTP APIs without the ceremony of controllers, model binding attributes, and startup classes. They use top-level statements and lambda-based endpoint definitions to reduce boilerplate while retaining the full power of the ASP.NET Core pipeline.
Core Concepts
App Builder and Endpoint Routing
The WebApplication builder is the entry point. Endpoints are registered directly on the app instance using HTTP method extensions.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.MapGet("/", () => "Hello, World!");
app.Run();
Parameter Binding
Minimal APIs automatically bind parameters from route values, query strings, headers, and the request body.
// Route parameter
app.MapGet("/users/{id:int}", (int id) => $"User {id}");
// Query string
app.MapGet("/search", (string? q, int page = 1) =>
Results.Ok(new { Query = q, Page = page }));
// Request body (POST/PUT)
app.MapPost("/users", (CreateUserRequest request) =>
{
// request is deserialized from JSON body
return Results.Created($"/users/{request.Id}", request);
});
// Explicit binding sources
app.MapGet("/items", ([FromHeader(Name = "X-Tenant")] string tenant,
[FromQuery] int? limit) =>
Results.Ok(new { Tenant = tenant, Limit = limit ?? 50 }));
Typed Results
Use TypedResults for OpenAPI-friendly return types that improve Swagger documentation.
app.MapGet("/orders/{id}", Results<Ok<Order>, NotFound> (int id, OrderService svc) =>
{
var order = svc.Find(id);
return order is not null
? TypedResults.Ok(order)
: TypedResults.NotFound();
});
Route Groups
Organize related endpoints under a shared prefix with MapGroup.
var api = app.MapGroup("/api/v1");
var users = api.MapGroup("/users")
.RequireAuthorization()
.WithTags("Users");
users.MapGet("/", (UserService svc) => svc.GetAll());
users.MapGet("/{id:int}", (int id, UserService svc) => svc.GetById(id));
users.MapPost("/", (CreateUserRequest req, UserService svc) => svc.Create(req));
Implementation Patterns
Endpoint Filters (Middleware for Endpoints)
app.MapPost("/orders", (CreateOrderRequest req, OrderService svc) => svc.Create(req))
.AddEndpointFilter(async (context, next) =>
{
var request = context.GetArgument<CreateOrderRequest>(0);
if (string.IsNullOrEmpty(request.ProductName))
return Results.ValidationProblem(
new Dictionary<string, string[]>
{
["ProductName"] = ["Product name is required"]
});
return await next(context);
});
Validation with FluentValidation
public class ValidationFilter<T>(IValidator<T> validator) : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context, EndpointFilterDelegate next)
{
var model = context.Arguments.OfType<T>().First();
var result = await validator.ValidateAsync(model);
if (!result.IsValid)
return Results.ValidationProblem(result.ToDictionary());
return await next(context);
}
}
// Registration
users.MapPost("/", (CreateUserRequest req, UserService svc) => svc.Create(req))
.AddEndpointFilter<ValidationFilter<CreateUserRequest>>();
Organizing Endpoints into Static Classes
public static class UserEndpoints
{
public static RouteGroupBuilder MapUserEndpoints(this RouteGroupBuilder group)
{
group.MapGet("/", GetAll);
group.MapGet("/{id:int}", GetById);
group.MapPost("/", Create);
return group;
}
private static async Task<Ok<List<UserDto>>> GetAll(UserService svc)
{
var users = await svc.GetAllAsync();
return TypedResults.Ok(users);
}
private static async Task<Results<Ok<UserDto>, NotFound>> GetById(
int id, UserService svc)
{
var user = await svc.GetByIdAsync(id);
return user is not null
? TypedResults.Ok(user)
: TypedResults.NotFound();
}
private static async Task<Created<UserDto>> Create(
CreateUserRequest req, UserService svc)
{
var user = await svc.CreateAsync(req);
return TypedResults.Created($"/users/{user.Id}", user);
}
}
// In Program.cs
var users = api.MapGroup("/users").RequireAuthorization();
users.MapUserEndpoints();
Best Practices
- Use route groups to share prefixes, filters, and metadata across related endpoints.
- Return
TypedResultsinstead ofResultsso that OpenAPI metadata is generated automatically. - Extract endpoint definitions into static classes or extension methods once
Program.csgrows beyond ~50 lines. - Use endpoint filters for cross-cutting concerns like validation, logging, or authorization checks specific to an endpoint.
- Register services in DI and accept them as handler parameters rather than resolving them manually.
- Leverage
AsParametersfor handlers with many parameters to group them into a single record or class.
public record GetOrdersRequest(
[FromQuery] int Page,
[FromQuery] int PageSize,
[FromHeader(Name = "X-Tenant")] string Tenant);
app.MapGet("/orders", ([AsParameters] GetOrdersRequest req) =>
Results.Ok(new { req.Page, req.PageSize, req.Tenant }));
Common Pitfalls
- Forgetting
AddEndpointsApiExplorer()— without it, Swagger/OpenAPI will not discover minimal API endpoints. - Blocking on async code — always use
async/await; never call.Resultor.Wait()on tasks inside handlers. - Not constraining route parameters — use constraints like
{id:int}or{slug:regex(^[a-z-]+$)}to avoid ambiguous routing. - Putting all endpoints in
Program.cs— this works for small APIs but becomes unmanageable quickly; useMapGroupand extension methods early. - Ignoring
Results.Problemfor errors — return RFC 7807 problem details instead of bare status codes so clients get structured error information.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add csharp-dotnet-skills
Related Skills
Async Patterns
Async/await patterns, Task-based concurrency, and cancellation in C#/.NET
Blazor
Blazor component model, rendering modes, and interactive web UI development in .NET
Configuration
.NET configuration system, options pattern, and secrets management
Dependency Injection
Dependency injection patterns and service registration in .NET's built-in DI container
Entity Framework
Entity Framework Core ORM for data access, migrations, and query optimization in .NET
Mediatr
MediatR library for implementing CQRS, commands, queries, and pipeline behaviors in .NET