dotnet-minimal-apis

SKILL.md

dotnet-minimal-apis

Minimal APIs are Microsoft's recommended approach for new ASP.NET Core HTTP API projects. They provide a lightweight, lambda-based programming model with first-class OpenAPI support, endpoint filters for cross-cutting concerns, and route groups for organization at scale.

Scope

  • Route groups and endpoint organization
  • Endpoint filters for cross-cutting concerns
  • TypedResults for compile-time response type safety
  • Parameter binding (route, query, body, services)
  • JSON configuration with ConfigureHttpJsonOptions
  • Carter library integration for auto-discovery modules

Out of scope

  • API versioning strategies -- see [skill:dotnet-api-versioning]
  • Input validation frameworks -- see [skill:dotnet-input-validation]
  • Architectural patterns (vertical slices, CQRS) -- see [skill:dotnet-architecture-patterns]
  • Authentication and authorization -- see [skill:dotnet-api-security]
  • OpenAPI document generation -- see [skill:dotnet-openapi]
  • gRPC and real-time communication -- see [skill:dotnet-grpc] and [skill:dotnet-realtime-communication]

Cross-references: [skill:dotnet-architecture-patterns] for organizing large APIs, [skill:dotnet-input-validation] for request validation, [skill:dotnet-api-versioning] for versioning strategies, [skill:dotnet-openapi] for OpenAPI customization.


Route Groups

Route groups organize related endpoints under a shared prefix, applying common configuration (filters, metadata, authorization) once. They replace repetitive chaining of MapGet/MapPost with shared prefixes.


var app = builder.Build();

// Group endpoints under /api/products with shared configuration
var products = app.MapGroup("/api/products")
    .WithTags("Products")
    .RequireAuthorization();

products.MapGet("/", async (AppDbContext db) =>
    TypedResults.Ok(await db.Products.ToListAsync()));

products.MapGet("/{id:int}", async (int id, AppDbContext db) =>
    await db.Products.FindAsync(id) is Product product
        ? TypedResults.Ok(product)
        : TypedResults.NotFound());

products.MapPost("/", async (CreateProductDto dto, AppDbContext db) =>
{
    var product = new Product { Name = dto.Name, Price = dto.Price };
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return TypedResults.Created($"/api/products/{product.Id}", product);
});

products.MapDelete("/{id:int}", async (int id, AppDbContext db) =>
{
    if (await db.Products.FindAsync(id) is not Product product)
        return TypedResults.NotFound();

    db.Products.Remove(product);
    await db.SaveChangesAsync();
    return TypedResults.NoContent();
});

```text

### Nested Groups

Groups can be nested to compose prefixes and filters:

```csharp

var api = app.MapGroup("/api")
    .AddEndpointFilter<RequestLoggingFilter>();

var v1 = api.MapGroup("/v1");
var products = v1.MapGroup("/products").WithTags("Products");
var orders = v1.MapGroup("/orders").WithTags("Orders");

// Registers as: GET /api/v1/products
products.MapGet("/", GetProducts);
// Registers as: POST /api/v1/orders
orders.MapPost("/", CreateOrder);

```text

---

## Endpoint Filters

Endpoint filters provide a pipeline for cross-cutting concerns (logging, validation, authorization enrichment) similar
to MVC action filters but specific to Minimal APIs.

### IEndpointFilter Interface

```csharp

public sealed class ValidationFilter<T>(IValidator<T> validator) : IEndpointFilter
    where T : class
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        // Extract the argument of type T from the endpoint parameters
        var argument = context.Arguments
            .OfType<T>()
            .FirstOrDefault();

        if (argument is null)
            return TypedResults.BadRequest("Request body is required");

        var result = await validator.ValidateAsync(argument);
        if (!result.IsValid)
        {
            return TypedResults.ValidationProblem(
                result.ToDictionary());
        }

        return await next(context);
    }
}

```text

### Applying Filters

```csharp

// Apply to a single endpoint
products.MapPost("/", CreateProduct)
    .AddEndpointFilter<ValidationFilter<CreateProductDto>>();

// Apply to an entire route group
var products = app.MapGroup("/api/products")
    .AddEndpointFilter<RequestLoggingFilter>();

// Inline filter using a lambda
products.MapGet("/{id:int}", GetProductById)
    .AddEndpointFilter(async (context, next) =>
    {
        var id = context.GetArgument<int>(0);
        if (id <= 0)
            return TypedResults.BadRequest("ID must be positive");

        return await next(context);
    });

```text

### Filter Execution Order

Filters execute in registration order (first registered = outermost). The endpoint handler runs after all filters pass:

```text

Request -> Filter1 -> Filter2 -> Filter3 -> Handler
Response <- Filter1 <- Filter2 <- Filter3 <-

