Minimal APIs Deep Dive

Minimal APIs Deep Dive

Markus Lehmann ยท November 18, 2025
ASP.NET CoreMinimal APIsREST
.NETTutorials

Minimal APIs were introduced in .NET 6 as a simpler alternative to controllers. Since then, they've matured significantly. With .NET 10, they've become a viable option for production applications. Let's explore the advanced patterns that make them truly practical.

Beyond the Basics

Most Minimal API examples show trivial endpoints:

app.MapGet("/hello", () => "Hello, World!");

That's fine for demos, but real applications need validation, error handling, authorization, and proper separation of concerns. Here's how to achieve that.

Route Groups for Organization

Instead of cluttering Program.cs with dozens of endpoint definitions, use route groups and extension methods:

// Program.cs
app.MapGroup("/api/books")
   .MapBookEndpoints()
   .RequireAuthorization();

// BookEndpoints.cs
public static class BookEndpoints
{
    public static RouteGroupBuilder MapBookEndpoints(this RouteGroupBuilder group)
    {
        group.MapGet("/", GetAll);
        group.MapGet("/{id:guid}", GetById);
        group.MapPost("/", Create);
        group.MapPut("/{id:guid}", Update);
        group.MapDelete("/{id:guid}", Delete);
        return group;
    }

    private static async Task<IResult> GetAll(
        IBookService bookService,
        CancellationToken ct)
    {
        var books = await bookService.GetAllAsync(ct);
        return TypedResults.Ok(books);
    }

    private static async Task<IResult> GetById(
        Guid id,
        IBookService bookService,
        CancellationToken ct)
    {
        var book = await bookService.GetByIdAsync(id, ct);
        return book is not null
            ? TypedResults.Ok(book)
            : TypedResults.NotFound();
    }
}

This gives you the same organization as controllers without the ceremony of class inheritance and attribute routing.

Validation with Endpoint Filters

Minimal APIs support filters, which are analogous to action filters in MVC. Use them for validation:

public class ValidationFilter<T> : IEndpointFilter where T : class
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var model = context.Arguments.OfType<T>().FirstOrDefault();
        if (model is null)
            return TypedResults.BadRequest("Request body is required");

        var validator = context.HttpContext.RequestServices
            .GetService<IValidator<T>>();

        if (validator is not null)
        {
            var result = await validator.ValidateAsync(model);
            if (!result.IsValid)
                return TypedResults.ValidationProblem(result.ToDictionary());
        }

        return await next(context);
    }
}

Apply it to specific endpoints:

group.MapPost("/", Create)
     .AddEndpointFilter<ValidationFilter<CreateBookRequest>>();

TypedResults for Better OpenAPI

Using TypedResults instead of Results gives you proper OpenAPI documentation without additional attributes:

private static async Task<Results<Ok<BookDto>, NotFound, ValidationProblem>> GetById(
    Guid id, IBookService bookService)
{
    var book = await bookService.GetByIdAsync(id);
    return book is not null
        ? TypedResults.Ok(book)
        : TypedResults.NotFound();
}

The Results<T1, T2, T3> return type tells the OpenAPI generator exactly which status codes and response types this endpoint can produce.

Structured Error Handling

Wrap your endpoints with consistent error handling using a global exception handler:

app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        context.Response.ContentType = "application/problem+json";
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;

        var problem = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "An unexpected error occurred",
            Instance = context.Request.Path
        };

        context.Response.StatusCode = problem.Status.Value;
        await context.Response.WriteAsJsonAsync(problem);
    });
});

Dependency Injection in Parameters

One of the strengths of Minimal APIs is how naturally DI integrates with endpoint parameters. Services are injected directly as method parameters:

group.MapPost("/", async (
    CreateBookRequest request,
    IBookService bookService,
    ILogger<BookEndpoints> logger,
    ClaimsPrincipal user,
    CancellationToken ct) =>
{
    var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
    logger.LogInformation("User {UserId} creating book", userId);
    var book = await bookService.CreateAsync(request, userId!, ct);
    return TypedResults.Created($"/api/books/{book.Id}", book);
});

No constructor injection, no field declarations. The DI container resolves parameters automatically based on their types.

When to Choose Minimal APIs

Minimal APIs shine for microservices, simple CRUD APIs, and projects where you want full control over the request pipeline. For large applications with dozens of related endpoints, controllers still offer better organization through inheritance and base controller patterns. The choice isn't binary though. You can mix controllers and Minimal APIs in the same project and use each where they fit best.


Comments (2)

Hannah Mar 06, 2026 · 17:08

I have a question: does this also apply to older versions?

Ben Feb 22, 2026 · 20:08

Thanks for your comment — glad it helped!

Clara Feb 23, 2026 · 20:08

I had the same experience, can confirm.

Anna Mar 07, 2026 · 17:08

Thanks for sharing — very helpful.


Leave a comment

An unhandled error has occurred. Reload ๐Ÿ—™

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.