Learn ASP.NET Core Minimal APIs: route handlers, parameter binding, endpoint filters, route groups, and when to use them over controllers.
Minimal APIs (introduced in .NET 6) provide a lightweight way to define HTTP endpoints directly in Program.cs without controllers. They are ideal for microservices, serverless functions, and simple APIs where the overhead of full MVC is not warranted.
Define endpoints with app.MapGet, MapPost, MapPut, MapDelete using lambda handlers:
1var builder = WebApplication.CreateBuilder(args);
2var app = builder.Build();
3
4// Simple GET endpoint
5app.MapGet("/", () => "Hello World!");
6
7// Return JSON
8app.MapGet("/api/items", () => new[] { "Item1", "Item2", "Item3" });
9
10// Route parameters
11app.MapGet("/api/items/{id}", (int id) => new { Id = id, Name = $"Item {id}" });
12
13// Query parameters
14app.MapGet("/api/search", (string? q, int page = 1) =>
15 new { Query = q, Page = page });
16
17// POST with body
18app.MapPost("/api/items", (ItemDto item) =>
19 Results.Created($"/api/items/{item.Id}", item));
20
21// PUT with route and body
22app.MapPut("/api/items/{id}", (int id, ItemDto item) =>
23 Results.Ok(item));
24
25// DELETE
26app.MapDelete("/api/items/{id}", (int id) => Results.NoContent());
27
28app.Run();
29
30record ItemDto(int Id, string Name, decimal Price);1// Typed results
2app.MapGet("/api/products/{id}", async (int id, IProductService service) =>
3{
4 var product = await service.GetByIdAsync(id);
5 return product is null
6 ? Results.NotFound()
7 : Results.Ok(product);
8});
9
10// Dependency injection
11app.MapGet("/api/users", async (
12 IUserRepository repo,
13 ILogger<Program> logger,
14 CancellationToken ct) =>
15{
16 logger.LogInformation("Getting all users");
17 return await repo.GetAllAsync(ct);
18});
19
20// HttpContext access
21app.MapGet("/api/context", (HttpContext context) =>
22{
23 var userAgent = context.Request.Headers.UserAgent;
24 var clientIp = context.Connection.RemoteIpAddress;
25 return new { UserAgent = userAgent.ToString(), ClientIp = clientIp?.ToString() };
26});
27
28// File downloads
29app.MapGet("/api/download/{filename}", (string filename) =>
30 Results.File($"/files/{filename}", "application/octet-stream", filename));
31
32// Streaming response
33app.MapGet("/api/stream", async (HttpContext context) =>
34{
35 context.Response.ContentType = "text/event-stream";
36 for (int i = 0; i < 10; i++)
37 {
38 await context.Response.WriteAsync($"data: Message {i}\\n\\n");
39 await context.Response.Body.FlushAsync();
40 await Task.Delay(1000);
41 }
42});Bind parameters from route, query, body, headers, and DI:
1// Validation filter
2app.MapPost("/api/items", (ItemDto item) => Results.Created($"/api/items/{item.Id}", item))
3 .AddEndpointFilter(async (context, next) =>
4 {
5 var item = context.GetArgument<ItemDto>(0);
6 if (string.IsNullOrEmpty(item.Name))
7 return Results.BadRequest("Name is required");
8 return await next(context);
9 });
10
11// Logging filter
12app.MapGet("/api/data", () => new { Data = "test" })
13 .AddEndpointFilter(async (context, next) =>
14 {
15 var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
16 logger.LogInformation("Request started: {Path}", context.HttpContext.Request.Path);
17 var result = await next(context);
18 logger.LogInformation("Request completed");
19 return result;
20 });
21
22// Typed filter class
23public class ValidateIdFilter : IEndpointFilter
24{
25 public async ValueTask<object?> InvokeAsync(
26 EndpointFilterInvocationContext context,
27 EndpointFilterDelegate next)
28 {
29 var id = context.GetArgument<int>(0);
30 if (id <= 0)
31 return Results.BadRequest("ID must be positive");
32 return await next(context);
33 }
34}
35
36app.MapGet("/api/items/{id}", (int id) => new { Id = id })
37 .AddEndpointFilter<ValidateIdFilter>();Endpoint filters apply cross-cutting concerns; route groups organize related endpoints:
1// Group endpoints with common prefix
2var api = app.MapGroup("/api");
3
4api.MapGet("/health", () => "OK");
5api.MapGet("/version", () => new { Version = "1.0.0" });
6
7// Nested groups with versioning
8var v1 = api.MapGroup("/v1");
9var v2 = api.MapGroup("/v2");
10
11v1.MapGet("/products", () => new[] { "Product v1" });
12v2.MapGet("/products", () => new[] { new { Id = 1, Name = "Product v2" } });
13
14// Groups with filters
15var authenticated = api.MapGroup("/secure")
16 .RequireAuthorization();
17
18authenticated.MapGet("/profile", (ClaimsPrincipal user) =>
19 new { UserId = user.FindFirstValue(ClaimTypes.NameIdentifier) });
20
21// Groups with tags for OpenAPI
22var products = api.MapGroup("/products")
23 .WithTags("Products")
24 .WithOpenApi();
25
26products.MapGet("/", () => new[] { "Product1" })
27 .WithName("GetProducts")
28 .WithSummary("Get all products");
29
30products.MapGet("/{id}", (int id) => new { Id = id })
31 .WithName("GetProductById");Building lightweight APIs with minimal code
Minimal APIs have lower startup overhead and are recommended for new microservices. Switch to controllers when you need complex model binding, view rendering, or large team familiarity with MVC conventions. TypedResults in Minimal APIs provide compile-time type safety and accurate OpenAPI schemas.
Your progress is saved in this browser, on this device only.