Minimal API Workshop
Module 1
Fundamentals & the Endpoint Pipeline
Route groups · binding · TypedResults · filters · OpenAPI
9:20 – 10:30 AM
By the end you can
- Explain the builder → services → pipeline → endpoints shape
- Organize endpoints with route groups and feature folders
- Bind parameters from every source
- Return strongly typed results and read the OpenAPI document
Anatomy of Program.cs
Four parts — every line belongs to one
The four parts
var builder = WebApplication.CreateBuilder(args); // 1. builder
builder.Services.AddOpenApi(); // 2. services (DI)
var app = builder.Build();
app.UseHttpsRedirection(); // 3. pipeline
app.MapGet("/health", () => "Healthy"); // 4. endpoints
app.Run();
“Minimal” = fewer abstractions between you and HTTP, not fewer capabilities.
Coming from controllers
| Controller world | Minimal API world |
|---|---|
[Route("api/[controller]")] | app.MapGroup("/api/products") |
[HttpGet("{id}")] | group.MapGet("/{id}", handler) |
ActionResult<T> | Results<Ok<T>, NotFound> |
| Action filters | Endpoint filters |
[Authorize] | .RequireAuthorization() |
Route groups + feature folders
public static RouteGroupBuilder MapProductEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/products").WithTags("Products");
group.MapGet("/", GetProducts).WithName("GetProducts");
group.MapGet("/{id:int}", GetProduct).WithName("GetProduct");
group.MapPost("/", CreateProduct).WithName("CreateProduct");
return group;
}
Anything applied to the group — a tag, filter, policy, cache — applies to every endpoint in it.
Parameter binding
| Source | How it binds |
|---|---|
Route /{id} | Parameter with a matching name |
| Query string | Simple types not matched by a route value |
| Body | A complex type on POST / PUT / PATCH |
| Header | [FromHeader] |
| Services | Any registered service |
| Special | HttpContext, CancellationToken, ClaimsPrincipal |
Bind a group of query values into one record with [AsParameters].
TypedResults, not Results
static async Task<Results<Ok<ProductDto>, NotFound>> GetProduct(int id, IProductCatalog catalog)
{
var product = await catalog.FindAsync(id);
return product is null
? TypedResults.NotFound()
: TypedResults.Ok(product.ToDto());
}
The Results<…> union is the contract: compile-time correctness and OpenAPI for free.
Endpoint filters
group.AddEndpointFilter(async (context, next) =>
{
var sw = Stopwatch.StartNew();
var result = await next(context);
context.HttpContext.Response.Headers["X-Elapsed-Ms"] = sw.ElapsedMilliseconds.ToString();
return result;
});
Middleware runs outside filters; filters run outside the handler — and see the bound arguments.
OpenAPI in .NET 10
builder.Services.AddOpenApi();
// ...
app.MapOpenApi(); // /openapi/v1.json
app.MapScalarApiReference(); // /scalar/v1 (browsable UI)
Built in — no Swashbuckle. Metadata flows from TypedResults, .WithName, .WithSummary.
Lab
Lab 1 · The Public Menu
- Build
/api/productswith filters, a single-item GET, and create - Return
TypedResults; add a timing endpoint filter - Read the OpenAPI document and fix the planted
Producesbug
Ends at checkpoint-1