Minimal APIs vs Controllers: A Decision Framework

TL;DR A decision rubric for choosing between Minimal APIs and Controllers based on team size, complexity, and maintainability requirements.

Microsoft recommends Minimal APIs for new projects. That does not mean they are always the right choice. This article provides a decision framework you can apply to your specific context.

Common questions this answers

  • When should I use Minimal APIs vs Controllers?
  • Is the performance difference significant?
  • How do I organize Minimal APIs as the project grows?
  • Can I use both in the same project?

Definition (what this means in practice)

Minimal APIs and Controllers are two approaches for building HTTP APIs in ASP.NET Core. Minimal APIs use a functional style with lambda handlers. Controllers use an object-oriented style with class-based organization. Both generate similar HTTP responses; the difference is in code organization and extensibility.

In practice, the choice affects how you structure code, how you test it, and how it scales with team size and complexity.

Terms used

  • Minimal APIs: endpoint definitions using MapGet, MapPost, etc., typically in Program.cs or extension methods.
  • Controllers: classes inheriting from ControllerBase with action methods decorated with route attributes.
  • Endpoint filter: Minimal API equivalent of action filters in MVC.
  • Model binding: automatic mapping of request data to method parameters.

Reader contract

This article is for:

  • Engineers starting new ASP.NET Core API projects.
  • Teams evaluating whether to migrate between approaches.

You will leave with:

  • A scoring rubric to apply to your project.
  • Clear thresholds for when each approach fits.
  • Guidance on the hybrid approach.

This is not for:

  • Minimal API syntax tutorials.
  • MVC fundamentals explanations.

Quick start (10 minutes)

If you need a quick answer:

Verified on: ASP.NET Core (.NET 10).

Use Minimal APIs when:

  • Building a small service (under 20 endpoints).
  • Team is comfortable with functional style.
  • You want minimal framework overhead and less boilerplate.
  • The API surface is simple CRUD.

Use Controllers when:

  • Building a large API (50+ endpoints).
  • Multiple teams work on the same codebase.
  • You need extensive filter pipelines.
  • Team is experienced with MVC patterns.

Use both when:

  • Migrating incrementally.
  • Different parts have different complexity levels.

Microsoft's recommendation

Microsoft's official documentation recommends starting with Minimal APIs for new projects. The guidance emphasizes simpler syntax and reduced overhead compared to controller-based APIs.

However, the same documentation notes that controller-based APIs remain fully supported and are appropriate for larger applications that benefit from the structure controllers provide.

The recommendation is a starting point, not a mandate. Apply the decision criteria below.

Decision matrix: the 6 criteria

Score each criterion 0-2 for your project. Higher total favors Controllers; lower favors Minimal APIs.

Criterion Score 0 (Minimal APIs) Score 1 (Either) Score 2 (Controllers)
Endpoint count Under 15 15-40 Over 40
Team size 1-2 developers 3-5 developers 6+ developers
Filter complexity None or simple Moderate Extensive pipelines
API versioning None Simple Complex multi-version
Existing codebase Greenfield Small existing Large MVC codebase
Team MVC experience Low Mixed High

Interpretation:

  • Score 0-4: Lean toward Minimal APIs
  • Score 5-7: Either works; choose based on team preference
  • Score 8-12: Lean toward Controllers

Equivalent implementations

The same endpoint in both styles:

Minimal API

// Program.cs or extension method
app.MapGet("/articles/{slug}", async (string slug, AppDbContext db) =>
{
    var article = await db.Articles
        .AsNoTracking()
        .FirstOrDefaultAsync(a => a.Slug == slug);

    return article is null
        ? Results.NotFound()
        : Results.Ok(article);
});

Controller

[ApiController]
[Route("articles")]
public class ArticlesController(AppDbContext db) : ControllerBase
{
    [HttpGet("{slug}")]
    public async Task<IActionResult> Get(string slug)
    {
        var article = await db.Articles
            .AsNoTracking()
            .FirstOrDefaultAsync(a => a.Slug == slug);

        return article is null
            ? NotFound()
            : Ok(article);
    }
}

The logic is identical. The difference is organizational.

Team size and structure

