minimal-api
codewithmukesh/dotnet-claude-kit
Build type-safe HTTP endpoints with .NET 10 minimal APIs, auto-discovery, and OpenAPI documentation.
What is minimal-api?
Minimal APIs are the default for building HTTP endpoints in .NET 10. Use this skill when creating or configuring API endpoints, setting up routing, documenting with OpenAPI, or implementing cross-cutting concerns like validation and rate limiting.
- Auto-discover and register endpoint groups via IEndpointGroup interface pattern
- Use TypedResults for compile-time type safety and automatic OpenAPI schema generation
- Bind parameters from routes, queries, headers, body, and dependency injection automatically
- Apply endpoint filters for validation, logging, idempotency, and other cross-cutting concerns
- Configure rate limiting and output caching at the group or endpoint level
- Document endpoints with metadata (.WithName, .WithSummary, .WithTags) that feeds into OpenAPI specs
How to install minimal-api
npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill minimal-api- .NET 10 SDK installed
- Basic understanding of HTTP verbs and REST concepts
How to use minimal-api
- 1.Create an IEndpointGroup interface in Extensions/IEndpointGroup.cs
- 2.Implement EndpointExtensions.MapEndpoints() for auto-discovery in Extensions/EndpointExtensions.cs
- 3.Call app.MapEndpoints() once in Program.cs (never changes after)
- 4.Create one endpoint group file per feature (e.g., Features/Orders/OrderEndpoints.cs) implementing IEndpointGroup
- 5.Use MapGroup() to organize related endpoints and apply shared metadata or filters
- 6.Return TypedResults (Ok, Created, NotFound, ValidationProblem) for type-safe responses
- 7.Add endpoint metadata with .WithName(), .WithSummary(), .Produces() for OpenAPI documentation
Use cases
- Building a REST API with multiple endpoint groups organized by feature
- Adding validation and error handling to mutating endpoints using filters
- Configuring OpenAPI/Swagger documentation without external libraries
- Implementing rate limiting and caching policies for API endpoints
- Migrating from controller-based APIs to minimal APIs for better performance
- Backend developers building .NET 10 APIs
- Teams adopting minimal APIs as the default over controllers
- Developers implementing OpenAPI documentation and Swagger UI
- API architects designing scalable endpoint organization
minimal-api FAQ
Minimal APIs are the default for .NET 10. Use controllers only when migrating legacy code. Minimal APIs are lighter, faster, and compose well with any architecture.
Use the IEndpointGroup auto-discovery pattern: one file per endpoint group, each implementing IEndpointGroup. A single app.MapEndpoints() call in Program.cs discovers all groups automatically.
Use TypedResults instead of Results, and add endpoint metadata with .WithName(), .WithSummary(), .Produces(), and .ProducesProblem(). This metadata feeds directly into the OpenAPI spec.
Apply the canonical ValidationFilter<TRequest> from the error-handling skill to your endpoint. It resolves FluentValidation validators from DI and returns validation problems automatically.
Yes. Call group.AddEndpointFilter<YourFilter>() on the MapGroup result to apply a filter to all endpoints in that group.
Full instructions (SKILL.md)
Source of truth, from codewithmukesh/dotnet-claude-kit.
name: minimal-api description: > .NET 10 minimal APIs — the default for building HTTP endpoints. Covers MapGroup, endpoint filters, TypedResults, OpenAPI metadata, parameter binding, and route conventions. Load this skill when creating API endpoints, configuring routing, setting up OpenAPI documentation, or when the user mentions "endpoint", "MapGet", "MapPost", "MapGroup", "TypedResults", "route", "minimal API", "OpenAPI", "swagger", "rate limiting", or "output caching".
Minimal APIs (.NET 10)
Core Principles
- Minimal APIs are the default — Use controllers only when migrating legacy code. Minimal APIs are lighter, faster, and compose well with any architecture style.
- Group endpoints with
MapGroup— Never scatter individualMapGet/MapPostcalls inProgram.cs. Group related endpoints together. - Use
TypedResultsfor OpenAPI —TypedResults.Ok(value)gives you compile-time type safety AND correct OpenAPI documentation.Results.Ok(value)does not. - Metadata over comments — Use
.WithName(),.WithTags(),.WithSummary()to document endpoints. The metadata feeds into OpenAPI specs.
Patterns
Endpoint Group Auto-Discovery (Required Pattern)
Every endpoint group lives in its own file and implements IEndpointGroup. A single app.MapEndpoints() call in Program.cs discovers and registers all groups automatically. Program.cs never changes when you add new endpoint groups.
// Extensions/IEndpointGroup.cs
public interface IEndpointGroup
{
void Map(IEndpointRouteBuilder app);
}
// Extensions/EndpointExtensions.cs
public static class EndpointExtensions
{
public static WebApplication MapEndpoints(this WebApplication app)
{
var groups = typeof(Program).Assembly
.GetTypes()
.Where(t => t.IsAssignableTo(typeof(IEndpointGroup)) && !t.IsInterface && !t.IsAbstract)
.Select(Activator.CreateInstance)
.Cast<IEndpointGroup>();
foreach (var group in groups)
group.Map(app);
return app;
}
}
// Program.cs — this NEVER changes when adding endpoints
var app = builder.Build();
app.MapEndpoints();
app.Run();
// Features/Orders/OrderEndpoints.cs — one file per endpoint group
public sealed class OrderEndpoints : IEndpointGroup
{
public void Map(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/orders").WithTags("Orders");
group.MapPost("/", CreateOrder)
.WithName("CreateOrder")
.WithSummary("Create a new order")
.Produces<OrderResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.RequireAuthorization();
group.MapGet("/{id:guid}", GetOrder)
.WithName("GetOrder")
.Produces<OrderResponse>()
.ProducesProblem(StatusCodes.Status404NotFound);
group.MapGet("/", ListOrders)
.WithName("ListOrders")
.Produces<PagedList<OrderResponse>>();
}
private static async Task<Results<Created<OrderResponse>, ValidationProblem>> CreateOrder(
CreateOrderRequest request,
ISender sender,
CancellationToken ct)
{
var result = await sender.Send(new CreateOrder.Command(request.CustomerId, request.Items), ct);
return result.IsSuccess
? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
: TypedResults.ValidationProblem(result.Errors);
}
private static async Task<Results<Ok<OrderResponse>, NotFound>> GetOrder(
Guid id,
ISender sender,
CancellationToken ct)
{
var result = await sender.Send(new GetOrder.Query(id), ct);
return result.IsSuccess
? TypedResults.Ok(result.Value)
: TypedResults.NotFound();
}
private static async Task<Ok<PagedList<OrderResponse>>> ListOrders(
[AsParameters] ListOrdersQuery query,
ISender sender,
CancellationToken ct)
{
var result = await sender.Send(query, ct);
return TypedResults.Ok(result);
}
}
TypedResults for Type-Safe Responses
TypedResults provides compile-time guarantees and automatic OpenAPI schema generation.
// GOOD — TypedResults with union return type
private static async Task<Results<Ok<Product>, NotFound, ValidationProblem>> GetProduct(
Guid id,
AppDbContext db,
CancellationToken ct)
{
var product = await db.Products.FindAsync([id], ct);
return product is not null
? TypedResults.Ok(product)
: TypedResults.NotFound();
}
Parameter Binding
.NET 10 minimal APIs bind parameters from route, query, header, body, and DI automatically.
// Route parameters
app.MapGet("/orders/{id:guid}", (Guid id) => ...);
// Query parameters (nullable = optional)
app.MapGet("/orders", (int page, int? pageSize, string? status) => ...);
// Complex query parameters with [AsParameters]
public record ListOrdersQuery(int Page = 1, int PageSize = 20, string? Status = null);
app.MapGet("/orders", ([AsParameters] ListOrdersQuery query) => ...);
// Header binding
app.MapGet("/orders", ([FromHeader(Name = "X-Correlation-Id")] string? correlationId) => ...);
// DI services are auto-resolved (no attribute needed)
app.MapPost("/orders", (CreateOrderRequest request, ISender sender) => ...);
Endpoint Filters
Filters are the minimal API equivalent of action filters. Use them for cross-cutting concerns like validation, logging, and idempotency checks.
The canonical ValidationFilter<TRequest> implementation (FluentValidation, resolves the validator from DI and skips gracefully when none is registered) lives in the error-handling skill — use that one, don't re-implement it per project.
// Apply the canonical filter (see error-handling skill) to a mutating endpoint
group.MapPost("/", CreateOrder)
.AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();
// Apply a filter to a group (affects all endpoints in the group)
group.AddEndpointFilter<LoggingFilter>();
OpenAPI / Swagger Configuration
.NET 10 has built-in OpenAPI support. Use it instead of Swashbuckle.
// Program.cs — service registration only, no endpoint wiring
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.MapEndpoints(); // auto-discovers all IEndpointGroup implementations
// Endpoint metadata enriches the OpenAPI spec
group.MapPost("/", CreateOrder)
.WithName("CreateOrder")
.WithSummary("Create a new order")
.WithDescription("Creates a new order for the specified customer with the given line items.")
.Produces<OrderResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.ProducesProblem(StatusCodes.Status500InternalServerError);
Rate Limiting
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
});
});
// Apply inside an IEndpointGroup.Map method
var group = app.MapGroup("/api/orders")
.WithTags("Orders")
.RequireRateLimiting("api");
Output Caching
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromMinutes(5)));
options.AddPolicy("ByIdCache", builder => builder
.Expire(TimeSpan.FromMinutes(10))
.SetVaryByRouteValue("id"));
});
group.MapGet("/{id:guid}", GetOrder)
.CacheOutput("ByIdCache");
Anti-patterns
Don't Put Endpoints in Program.cs
// BAD — endpoints scattered in Program.cs
app.MapGet("/orders", async (AppDbContext db) => await db.Orders.ToListAsync());
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) => await db.Orders.FindAsync(id));
app.MapPost("/orders", async (Order order, AppDbContext db) => { /* ... */ });
app.MapGet("/products", async (AppDbContext db) => await db.Products.ToListAsync());
// ALSO BAD — manual MapGroup calls in Program.cs (grows with every feature)
app.MapGroup("/api/orders").WithTags("Orders").MapOrderEndpoints();
app.MapGroup("/api/products").WithTags("Products").MapProductEndpoints();
app.MapGroup("/api/customers").WithTags("Customers").MapCustomerEndpoints();
// Program.cs grows every time you add a feature...
// GOOD — auto-discovered, Program.cs never changes
app.MapEndpoints(); // discovers all IEndpointGroup implementations
Don't Use Untyped Results
// BAD — Results.Ok doesn't contribute to OpenAPI schema
private static async Task<IResult> GetOrder(Guid id, AppDbContext db)
{
var order = await db.Orders.FindAsync(id);
return order is not null ? Results.Ok(order) : Results.NotFound();
}
// GOOD — TypedResults with explicit union type
private static async Task<Results<Ok<Order>, NotFound>> GetOrder(Guid id, AppDbContext db)
{
var order = await db.Orders.FindAsync(id);
return order is not null ? TypedResults.Ok(order) : TypedResults.NotFound();
}
Don't Return Domain Entities Directly
// BAD — leaks internal structure, can't evolve independently
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) =>
await db.Orders.Include(o => o.Items).FirstOrDefaultAsync(o => o.Id == id));
// GOOD — map to a response DTO
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) =>
{
var order = await db.Orders
.Where(o => o.Id == id)
.Select(o => new OrderResponse(o.Id, o.Total, o.CreatedAt))
.FirstOrDefaultAsync();
return order is not null ? TypedResults.Ok(order) : TypedResults.NotFound();
});
Decision Guide
| Scenario | Recommendation |
|---|---|
| New HTTP API | IEndpointGroup per feature + app.MapEndpoints() auto-discovery |
| Existing MVC project | Keep controllers, migrate incrementally |
| OpenAPI documentation | Use TypedResults + .WithName() + .WithSummary() |
| Request validation | Endpoint filter with FluentValidation |
| Authentication/authorization | .RequireAuthorization("PolicyName") on group or endpoint |
| Rate limiting | AddRateLimiter + .RequireRateLimiting() |
| Response caching | AddOutputCache + .CacheOutput() |
| Complex model binding | [AsParameters] with a record type |
Related skills
More from codewithmukesh/dotnet-claude-kit and the wider catalog.

modern-csharp
Modern C# 14 and .NET 10 language features: primary constructors, records, pattern matching, spans, and the field keyword.

openapi
Built-in OpenAPI support for .NET 10 with document generation, transformers, and security schemes—no Swashbuckle needed.

opentelemetry
Distributed tracing, metrics, and logs for .NET 10 with OpenTelemetry and OTLP export.

outdated
Audit .NET NuGet packages for vulnerabilities, staleness, and commercial-license traps.

plan
Architecture-aware planning for .NET projects before implementation.

project-setup
Tech-stack selection advisor for .NET projects with recommended defaults and rationale.