Breaking / LiveLIVE
DEV CommunityBear UI 1.3.5 — charts, AI, code, maps, and kanban now have their own packagesWIREDOil Spills Wreak Havoc on the Bottom of the Ocean, Even a Decade LaterHacker NewsFrance asked ISPs to prepare temporary DNS blocks for major social platformsGoogle DevelopersBring multimodal semantic search to the edge with EmbeddingGemma 2The Hacker NewsOpenAI AI Agents Demonstrate VM Escapes and Secret Cross-Sandbox CommunicationInfoQPingCAP Breach Exposes Customer Records and Source CodeThe VergeBlue Planet III is keeping the rough edges in the age of AITechCrunchStarlink seems to be working normally again in UkraineBleepingComputerBlackX UEFI bootkit exploits Baton Drop flaw to bypass Secure BootArs TechnicaA 1,000-mile cloud on Mars may reveal physics never seen in natureGitHub ChangelogUsage metrics API adds usage from tools like OpenCodeMicrosoft Power PlatformAI that understands your business: Dataverse and Work IQ at PPCC 2026AWS What's NewWeb Search on Amazon Bedrock now supports domain filtering and user location controlsMIT News: AIAt MIT, Martha Minow speaks on sustaining democracy, both by using and overcoming AIDEV CommunityBear UI 1.3.5 — charts, AI, code, maps, and kanban now have their own packagesWIREDOil Spills Wreak Havoc on the Bottom of the Ocean, Even a Decade LaterHacker NewsFrance asked ISPs to prepare temporary DNS blocks for major social platformsGoogle DevelopersBring multimodal semantic search to the edge with EmbeddingGemma 2The Hacker NewsOpenAI AI Agents Demonstrate VM Escapes and Secret Cross-Sandbox CommunicationInfoQPingCAP Breach Exposes Customer Records and Source CodeThe VergeBlue Planet III is keeping the rough edges in the age of AITechCrunchStarlink seems to be working normally again in UkraineBleepingComputerBlackX UEFI bootkit exploits Baton Drop flaw to bypass Secure BootArs TechnicaA 1,000-mile cloud on Mars may reveal physics never seen in natureGitHub ChangelogUsage metrics API adds usage from tools like OpenCodeMicrosoft Power PlatformAI that understands your business: Dataverse and Work IQ at PPCC 2026AWS What's NewWeb Search on Amazon Bedrock now supports domain filtering and user location controlsMIT News: AIAt MIT, Martha Minow speaks on sustaining democracy, both by using and overcoming AI

ASP.NET Core Minimal APIs vs Controllers: An Architect's Guide for 2026

Beyond hello-world: how minimal APIs and controllers compare on structure, validation, filters, OpenAPI, versioning, testing and performance in large production codebases.

By KHR Editorial Team··6 min read
ASP.NET Core Minimal APIs vs Controllers: An Architect's Guide for 2026

Minimal APIs started as a lightweight option for small services. In current ASP.NET Core releases they are a complete programming model with route groups, endpoint filters, typed results, built-in OpenAPI document generation and good support for Native AOT. Controllers remain fully supported and are still the default in many enterprise codebases.

For an architect the question is not "which one works" (both do), but which one leads to a codebase that stays consistent, testable and fast as dozens of developers add hundreds of endpoints. This guide compares them on the dimensions that matter at that scale.

The request pipeline is the same

Both models run on the same host, middleware, routing, dependency injection and serialization. Authentication, CORS, rate limiting, output caching and exception handling are configured once and apply to both.

Where minimal API features plug into the pipeline Minimal APIs add route groups, endpoint filters and typed results on top of the shared pipeline.

The differences are in how endpoints are declared, how cross-cutting behaviour is attached, and how much the framework does implicitly.

Feature comparison

Feature comparison of minimal APIs and controllers ◐ means supported with extra setup or partially.

Route groups vs controller conventions

Route groups let you apply a prefix, tags, authorization, rate limiting and filters to a set of endpoints in one place:

var orders = app.MapGroup("/api/v1/orders")
    .WithTags("Orders")
    .RequireAuthorization("orders.read")
    .AddEndpointFilter<CorrelationFilter>();