Team dynamics affect the choice more than technical factors.

Small teams (1-3 developers)

Minimal APIs reduce ceremony. Everyone sees all endpoints. Conventions are implicit and shared naturally.

Medium teams (4-8 developers)

Either approach works. If choosing Minimal APIs, establish clear organization patterns early (one file per feature, extension method groups).

Large teams (8+ developers)

Controllers provide natural boundaries. Each controller is a unit of ownership. Filters and conventions are explicit and discoverable.

Multi-team codebases

Controllers win. Teams can own controllers without stepping on each other. Shared filters are explicit contracts.

Complexity thresholds

Minimal APIs start simple but can become messy as complexity grows.

Signs Minimal APIs are working

  • Endpoints fit in Program.cs or a few extension files.
  • Route groups are logical and shallow.
  • Endpoint filters are simple or unused.
  • New team members find endpoints quickly.

Signs Minimal APIs need refactoring

  • Program.cs exceeds 200 lines.
  • Endpoint logic is duplicated across handlers.
  • You are building custom abstractions to organize endpoints.
  • Filters are nested and hard to trace.

At this point, consider:

  1. Extracting to extension methods per feature area.
  2. Moving to Controllers for the complex parts.
  3. Building a hybrid architecture.

Performance reality check

Microsoft notes that Minimal APIs can have reduced overhead compared to controller-based APIs. In most real-world APIs, the difference isn't the dominant factor.

What the benchmarks show

You can often measure slightly lower allocations and marginally better throughput with Minimal APIs in microbenchmarks, but the impact varies by workload.

When it matters

  • Very high-throughput services.
  • Latency-sensitive hot paths.
  • Scenarios where framework overhead is the bottleneck (not I/O).

When it does not matter

  • Most API workloads where database or network I/O dominates.
  • APIs with OutputCache or other caching.
  • Moderate traffic applications.

Guidance: Do not choose based on performance unless you have measured a bottleneck. Choose based on maintainability.

Testability comparison

Both approaches are testable. The mechanics differ.

Minimal APIs

Test using WebApplicationFactory for integration tests. Unit testing handlers requires extracting logic to testable classes.

// Integration test
public class ArticleEndpointTests(WebApplicationFactory<Program> factory)
    : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task GetArticle_ReturnsOk_WhenExists()
    {
        var client = factory.CreateClient();
        var response = await client.GetAsync("/articles/test-slug");
        response.StatusCode.Should().Be(HttpStatusCode.OK);
    }
}

Controllers

Same integration test approach, but controllers are also easy to unit test by instantiating directly with mock dependencies.

// Unit test
[Fact]
public async Task Get_ReturnsNotFound_WhenMissing()
{
    var mockDb = CreateMockDbContext();
    var controller = new ArticlesController(mockDb);

    var result = await controller.Get("missing-slug");

    result.Should().BeOfType<NotFoundResult>();
}

Guidance: If your team prioritizes unit tests of HTTP handlers, Controllers are easier. If you rely on integration tests, both are equivalent.

The hybrid approach

You can use both in the same application. This is explicitly supported.

When to mix

  • Migrating from Controllers to Minimal APIs incrementally.
  • Simple endpoints as Minimal APIs, complex features as Controllers.
  • Different teams prefer different styles.

How to organize

// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); // Enable controllers

var app = builder.Build();

// Minimal API endpoints
app.MapGet("/health", () => Results.Ok());
app.MapGet("/version", () => Results.Ok(new { Version = "1.0" }));

// Controller endpoints
app.MapControllers();

app.Run();

Boundaries to maintain

  • Do not duplicate endpoints between styles.
  • Document which style applies to which feature area.
  • Share filters/middleware consistently.

API versioning comparison

API versioning complexity often drives the Controller vs Minimal API decision.

Versioning strategies

Strategy URL Example Minimal API Controller
URL segment /v1/articles Route groups Route attributes
Query string /articles?api-version=1.0 Manual parsing Built-in support
Header X-Api-Version: 1.0 Endpoint filters Action filters
Media type Accept: application/vnd.api.v1+json Complex Built-in support

Minimal API versioning with Asp.Versioning

