Skip to content
ExponaBLOG
.NET & ASP.NET CORE · DAY 002 / 100

Minimal APIs: a production-ready setup, explained simply

Minimal APIs are not just for demos. This is how I structure them for real services.

Expona Admin7 min read

Opening: Why Minimal APIs Deserve Your Production Workload

When Minimal APIs landed in .NET 6, many dismissed them as a lightweight demo framework for hobbyists. I made that mistake too. Three years and five production services later, I've learned that Minimal APIs: a production-ready setup is not just viable—it's often the smarter choice for microservices, feature APIs, and teams that value velocity without sacrificing quality.

The misconception lingers because early examples showed trivial endpoints in Program.cs. But that's like judging ASP.NET Core Controllers by a hello-world sample. Done right, Minimal APIs give you the same DI container, middleware pipeline, and hosting flexibility as traditional controllers, with less ceremony and better discoverability for small-to-medium teams.

Start Lean — A Minimal API Is a Full ASP.NET Core App

The first thing to understand: there's no trade-off in capability. When you create a Minimal API, you're building a genuine ASP.NET Core application. You get the exact same dependency injection, middleware, and hosting infrastructure. The difference is syntax, not substance.

Here's the absolute foundation:

csharp
var builder = WebApplication.CreateBuilder(args);

// Full DI container, just like controllers
builder.Services.AddScoped<OrderService>();
builder.Services.AddProblemDetails();
builder.Services.AddAuthentication().AddJwtBearer(...);

var app = builder.Build();

// Full middleware pipeline
app.UseAuthentication();
app.UseAuthorization();
app.UseExceptionHandler();

// Endpoints
app.MapGet("/health", () => Results.Ok(new { status = "healthy" }));

app.Run();

I've seen teams hesitate here, thinking Minimal APIs are "lightweight" in a limiting sense. Wrong. Add AddProblemDetails(), wire up authentication, inject services—it's all there. The "minimal" refers to boilerplate, not power.

In production, I always include:

  • AddProblemDetails() — RFC 7807 problem responses, consistent error handling
  • Structured logging with Serilog or similar
  • Health check endpoints for load balancers
  • OpenAPI/Swagger generation (more on that later)

A real health check endpoint I use:

csharp
app.MapGet("/health", async (IHealthCheckService health) =>
{
    var result = await health.CheckHealthAsync();
    return result.Status == HealthStatus.Healthy
        ? Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow })
        : Results.ServiceUnavailable(new { status = "unhealthy" });
})
.WithName("HealthCheck")
.WithOpenApi()
.AllowAnonymous();

That single endpoint is production-grade: dependency injected, async, problem-aware, and documented for OpenAPI.

Group Your Routes — Shared Prefixes, Filters, and Auth Per Feature

Once you have more than three endpoints, flat mapping in Program.cs becomes a nightmare. Route groups are the solution, and they're more powerful than they first appear.

A route group lets you:

  • Share a URL prefix across related endpoints
  • Apply authorization, tags, and metadata once
  • Keep related logic visibly grouped

Here's how I structure an Orders feature:

csharp
var orders = app.MapGroup("/api/orders")
    .RequireAuthorization()
    .RequireAuthorization("AdminOnly")  // Custom policy
    .WithTags("Orders")
    .WithOpenApi();

orders.MapGet("/", GetOrders)
    .WithName("ListOrders")
    .WithDescription("Fetch paginated orders");

orders.MapGet("/{id}", GetOrder)
    .WithName("GetOrder")
    .WithDescription("Fetch a single order by ID");

orders.MapPost("/", CreateOrder)
    .WithName("CreateOrder")
    .AllowAnonymous();  // Override group auth if needed

The beauty here: I've applied RequireAuthorization("AdminOnly") once, and all four endpoints inherit it (except the override on CreateOrder). If I need to add a custom header validation or logging filter, I do it once for the group.

Real-world example from a ride-hailing service I built: I had driver endpoints and passenger endpoints under different auth policies. Groups made it trivial:

csharp
app.MapGroup("/api/drivers")
    .RequireAuthorization("DriverRole")
    .MapDriverEndpoints();

app.MapGroup("/api/passengers")
    .RequireAuthorization("PassengerRole")
    .MapPassengerEndpoints();

Each group enforces its policy without duplicating authorization attributes on every handler.

Typed Results — Explicit Responses That Feed OpenAPI Automatically

This is where Minimal APIs shine compared to ad-hoc REST APIs. TypedResults make your response contracts explicit and automatically populate your OpenAPI schema.

Without typed results, you might write:

csharp
app.MapGet("/api/orders/{id}", async (int id, AppDb db) =>
{
    var order = await db.Orders.FindAsync(id);
    if (order == null) return Results.NotFound();
    return Results.Ok(order.ToDto());
});

The issue: the method signature says nothing about what responses are possible. Your OpenAPI doc might miss the 404. Consumers reading the code are guessing.

With TypedResults, be explicit:

csharp
static async Task<Results<Ok<OrderDto>, NotFound>> GetOrder(
    int id,
    AppDb db)
{
    var order = await db.Orders.FindAsync(id);
    return order is null
        ? TypedResults.NotFound()
        : TypedResults.Ok(order.ToDto());
}

app.MapGet("/api/orders/{id}", GetOrder)
    .WithName("GetOrder")
    .Produces<OrderDto>(200)
    .Produces(404);