orders.MapGet("/", ListOrders);
orders.MapGet("/{id:int}", GetOrder);
orders.MapPost("/", CreateOrder).RequireAuthorization("orders.write");

Controllers achieve similar effects with attributes on the class and conventions, which many teams find familiar but which can scatter policy across files.

Typed results document behaviour

static async Task<Results<Ok<OrderDto>, NotFound>> GetOrder(int id, AppDb db, CancellationToken ct)
{
    var dto = await db.Orders.AsNoTracking()
        .Where(o => o.Id == id)
        .Select(o => new OrderDto(o.Id, o.Number, o.Total))
        .FirstOrDefaultAsync(ct);
    return dto is null ? TypedResults.NotFound() : TypedResults.Ok(dto);
}

The return type tells readers, tests and the OpenAPI generator exactly which responses are possible. Controllers can express the same with ActionResult<T> and [ProducesResponseType], but the information lives in attributes that drift out of date.

Validation: be explicit

Controllers with [ApiController] return a 400 ValidationProblemDetails automatically when data annotations fail. Minimal APIs historically required you to add validation yourself; newer releases add built-in validation support for minimal APIs, but many teams still standardise on an endpoint filter such as:

public sealed class ValidationFilter<T>(IValidator<T> validator) : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext ctx, EndpointFilterDelegate next)
    {
        var arg = ctx.Arguments.OfType<T>().FirstOrDefault();
        if (arg is null) return await next(ctx);
        var result = await validator.ValidateAsync(arg);
        return result.IsValid ? await next(ctx) : TypedResults.ValidationProblem(result.ToDictionary());
    }
}

Whatever you choose, the architectural rule is the same: every endpoint returns the same error shape, ideally RFC 7807 problem details, so clients can handle errors uniformly.

Structuring a large minimal API

The common fear is a 3,000-line Program.cs. The fix is a vertical-slice layout where each feature owns its endpoints, contracts and handlers.

Vertical-slice project layout Program.cs only wires services and calls one extension method per feature.

public static class OrderEndpoints
{
    public static IEndpointRouteBuilder MapOrders(this IEndpointRouteBuilder app)
    {
        var g = app.MapGroup("/api/v1/orders").WithTags("Orders").RequireAuthorization();
        g.MapGet("/", List);
        g.MapGet("/{id:int}", Get);
        g.MapPost("/", Create).AddEndpointFilter<ValidationFilter<CreateOrder>>();
        return app;
    }
    // static handlers below...
}

Static handler methods with explicit parameters are easy to unit test and make dependencies visible, which improves code review.

Versioning

Two approaches work with both models:

  • Route-based groups (/api/v1, /api/v2), simple and very visible to clients.
  • The Asp.Versioning libraries, which support query, header and media-type versioning, version sets and deprecation reporting for both minimal APIs and controllers.

Keep version-specific contracts (DTOs) separate even when the handler logic is shared, so a change for v2 never silently alters v1 responses.

OpenAPI and client generation

Typed results, WithName, WithSummary, Produces and parameter metadata produce precise OpenAPI documents. Recent ASP.NET Core versions include built-in OpenAPI document generation, which you can render with Swagger UI or Scalar and use for client generation. Precise contracts matter most for public and partner APIs; review the generated document in pull requests the same way you review code.

Testing

Use WebApplicationFactory<Program> for integration tests in both models: they exercise routing, filters, serialization and auth exactly as production does. Add unit tests for handlers (minimal APIs) or controller actions only for complex logic. Testcontainers can provide a real SQL Server or PostgreSQL instance so tests catch provider-specific behaviour.

Performance

Minimal APIs avoid some MVC machinery such as model binding providers and action filters, which reduces per-request allocations. In high-throughput services and especially with Native AOT, minimal APIs have a clear advantage, and the AOT story for MVC controllers is limited. In typical line-of-business APIs, database access and network calls dominate latency, so architecture and data access patterns matter far more than the endpoint style.

A decision framework

Context Recommendation
New microservice or API-only project Minimal APIs with route groups and vertical slices
Native AOT, serverless, cold-start sensitive Minimal APIs
Large existing controller codebase Keep controllers; consider minimal APIs for new isolated features
Same project renders Razor views Controllers (MVC)
Team new to ASP.NET Core Either, but write and enforce a short style guide first