```text

---

## TypedResults

Always use `TypedResults` (static factory) instead of `Results` (interface factory) for Minimal API return values.
`TypedResults` returns concrete types that the OpenAPI metadata generator can inspect at build time, producing accurate
response schemas automatically.

```csharp

// PREFERRED: TypedResults -- concrete return types, auto-generates OpenAPI metadata
products.MapGet("/{id:int}", async Task<Results<Ok<Product>, NotFound>> (
    int id, AppDbContext db) =>
    await db.Products.FindAsync(id) is Product product
        ? TypedResults.Ok(product)
        : TypedResults.NotFound());

// AVOID: Results -- returns IResult, OpenAPI generator cannot infer response types
products.MapGet("/{id:int}", async (int id, AppDbContext db) =>
    await db.Products.FindAsync(id) is Product product
        ? Results.Ok(product)
        : Results.NotFound());

```text

### Union Return Types

Use `Results<T1, T2, ...>` to declare all possible response types for a single endpoint. This enables accurate OpenAPI
documentation with multiple response codes:

```csharp

products.MapPost("/", async Task<Results<Created<Product>, ValidationProblem, Conflict>> (
    CreateProductDto dto, AppDbContext db) =>
{
    if (await db.Products.AnyAsync(p => p.Sku == dto.Sku))
        return TypedResults.Conflict();

    var product = new Product { Name = dto.Name, Sku = dto.Sku, Price = dto.Price };
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return TypedResults.Created($"/api/products/{product.Id}", product);
});

```text

---

## OpenAPI 3.1 Integration

.NET 10 adds built-in OpenAPI 3.1 support via `Microsoft.AspNetCore.OpenApi`. Minimal APIs generate OpenAPI metadata
from `TypedResults`, parameter bindings, and attributes automatically.

```csharp

builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi(); // Serves /openapi/v1.json
}

```json

### Enriching Metadata

```csharp

products.MapGet("/{id:int}", GetProductById)
    .WithName("GetProductById")
    .WithSummary("Get a product by its ID")
    .WithDescription("Returns the product details for the specified ID, or 404 if not found.")
    .Produces<Product>(StatusCodes.Status200OK)
    .ProducesProblem(StatusCodes.Status404NotFound);

```text

For advanced OpenAPI customization (document transformers, operation transformers, schema customization), see
[skill:dotnet-openapi].

---

## Organization Patterns for Scale

As an API grows beyond a handful of endpoints, organize endpoints into separate static classes or extension methods.

### Extension Method Pattern

```csharp

// ProductEndpoints.cs
public static class ProductEndpoints
{
    public static RouteGroupBuilder MapProductEndpoints(this IEndpointRouteBuilder routes)
    {
        var group = routes.MapGroup("/api/products")
            .WithTags("Products");

        group.MapGet("/", GetAll);
        group.MapGet("/{id:int}", GetById);
        group.MapPost("/", Create);
        group.MapPut("/{id:int}", Update);
        group.MapDelete("/{id:int}", Delete);

        return group;
    }

    private static async Task<Ok<List<Product>>> GetAll(AppDbContext db) =>
        TypedResults.Ok(await db.Products.ToListAsync());

    private static async Task<Results<Ok<Product>, NotFound>> GetById(
        int id, AppDbContext db) =>
        await db.Products.FindAsync(id) is Product p
            ? TypedResults.Ok(p)
            : TypedResults.NotFound();

    private static async Task<Created<Product>> Create(
        CreateProductDto dto, AppDbContext db)
    {
        var product = new Product { Name = dto.Name, Price = dto.Price };
        db.Products.Add(product);
        await db.SaveChangesAsync();
        return TypedResults.Created($"/api/products/{product.Id}", product);
    }

    private static async Task<Results<NoContent, NotFound>> Update(
        int id, UpdateProductDto dto, AppDbContext db)
    {
        var product = await db.Products.FindAsync(id);
        if (product is null) return TypedResults.NotFound();

        product.Name = dto.Name;
        product.Price = dto.Price;
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }

    private static async Task<Results<NoContent, NotFound>> Delete(
        int id, AppDbContext db)
    {
        var product = await db.Products.FindAsync(id);
        if (product is null) return TypedResults.NotFound();

        db.Products.Remove(product);
        await db.SaveChangesAsync();
        return TypedResults.NoContent();
    }
}

// Program.cs
app.MapProductEndpoints();
app.MapOrderEndpoints();
app.MapCustomerEndpoints();

```csharp

### Carter Library

For projects that prefer auto-discovery of endpoint modules, the Carter library provides an `ICarterModule` interface:

```csharp

