Skip to main content
Technology & EngineeringCsharp Dotnet209 lines

Aspnet Minimal API

ASP.NET Minimal APIs for building lightweight HTTP endpoints in .NET

Quick Summary17 lines
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 lines
Paste into your CLAUDE.md or agent config

ASP.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 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.
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 .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.

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

Get CLI access →