// Program.cs
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
})
.AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});

var app = builder.Build();

// Version 1 endpoints
var v1 = app.NewVersionedApi()
    .MapGroup("/api/v{version:apiVersion}/articles")
    .HasApiVersion(1.0);

v1.MapGet("/", (AppDbContext db) => db.Articles.ToListAsync());
v1.MapGet("/{id}", (int id, AppDbContext db) => db.Articles.FindAsync(id));

// Version 2 endpoints (breaking changes)
var v2 = app.NewVersionedApi()
    .MapGroup("/api/v{version:apiVersion}/articles")
    .HasApiVersion(2.0);

v2.MapGet("/", (AppDbContext db, int? limit) =>
    db.Articles.Take(limit ?? 100).ToListAsync());
v2.MapGet("/{slug}", (string slug, AppDbContext db) =>  // Changed from id to slug
    db.Articles.FirstOrDefaultAsync(a => a.Slug == slug));

Controller versioning with Asp.Versioning

[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/articles")]
public class ArticlesV1Controller(AppDbContext db) : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetAll()
        => Ok(await db.Articles.ToListAsync());

    [HttpGet("{id:int}")]
    public async Task<IActionResult> Get(int id)
        => Ok(await db.Articles.FindAsync(id));
}

[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/articles")]
public class ArticlesV2Controller(AppDbContext db) : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetAll(int? limit = 100)
        => Ok(await db.Articles.Take(limit ?? 100).ToListAsync());

    [HttpGet("{slug}")]
    public async Task<IActionResult> Get(string slug)
        => Ok(await db.Articles.FirstOrDefaultAsync(a => a.Slug == slug));
}

Versioning decision guidance

Complexity Recommendation
Single version API Either approach works
2-3 versions, minor differences Minimal APIs with route groups
Multiple versions, breaking changes Controllers (clearer separation)
Sunset strategy needed Controllers (easier deprecation attributes)

OpenAPI and Swagger comparison

OpenAPI documentation generation differs significantly between approaches.

Minimal API OpenAPI (.NET 10+)

// Program.cs
builder.Services.AddOpenApi();

var app = builder.Build();

// Add metadata to endpoints
app.MapGet("/articles/{slug}", async (string slug, AppDbContext db) =>
{
    var article = await db.Articles.FindAsync(slug);
    return article is null ? Results.NotFound() : Results.Ok(article);
})
.WithName("GetArticle")
.WithDescription("Retrieves an article by its URL slug")
.WithTags("Articles")
.Produces<Article>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.WithOpenApi(operation =>
{
    operation.Parameters[0].Description = "The URL-friendly article identifier";
    return operation;
});

app.MapOpenApi();  // Exposes /openapi/v1.json

Controller OpenAPI