// <PackageReference Include="Carter" Version="8.*" />
public sealed class ProductModule : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/products").WithTags("Products");

        group.MapGet("/", async (AppDbContext db) =>
            TypedResults.Ok(await db.Products.ToListAsync()));

        group.MapGet("/{id:int}", async (int id, AppDbContext db) =>
            await db.Products.FindAsync(id) is Product p
                ? TypedResults.Ok(p)
                : TypedResults.NotFound());
    }
}

// Program.cs
builder.Services.AddCarter();
var app = builder.Build();
app.MapCarter(); // Auto-discovers and registers all ICarterModule implementations

```csharp

### Vertical Slice Organization

For projects using vertical slice architecture (see [skill:dotnet-architecture-patterns]), each feature owns its
endpoints, handlers, and models in a single directory:

```text

Features/
  Products/
    GetProducts.cs       # Endpoint + handler + response DTO
    CreateProduct.cs     # Endpoint + handler + request/response DTOs
    UpdateProduct.cs
    DeleteProduct.cs
    ProductEndpoints.cs  # Route group registration

```csharp

---

## JSON Configuration

Minimal APIs use `System.Text.Json` by default. Configure JSON options globally for all Minimal API endpoints:

```csharp

// ConfigureHttpJsonOptions applies to Minimal APIs ONLY, not MVC controllers
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
    options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});

```json

**Gotcha:** `ConfigureHttpJsonOptions` configures JSON serialization for Minimal APIs only. MVC controllers use a
separate pipeline -- configure via `builder.Services.AddControllers().AddJsonOptions(...)`. Mixing them up has no
effect.

---

## Parameter Binding

Minimal APIs bind parameters from route, query, headers, body, and DI automatically based on type and attribute
annotations.

```csharp

// Route parameter (from URL segment)
app.MapGet("/products/{id:int}", (int id) => ...);

// Query string
app.MapGet("/products", ([FromQuery] int page, [FromQuery] int pageSize) => ...);

// Header
app.MapGet("/products", ([FromHeader(Name = "X-Correlation-Id")] string correlationId) => ...);

// Body (JSON deserialized)
app.MapPost("/products", (CreateProductDto dto) => ...);

// DI-injected services (resolved automatically)
app.MapGet("/products", (AppDbContext db, ILogger<Program> logger) => ...);

// AsParameters: bind a complex object from multiple sources
app.MapGet("/products", ([AsParameters] ProductQuery query) => ...);

public record ProductQuery(
    [FromQuery] int Page = 1,
    [FromQuery] int PageSize = 20,
    [FromQuery] string? SortBy = null);

```text

---

## Agent Gotchas

1. **Do not use `Results` when `TypedResults` is available** -- `Results.Ok(value)` returns `IResult` and the OpenAPI
   generator cannot infer response schemas. Use `TypedResults.Ok(value)` to enable automatic schema generation.
2. **Do not forget `ConfigureHttpJsonOptions` only applies to Minimal APIs** -- MVC controllers need
   `.AddControllers().AddJsonOptions()` separately.
3. **Do not apply validation logic inline in every endpoint** -- use endpoint filters or cross-reference
   [skill:dotnet-input-validation] for centralized validation patterns.
4. **Do not register filters in the wrong order** -- first-registered filter is outermost. Put broad filters (logging)
   first, specific filters (validation) closer to the handler.
5. **Do not put all endpoints in `Program.cs`** -- organize into extension method classes or Carter modules once you
   have more than a handful of endpoints.

---

## Prerequisites

- .NET 8.0+ (LTS baseline for Minimal APIs with route groups and endpoint filters)
- .NET 10.0 for built-in OpenAPI 3.1, SSE, and built-in validation support
- `Microsoft.AspNetCore.OpenApi` for OpenAPI document generation
- `Carter` (optional) for auto-discovery endpoint modules

---

## Knowledge Sources

Minimal API patterns in this skill are grounded in guidance from:

- **David Fowler** -- AspNetCoreDiagnosticScenarios
  ([github.com/davidfowl/AspNetCoreDiagnosticScenarios](https://github.com/davidfowl/AspNetCoreDiagnosticScenarios)).
  Authoritative source on ASP.NET Core request pipeline design, middleware best practices, and diagnostic anti-patterns.

> These sources inform the patterns and rationale presented above. This skill does not claim to represent or speak for
> any individual.

---

## References

- [Minimal APIs Overview](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-10.0)
- [Route Groups](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/route-handlers?view=aspnetcore-10.0#route-groups)
- [Endpoint Filters](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/min-api-filters?view=aspnetcore-10.0)
- [OpenAPI in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview?view=aspnetcore-10.0)
- [Carter Library](https://github.com/CarterCommunity/Carter)
Weekly Installs
1
First Seen
11 days ago
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1