Mixing both in one application is supported and often pragmatic during migration. What hurts is inconsistency: two validation approaches, two error shapes, two ways of naming routes.

Cross-cutting concerns that must be identical

Whichever model you choose, a production API needs a small set of behaviours applied uniformly. Write them once, as middleware or group-level conventions, and verify them in integration tests:

  • Error handling. Register AddProblemDetails() and UseExceptionHandler() so unhandled exceptions become RFC 7807 responses with a trace identifier, never stack traces. Map domain exceptions (not found, conflict, forbidden) to status codes in one place.
  • Correlation and logging. Propagate a correlation ID from the incoming request (or Activity.Current) into structured logs and outgoing HTTP calls. OpenTelemetry instrumentation for ASP.NET Core, HttpClient and your database provider gives traces across services with very little code.
  • Authorization. Prefer named policies (orders.read, orders.write) over role checks scattered in handlers. Policies are testable, discoverable and work identically on groups, endpoints and controllers.
  • Rate limiting. The built-in rate limiting middleware supports fixed window, sliding window, token bucket and concurrency limiters. Partition by API key or user, not only by IP address, for authenticated APIs.
  • Idempotency. For POST operations that create money-related records (orders, invoices, payments), accept an Idempotency-Key header and store the first response. An endpoint filter is a clean place to implement this for minimal APIs; an action filter for controllers.
  • Cancellation. Accept a CancellationToken in every handler and pass it to EF Core and HttpClient calls, so abandoned requests stop consuming database time.

Data access patterns that matter more than endpoint style

In profiling real line-of-business APIs, the endpoint model is rarely the bottleneck. The usual culprits are data access mistakes:

Problem Symptom Fix
Loading full entities for read endpoints High memory, slow serialization Project to DTOs with Select and use AsNoTracking
N+1 queries from lazy loading Hundreds of queries per request Explicit Include or projection; disable lazy loading
Unbounded list endpoints Timeouts as data grows Mandatory paging with a maximum page size
Synchronous I/O Thread pool starvation under load Async all the way down
Chatty external calls Latency dominated by network Batch, cache or move to background processing

Measure with real data volumes using dotnet-counters, Application Insights or OpenTelemetry traces before deciding that the framework layer needs optimisation.

Migrating gradually

If you decide to move a controller codebase towards minimal APIs, do it feature by feature. Create the new route group under the same URL prefix, port the endpoints with their contracts unchanged, run the existing integration tests against both versions, then delete the controller. Because both models share routing, the switch is invisible to clients as long as routes, status codes and payloads stay identical. Snapshot tests of the OpenAPI document are a cheap safety net that catches accidental contract changes during such a migration.

Plan the order by risk: start with read-only endpoints that have good test coverage, then move write endpoints, and leave the most complex, least tested areas for last, after the team has settled the conventions on simpler features.

Frequently asked questions

Is it worth migrating controllers to minimal APIs? Only with a concrete reason such as Native AOT, performance targets or a planned restructuring into vertical slices. Rewriting working code for style alone rarely pays off.

Can minimal APIs use authorization policies and output caching? Yes. RequireAuthorization, RequireRateLimiting, CacheOutput and CORS policies apply per endpoint or per group.

How do we keep handlers from growing too large? Keep endpoints thin: bind, validate, call an application service or handler, map the result. Business rules belong in services that can be tested without HTTP.

Key takeaways

  • Both models share the same pipeline; the difference is declaration style and implicit behaviour.
  • Minimal APIs excel with route groups, typed results, endpoint filters and Native AOT.
  • Structure large APIs as vertical slices and enforce one validation and error format.
  • Choose by context, migrate only for clear benefits, and avoid mixing conventions without rules.

KHR Digital Solutions builds and reviews ASP.NET Core APIs for ERP, POS and HR platforms, including architecture, versioning and performance tuning.

Sources and further reading

  1. Minimal APIs overview — Microsoft Learn
  2. Create web APIs with ASP.NET Core (controllers) — Microsoft Learn
  3. Filters in minimal API apps — Microsoft Learn
  4. ASP.NET API Versioning — GitHub
ASP.NET Core Minimal APIs vs Controllers: An Architect's Guide for 202 | KHR Tech Blog