[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class ArticlesController(AppDbContext db) : ControllerBase
{
    /// <summary>
    /// Retrieves an article by its URL slug.
    /// </summary>
    /// <param name="slug">The URL-friendly article identifier</param>
    /// <returns>The article if found</returns>
    /// <response code="200">Returns the article</response>
    /// <response code="404">Article not found</response>
    [HttpGet("{slug}")]
    [ProducesResponseType(typeof(Article), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Get(string slug)
    {
        var article = await db.Articles.FindAsync(slug);
        return article is null ? NotFound() : Ok(article);
    }
}

OpenAPI feature comparison

Feature Minimal APIs Controllers
Auto-discovery Yes Yes
XML comments Limited Full support
Response types WithOpenApi() ProducesResponseType
Grouping WithTags() Route/controller name
Examples Manual via WithOpenApi() Swashbuckle annotations
Versioned docs Requires configuration Built-in with Asp.Versioning

OpenAPI recommendation

  • Simple APIs: Minimal API's WithOpenApi() is sufficient
  • Public APIs: Controllers with XML documentation provide richer docs
  • Generated clients: Both work; Controllers often cleaner for complex DTOs

Migration patterns

Controllers to Minimal APIs

  1. Start with new, simple endpoints.
  2. Extract shared logic to services (not controller methods).
  3. Migrate one controller at a time.
  4. Retire controllers when empty.

Minimal APIs to Controllers

  1. Group related endpoints into a controller.
  2. Convert handlers to action methods.
  3. Convert endpoint filters to action filters.
  4. Update tests (usually minimal changes).

Incremental migration

The hybrid approach allows gradual migration. There is no pressure to convert everything at once.

Copy/paste artifact: decision scorecard

Minimal APIs vs Controllers Decision Scorecard

Project: _________________ Date: _________

Score each 0-2 (0 = Minimal APIs, 2 = Controllers)

[ ] Endpoint count
    0: Under 15 | 1: 15-40 | 2: Over 40

[ ] Team size
    0: 1-2 devs | 1: 3-5 devs | 2: 6+ devs

[ ] Filter complexity
    0: None/simple | 1: Moderate | 2: Extensive

[ ] API versioning needs
    0: None | 1: Simple | 2: Complex multi-version

[ ] Existing codebase
    0: Greenfield | 1: Small existing | 2: Large MVC

[ ] Team MVC experience
    0: Low | 1: Mixed | 2: High

TOTAL: _____ / 12

Interpretation:
  0-4:  Minimal APIs recommended
  5-7:  Either works, team preference
  8-12: Controllers recommended

Notes:
_________________________________
_________________________________

Common failure modes

  1. Choosing Minimal APIs because they are "new" without evaluating fit.
  2. Overengineering Minimal API organization to replicate controller structure.
  3. Ignoring team experience and forcing a style change.
  4. Assuming performance difference matters without measuring.
  5. Mixing styles inconsistently without clear boundaries.

Checklist

  • Scored the 6 decision criteria for your project.
  • Considered team size and experience.
  • Evaluated complexity trajectory (will it grow?).
  • Decided on hybrid vs single approach.
  • Established organization patterns for chosen style.

FAQ

Should I always use Minimal APIs for new projects?

No. Microsoft recommends them as a starting point, but the decision depends on your context. Use the scoring rubric.

Can I mix Controllers and Minimal APIs?

Yes. This is explicitly supported and useful for incremental migration or mixed-complexity codebases.

Is the performance difference significant?

Rarely. Choose based on maintainability unless you have measured a performance bottleneck.

How do I organize Minimal APIs as the project grows?

Use extension methods to group related endpoints. Create one file per feature area. Use route groups for shared prefixes and filters.

// ArticleEndpoints.cs
public static class ArticleEndpoints
{
    public static void MapArticleEndpoints(this WebApplication app)
    {
        var group = app.MapGroup("/articles");
        group.MapGet("/", GetAll);
        group.MapGet("/{slug}", GetBySlug);
        group.MapPost("/", Create);
    }

    private static async Task<IResult> GetAll(AppDbContext db) => // ...
    private static async Task<IResult> GetBySlug(string slug, AppDbContext db) => // ...
    private static async Task<IResult> Create(CreateArticleRequest request, AppDbContext db) => // ...
}

What about API versioning?

API versioning is largely driven by whichever strategy (and any libraries) you choose. If you expect complex, multi-version APIs, make sure your planned versioning approach fits your endpoint style before committing.

Do action filters work with Minimal APIs?

No. Minimal APIs use endpoint filters, which have similar capabilities but different syntax. If you have extensive custom action filter logic, Controllers may be easier.

What to do next

Score your current or planned project using the decision rubric. If the score is in the middle range, prototype both approaches for one feature and compare.

For more on building production-quality ASP.NET Core applications, read EF Core Performance Mistakes That Ship to Production.

If you want help evaluating API architecture for your project, reach out via Contact.

References

Author notes

Decisions:

  • Provide a scoring rubric, not just pros/cons. Rationale: engineers need actionable criteria, not feature lists.
  • Recommend based on team size and complexity, not performance. Rationale: performance difference is negligible for most workloads.
  • Explicitly support hybrid approach. Rationale: real projects often need flexibility during migration or mixed-complexity scenarios.

Observations:

  • Teams that choose Minimal APIs for "simplicity" sometimes rebuild controller-like abstractions.
  • Teams with MVC experience adopt Controllers faster and with fewer organizational debates.
  • Performance-based decisions without measurement often lead to regret.