# Minimal APIs in .NET: Complete Guide

> Minimal APIs are a lightweight way to build HTTP APIs in .NET without controllers, startup classes, or conventions. Here is a complete guide covering routing, validation, authentication, and structuring Minimal APIs at scale.

Published: 2026-07-04. Last updated: 2026-07-14. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/minimal-apis-dotnet

Minimal APIs are a strong default for a new .NET API.
An endpoint is a route and a lambda: no controllers, no attributes, no base classes.
But minimal doesn't mean toy. Routing, validation, authentication, filters, and OpenAPI are all covered.
This guide walks the full surface, from your first `MapGet` to structuring hundreds of endpoints.

## What Are Minimal APIs?

Minimal APIs are a lightweight way to build HTTP APIs in .NET without controllers, startup classes, or conventions.
They were introduced in .NET 6 as an alternative to controller-based APIs, and they let you define endpoints directly on the `WebApplication` object with simple lambda expressions:

```csharp
var app = builder.Build();

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

app.Run();
```

No controllers. No startup class. No conventions to learn.

For simple APIs, this is all you need. For complex APIs, you'll want to organize things - and I'll show you how.

## Basic Operations

### GET Endpoints

```csharp
app.MapGet("/api/orders", async (AppDbContext dbContext) =>
{
    var orders = await dbContext.Orders
        .Select(o => new OrderResponse(o.Id, o.Status, o.TotalAmount))
        .ToListAsync();

    return Results.Ok(orders);
});

app.MapGet("/api/orders/{id:guid}", async (
    Guid id,
    AppDbContext dbContext) =>
{
    var order = await dbContext.Orders.FindAsync(id);

    return order is not null
        ? Results.Ok(order)
        : Results.NotFound();
});
```

### POST Endpoints

```csharp
app.MapPost("/api/orders", async (
    CreateOrderRequest request,
    ICommandHandler<CreateOrderCommand, Guid> handler,
    CancellationToken cancellationToken) =>
{
    var command = new CreateOrderCommand(request.CustomerId, request.Items);
    var result = await handler.Handle(command, cancellationToken);

    return result.IsSuccess
        ? Results.Created($"/api/orders/{result.Value}", result.Value)
        : result.ToProblemDetails();
});
```

Three types in this snippet are mine, not ASP.NET Core's, so here's the decoder:

- `ICommandHandler<TCommand, TResponse>` (and its `IQueryHandler` sibling) is a hand-rolled [**CQRS**](https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start) handler interface: one class per use case, one `Handle` method.
- `Result` is a small type that carries either a value or an error, instead of throwing exceptions for expected failures. I cover it in [**functional error handling with the Result pattern**](https://milanjovanovic.tech/blog/functional-error-handling-in-dotnet-with-the-result-pattern).
- `ToProblemDetails()` is an extension method that maps a failed `Result` to a standard [**Problem Details**](https://milanjovanovic.tech/blog/problem-details-for-aspnetcore-apis) error response.

You'll see the same trio in the rest of the examples.

### PUT and DELETE

```csharp
app.MapPut("/api/orders/{id:guid}", async (
    Guid id,
    UpdateOrderRequest request,
    ICommandHandler<UpdateOrderCommand> handler,
    CancellationToken cancellationToken) =>
{
    var command = new UpdateOrderCommand(id, request.Status);
    var result = await handler.Handle(command, cancellationToken);

    return result.IsSuccess
        ? Results.NoContent()
        : result.ToProblemDetails();
});

app.MapDelete("/api/orders/{id:guid}", async (
    Guid id,
    ICommandHandler<DeleteOrderCommand> handler,
    CancellationToken cancellationToken) =>
{
    var command = new DeleteOrderCommand(id);
    var result = await handler.Handle(command, cancellationToken);

    return result.IsSuccess
        ? Results.NoContent()
        : result.ToProblemDetails();
});
```

## Parameter Binding

Minimal APIs bind parameters automatically from:

- **Route** - `{id:guid}` → `Guid id`
- **Query string** - `?page=1&size=10` → `int page, int size`
- **Body** - JSON body → typed object (for POST/PUT)
- **Services** - registered DI services
- **Special types** - `HttpContext`, `CancellationToken`, `ClaimsPrincipal`

```csharp
app.MapGet("/api/orders", async (
    [FromQuery] int page,                    // Query string
    [FromQuery] int pageSize,                // Query string
    [FromHeader(Name = "X-Tenant-Id")] string tenantId,  // Header
    AppDbContext dbContext,                    // DI service
    CancellationToken cancellationToken) =>   // Special type
{
    // ...
});
```

## Validation

Minimal APIs historically had no built-in model validation like controllers with `[ApiController]`.
.NET 10 changes that with built-in data annotations validation, but for complex rules the **endpoint filter** approach with FluentValidation remains the workhorse:

### Endpoint Filter Approach

```csharp
public class ValidationFilter<T> : IEndpointFilter where T : class
{
    private readonly IValidator<T> _validator;

    public ValidationFilter(IValidator<T> validator)
    {
        _validator = validator;
    }

    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var request = context.Arguments
            .OfType<T>()
            .FirstOrDefault();

        if (request is null)
        {
            return Results.BadRequest("Invalid request body.");
        }

        var validationResult = await _validator.ValidateAsync(request);

        if (!validationResult.IsValid)
        {
            return Results.ValidationProblem(
                validationResult.ToDictionary());
        }

        return await next(context);
    }
}
```

Usage:

```csharp
app.MapPost("/api/orders", async (CreateOrderRequest request, ...) => { ... })
   .AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();
```

## Authentication and Authorization

Minimal APIs use the standard ASP.NET Core auth pipeline.
Register the services first (the JWT bearer handler lives in the `Microsoft.AspNetCore.Authentication.JwtBearer` package), then add the middleware:

```csharp
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = builder.Configuration["Authentication:Authority"];
        options.Audience = builder.Configuration["Authentication:Audience"];
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminPolicy", policy => policy.RequireRole("admin"));
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
```

With that in place, protecting an endpoint is one method call:

```csharp
app.MapGet("/api/orders", async (...) => { ... })
   .RequireAuthorization();

app.MapDelete("/api/orders/{id:guid}", async (...) => { ... })
   .RequireAuthorization("AdminPolicy");

app.MapGet("/api/public/health", () => Results.Ok("Healthy"))
   .AllowAnonymous();
```

## Organizing Endpoints at Scale

For anything beyond a demo, you need structure. The key pattern is **endpoint grouping** with extension methods (I walk through the options in [**how to structure minimal APIs**](https://milanjovanovic.tech/blog/how-to-structure-minimal-apis)):

```csharp
public static class OrderEndpoints
{
    public static void MapOrderEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/orders")
            .WithTags("Orders")
            .RequireAuthorization();

        group.MapGet("/", GetOrders);
        group.MapGet("/{id:guid}", GetOrderById);
        group.MapPost("/", CreateOrder);
        group.MapPut("/{id:guid}", UpdateOrder);
        group.MapDelete("/{id:guid}", DeleteOrder);
    }

    private static async Task<IResult> GetOrders(
        AppDbContext dbContext,
        CancellationToken cancellationToken)
    {
        var orders = await dbContext.Orders
            .Select(o => new OrderResponse(o.Id, o.Status, o.TotalAmount))
            .ToListAsync(cancellationToken);

        return Results.Ok(orders);
    }

    private static async Task<IResult> GetOrderById(
        Guid id,
        IQueryHandler<GetOrderByIdQuery, OrderResponse> handler,
        CancellationToken cancellationToken)
    {
        var result = await handler.Handle(new GetOrderByIdQuery(id), cancellationToken);

        return result.IsSuccess
            ? Results.Ok(result.Value)
            : Results.NotFound();
    }

    // ... other handlers
}
```

Register in `Program.cs`:

```csharp
app.MapOrderEndpoints();
app.MapCustomerEndpoints();
app.MapProductEndpoints();
```

Route groups let you apply shared configuration (authorization, tags, filters) once for all endpoints in the group.

## OpenAPI Support

Since .NET 9, OpenAPI document generation is built in (the templates no longer ship Swashbuckle):

```csharp
builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi(); // serves /openapi/v1.json
```

Describe endpoints with metadata so the generated document is actually useful:

```csharp
app.MapPost("/api/orders", async (...) => { ... })
   .WithName("CreateOrder")
   .WithDescription("Places a new order for a customer")
   .Produces<Guid>(StatusCodes.Status201Created)
   .ProducesProblem(StatusCodes.Status400BadRequest)
   .ProducesProblem(StatusCodes.Status404NotFound);
```

## Minimal APIs + Clean Architecture

Minimal APIs work well with [**Clean Architecture**](https://milanjovanovic.tech/blog/clean-architecture-and-the-benefits-of-structured-software-design). Your endpoints are thin - they map HTTP requests to commands/queries and return the result:

```csharp
group.MapPost("/", async (
    CreateOrderRequest request,
    ICommandHandler<CreateOrderCommand, Guid> handler,
    CancellationToken cancellationToken) =>
{
    var command = new CreateOrderCommand(request.CustomerId, request.Items);
    var result = await handler.Handle(command, cancellationToken);

    return result.IsSuccess
        ? Results.Created($"/api/orders/{result.Value}", result.Value)
        : result.ToProblemDetails();
});
```

The endpoint does three things:

![Request flow through a minimal API endpoint: HTTP request maps to a command or query, the handler runs the business logic, and the result maps back to an HTTP response](https://milanjovanovic.tech/blogs/articles/minimal-apis-dotnet/minimal-api-request-flow.png)

1. Maps the HTTP request to a command
2. Delegates to the handler
3. Maps the result to an HTTP response

No business logic. No database access. Just HTTP translation.

## When to Use Minimal APIs vs Controllers

**Minimal APIs when:**

- Building new APIs in .NET 6+
- You want less ceremony and faster startup
- Your endpoints follow simple patterns
- You're using [**CQRS**](https://milanjovanovic.tech/blog/cqrs-pattern-the-way-it-should-have-been-from-the-start) (endpoints are naturally thin)

**Controllers when:**

- You have an existing controller-based API and migration isn't worth it
- You need features that have better controller support (not common anymore)
- Your team is more familiar with the MVC pattern

For new projects, I default to Minimal APIs.
If you're weighing the decision seriously, I wrote a dedicated comparison: **Minimal APIs vs controllers**.

## Summary

**Keep endpoints focused on HTTP translation, with no business logic inside them.**
Minimal APIs are clean, lightweight, and production-ready. Organize them with extension methods and route groups. Use endpoint filters for cross-cutting concerns. Combine with CQRS handlers for maximum simplicity.

Thanks for reading, and stay awesome!

---

## Frequently asked questions

### What are Minimal APIs in .NET?

Minimal APIs, introduced in .NET 6, let you define HTTP endpoints directly on the WebApplication object with lambda expressions or method references, without controllers or attribute-based conventions. They support routing, model binding, DI, auth, filters, and OpenAPI.

### Are Minimal APIs production-ready?

Yes. Since .NET 7-8 they cover the full feature set most APIs need (filters, route groups, auth, rate limiting, OpenAPI) and have lower overhead than controllers. Many new production services use them exclusively.

### How do I validate requests in Minimal APIs?

Use an endpoint filter that runs a FluentValidation validator against the bound request object, or on .NET 10 use the built-in data annotations validation. Controllers' automatic ModelState validation does not apply to minimal endpoints.

### How do I organize Minimal APIs in a large project?

Group endpoints by feature into static classes with extension methods, and use MapGroup to share route prefixes, tags, authorization, and filters. This keeps Program.cs to a handful of registration calls.