Now the return type documents every response. OpenAPI sees Results<Ok<OrderDto>, NotFound> and generates correct schemas. Consumers know exactly what to expect. Your IDE can even help you handle all cases.

In a CRM system I built, this prevented dozens of API integration bugs. Frontend developers could see response types upfront instead of discovering them through trial and error.

One File Per Feature — Extension Methods Keep Program.cs Readable

Here's the dirty secret of Minimal APIs: if you map all endpoints directly in Program.cs, you'll hit 500+ lines and regret your life choices. The solution is simple: extension methods.

Create a Features/Orders/OrderEndpoints.cs file:

csharp
namespace MyApi.Features.Orders;

public static class OrderEndpoints
{
    public static void MapOrderEndpoints(this WebApplication app)
    {
        var group = app.MapGroup("/api/orders")
            .WithTags("Orders")
            .WithOpenApi();

        group.MapGet("/", GetOrders)
            .WithName("ListOrders");
        
        group.MapGet("/{id}", GetOrder)
            .WithName("GetOrder");
        
        group.MapPost("/", CreateOrder)
            .WithName("CreateOrder");
        
        group.MapPut("/{id}", UpdateOrder)
            .WithName("UpdateOrder");
        
        group.MapDelete("/{id}", DeleteOrder)
            .WithName("DeleteOrder");
    }

    private static async Task<Ok<List<OrderDto>>> GetOrders(OrderService svc) =>
        TypedResults.Ok(await svc.ListAsync());

    private static async Task<Results<Ok<OrderDto>, NotFound>> GetOrder(
        int id, OrderService svc) =>
        await svc.GetAsync(id) is { } dto
            ? TypedResults.Ok(dto)
            : TypedResults.NotFound();

    // ... other handlers
}

Then in Program.cs, one line per feature:

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<OrderService>();
builder.Services.AddScoped<InvoiceService>();
// ... DI setup

var app = builder.Build();

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

// Map features
app.MapOrderEndpoints();
app.MapInvoiceEndpoints();
app.MapCustomerEndpoints();

app.Run();

Program.cs stays clean at 30 lines, and each feature is isolated. I've done this pattern on APIs with 50+ endpoints without it becoming a maintenance nightmare. Compare that to a Controllers-based API where you'd have 15 controller files—this is actually simpler.

When to Pick Controllers — There Are Still Valid Reasons

I'll be honest: Minimal APIs aren't always the answer. Controllers remain first-class citizens and shine in specific scenarios:

  • Massive APIs with heavy conventions: If you have 200+ endpoints with consistent filters, model binding, and versioning, controllers' attribute-based model feels natural.
  • Complex model binding: Controllers handle form data, file uploads, and binding from multiple sources more elegantly with attributes.
  • Filter pipelines: Authorization filters, validation filters, and result filters are more discoverable in controller syntax if you have many of them.
  • Team expertise: If your team thinks in MVC patterns and minimal APIs feel alien, the cognitive overhead isn't worth it.

I still use controllers for legacy systems and massive B2B APIs where 500 endpoints with versioning, tenancy, and fine-grained permissions are the norm. But for microservices, feature APIs, and anything under 100 endpoints? Minimal APIs win on velocity and clarity.

Common Mistakes

Mistake 1: Putting all endpoint logic in Program.cs

I've seen teams do this and regret it after 20 endpoints. Use extension methods from day one. The cost is near-zero; the benefit compounds.

Mistake 2: Forgetting to use TypedResults

If you return Results.Ok(data) from some endpoints and data from others, your OpenAPI schema becomes a mess. Pick one style per project. I always use TypedResults with the explicit return type.

Mistake 3: Not adding OpenAPI metadata early

It's tempting to skip .WithName(), .WithDescription(), and .Produces() to move fast. Don't. Add them as you go. Frontend teams depend on accurate docs, and fixing them later is tedious.

Mistake 4: Ignoring exception handling

Minimal APIs don't magically handle exceptions. You still need app.UseExceptionHandler() and problem details. Wire it up in your lean start, not later.

Mistake 5: Coupling handlers to Program.cs structure

If your handler functions reference variables or services from Program.cs in nonstandard ways, refactoring becomes hard. Keep handlers as static methods or isolated classes that only depend on their parameters. Inject everything they need.

Key Takeaways

  • Minimal APIs are full ASP.NET Core applications—same DI, middleware, and hosting as controllers. The "minimal" refers to boilerplate, not capability.
  • Start lean: include AddProblemDetails(), authentication, and health checks from the beginning. Don't confuse lean with incomplete.
  • Use route groups to avoid repeating authorization, tags, and prefixes. One RequireAuthorization() per group, not per endpoint.
  • Always use TypedResults with explicit return types. This documents responses and generates accurate OpenAPI schemas automatically.
  • Extract endpoint handlers to extension methods in feature-organized files. Keep Program.cs as a readable map of your API structure.
  • Controllers and Minimal APIs coexist. Pick Minimal APIs for microservices and feature APIs under 100 endpoints; controllers still win for massive legacy systems with heavy conventions.
  • Avoid five common mistakes: dumping code in Program.cs, mixing result styles, skipping OpenAPI metadata, ignoring exception handling, and tight coupling between handlers and configuration.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles