Minimal APIs vs controllers questions test something more specific than "do you know the syntax difference." Both models have shared the same routing, dependency injection and middleware stack since Minimal APIs matured past their .NET 6 debut, so a senior engineer is expected to reason about the architectural trade-offs: per-request overhead, how large codebases stay organized without controller classes to hang conventions on, what changes for validation and Native AOT, and how you'd actually run a migration between the two without a big-bang rewrite. Interviewers use this topic to see whether a candidate makes technology choices based on team and system constraints rather than personal preference. The ten questions below cover the real trade-offs, organizing endpoints at scale, filters and validation, performance, Native AOT, testing, migrating from controllers, and keeping a team consistent once a choice is made.

Q1 Beyond syntax, what are the real architectural differences between Minimal APIs and controllers?#

Short answer: Minimal APIs map a route directly to a delegate with no controller instantiation or action-selection step, generating a compiled RequestDelegate per endpoint, while controllers go through a class-based model with action selection, model binding via a general-purpose binder, and a richer filter and content-negotiation pipeline; both sit on the same routing, DI, authentication and OpenAPI infrastructure underneath.

AspectMinimal APIsControllers
Unit of organizationRoute groups and extension methodsClasses, attributes and conventions
Request handlingDelegate, resolved and bound per endpoint at startupAction selected and bound per request through [ApiController]
Native AOTSupportedNot supported
FiltersEndpoint filters (IEndpointFilter)Action, result, resource and exception filters
Content negotiationJSON by defaultPluggable input/output formatters, including XML
Best fitNew APIs, microservices, AOT-sensitive servicesLarge existing MVC codebases, formatter-heavy APIs

The difference that matters most in a design discussion is the filter and formatter pipeline: controllers inherit decades of MVC extensibility, including content negotiation for non-JSON formats and a five-stage filter pipeline (authorization, resource, action, exception, result), while Minimal APIs deliberately keep a single, simpler filter concept. Neither is "more capable" in an absolute sense; they optimize for different kinds of complexity.

What interviewers look for: an answer that goes past "Minimal APIs are less code" and names the actual binding, filter and formatter differences, since those are what drive a real architecture decision.

Common mistakes: claiming Minimal APIs "can't do everything controllers can," which was true in .NET 6 but is no longer accurate for validation, filters and OpenAPI as of .NET 10; treating the choice as purely stylistic.

Follow-up questions:

  • What controller feature, if any, has no Minimal API equivalent today?
  • Why can both models coexist and share the same middleware pipeline?

Q2 How do you keep a Minimal API codebase with hundreds of endpoints organized and navigable?#

Short answer: Group endpoints by feature using MapGroup, with one static class per feature that exposes an extension method mapping its group and named handler methods, so Program.cs reads like a table of contents instead of accumulating dozens of inline lambdas.

C#
namespace Shop.Api.Features.Orders;

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

        group.MapGet("/{id:guid}", GetById).WithName("GetOrder");
        group.MapPost("/", Create);
        return routes;
    }

    public static async Task<Results<Ok<OrderDto>, NotFound>> GetById(
        Guid id, IOrderService orders, CancellationToken ct) =>
        await orders.FindAsync(id, ct) is { } order ? TypedResults.Ok(order) : TypedResults.NotFound();

    public static async Task<CreatedAtRoute<OrderDto>> Create(
        CreateOrder command, IOrderService orders, CancellationToken ct)
    {
        var order = await orders.CreateAsync(command, ct);
        return TypedResults.CreatedAtRoute(order, "GetOrder", new { id = order.Id });
    }
}

// Program.cs
app.MapOrderEndpoints();
app.MapCustomerEndpoints();

Named, static handler methods, rather than inline lambdas, are what actually makes this scale: they're independently unit-testable, they can carry XML doc comments into the generated OpenAPI document since .NET 10, and groups let authorization, tags, CORS and filters apply once at the group level instead of being repeated on every Map* call. Reflection-based assembly scanning that auto-discovers endpoint classes is popular in some codebases, but explicit extension methods stay trimming-safe for AOT and are easier for a new team member to trace from Program.cs.

What interviewers look for: a concrete organizational pattern with named methods and groups, not just "put them in separate files," plus awareness of the AOT and OpenAPI benefits of named methods.

Common mistakes: leaving dozens of inline lambdas in Program.cs; using reflection-based endpoint discovery in an AOT-published service, which breaks trimming guarantees.

Follow-up questions:

  • How would you apply one cross-cutting policy, such as an audit filter, to every group at once?
  • What's lost when handlers are anonymous lambdas instead of named methods?

Q3 Compare endpoint filters to MVC action filters. When does one do something the other genuinely can't?#

Short answer: Endpoint filters and MVC action filters solve the same problem, inspecting or short-circuiting a call after its arguments are bound, but they're separate systems with separate registration surfaces; the practical gap isn't capability so much as reach, since a filter written for one model doesn't automatically apply to the other without an explicit adapter such as AddEndpointFilter on a controller-hosted endpoint.

C#
public sealed class AuditFilter(ILogger<AuditFilter> logger) : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context, EndpointFilterDelegate next)
    {
        var result = await next(context); // Code before next() runs first; after, in reverse
        logger.LogInformation("{User} called {Endpoint}",
            context.HttpContext.User.Identity?.Name, context.HttpContext.GetEndpoint()?.DisplayName);
        return result;
    }
}

app.MapGroup("/todos").AddEndpointFilter<AuditFilter>();

MVC's filter pipeline has more stages than Minimal APIs exposes directly: authorization filters run before model binding, resource filters wrap binding itself, action filters see bound arguments, exception filters catch unhandled exceptions from the action, and result filters wrap how the result executes. Endpoint filters collapse most of that into one concept that runs after binding, which is simpler to reason about but means an MVC team migrating a resource filter, one that needs to run before binding completes, has to rethink where that logic belongs rather than doing a mechanical translation.

What interviewers look for: naming the actual MVC filter stages, not just "action filters exist," and recognizing that the gap is architectural reach rather than raw capability.

Common mistakes: assuming IEndpointFilter is strictly less powerful than MVC filters instead of differently scoped; forgetting that filters can take constructor dependencies from DI but aren't themselves resolved from the container.

Follow-up questions:

  • Which MVC filter stage has no direct Minimal API equivalent?
  • How would you apply one endpoint filter to both a Minimal API group and a controller?

Q4 How does validation differ between the two models, and where does FluentValidation still fit?#

Short answer: Controllers validate automatically through [ApiController] and DataAnnotations against ModelState, returning 400 on failure before the action runs; Minimal APIs had no built-in validation until .NET 10 added Microsoft.Extensions.Validation, enabled with AddValidation(), which validates the same DataAnnotations attributes on query, header and body parameters through a compile-time source generator.

C#
using System.ComponentModel.DataAnnotations;

builder.Services.AddProblemDetails();
builder.Services.AddValidation(); // Source generator discovers handler parameter types

app.MapPost("/customers", (CreateCustomer request) =>
    TypedResults.Created($"/customers/{Guid.NewGuid()}", request));

public sealed record CreateCustomer(
    [Required, StringLength(100)] string Name,
    [Required, EmailAddress] string Email) : IValidatableObject
{
    public IEnumerable<ValidationResult> Validate(ValidationContext context)
    {
        if (Name.Equals(Email, StringComparison.OrdinalIgnoreCase))
            yield return new ValidationResult("Name must differ from email.", [nameof(Name)]);
    }
}

Two behaviors trip candidates up in practice. Discovery for AddValidation() is compile-time and per-assembly: a source generator finds the parameter types used by handlers, so a project that maps endpoints in a different assembly than the one that calls AddValidation() silently gets no validation and no error, since nothing fails loudly. And property-level attributes always run before type-level attributes and IValidatableObject.Validate, so a custom Validate method that assumes a property has already passed its own [Required] check is safe to write that way. FluentValidation remains a reasonable choice for teams that already use it or prefer its fluent rule composition over attributes, in either model.

What interviewers look for: knowing that Minimal API validation is a .NET 10 addition, not something that existed since day one, and the assembly-scoping gotcha, which is a real "why isn't this working" interview follow-up.

Common mistakes: assuming DataAnnotations "just work" on Minimal API parameters without calling AddValidation(); not knowing that [ApiController]'s automatic 400 behavior has no equivalent without it.

Follow-up questions:

  • What happens if you call AddValidation() in one assembly but map endpoints in another?
  • How would you opt a single endpoint out of validation?

Q5 Why do Minimal APIs have lower per-request overhead than controllers?#

Short answer: Minimal APIs generate binding and invocation code once, at startup, either through expression trees compiled by RequestDelegateFactory or, when trimming or AOT publishing is enabled, through the Request Delegate Generator (RDG) emitting the same logic as plain C# at compile time, whereas controller actions go through a more general model-binding and action-invocation pipeline evaluated per request.

XML
<!-- Opting into compile-time delegate generation even without full AOT publishing -->
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>

The practical consequence is that Minimal API binding is compiled specifically for each endpoint's exact parameter list, with no generic model-binder indirection to walk through at request time, so there's very little per-request work beyond the actual binding and handler call. For most APIs this difference is not the deciding factor: it matters when you're running at very high request volumes or very tight cold-start budgets, such as scale-to-zero serverless functions, and matters far less for a typical internal service where database or network latency dwarfs framework overhead. Citing a specific benchmark number in an interview is less convincing than explaining the mechanism; interviewers usually push on the "why," not the "how much."

What interviewers look for: the actual mechanism (compiled per-endpoint delegates vs. general binding) rather than a memorized percentage, and the judgment to say when the difference actually matters.

Common mistakes: quoting a specific throughput number without being able to explain what produces it; assuming the performance gap alone should drive every API's framework choice.

Follow-up questions:

  • When does EnableRequestDelegateGenerator turn on automatically?
  • In what kind of service would this overhead difference actually change your architecture?

Q6 Why is Native AOT tied to Minimal APIs and not controllers? What breaks if you try anyway?#

Short answer: Native AOT publishing removes the JIT and reflection-based infrastructure that MVC's runtime model discovery, dynamic action invocation and default JSON serialization depend on, so only the Minimal API model, whose binding is generated ahead of time by the Request Delegate Generator, is supported for AOT today; publishing a controller-based API with PublishAot fails or falls back to unsupported reflection paths that the trimmer can't verify.

C#
var builder = WebApplication.CreateSlimBuilder(args);

// Reflection-free JSON is required: every type crossing the HTTP boundary needs
// source-generated metadata, since the reflection-based serializer is unavailable.
builder.Services.ConfigureHttpJsonOptions(options =>
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));

var app = builder.Build();
app.MapGet("/todos", () => TypedResults.Ok(new[] { new Todo(1, "Ship it") }));
app.Run();

public sealed record Todo(int Id, string Title);

[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonContext : JsonSerializerContext { }

Two requirements follow from removing reflection: JSON must use the System.Text.Json source generator instead of the reflection-based serializer, and handlers must be passed as lambdas or method groups directly to Map* calls, where the RDG can see and compile them, rather than assembled dynamically. Treat every AOT or trimming warning at publish time as a build-breaking bug rather than noise; a warning-free AOT publish is the actual signal that the native binary behaves the same as the JIT-compiled version, and skipping that discipline is how AOT services end up with runtime failures that never show up in a normal dotnet run.

What interviewers look for: the specific reason (reflection removal, compile-time delegate generation) rather than "controllers just don't support it," plus the two concrete requirements for making an app AOT-ready.

Common mistakes: assuming AOT is only about startup time and ignoring the JSON source-generation requirement; treating trimming warnings as safe to ignore.

Follow-up questions:

  • What does CreateSlimBuilder remove compared to CreateBuilder, and why does that matter for AOT?
  • How would you find every trimming-unsafe call in a service before publishing it with AOT?

Q7 How do you test each style, and what's genuinely easier or harder?#

Short answer: Both models support the same two test levels, fast in-process unit tests against handler logic and full-stack integration tests with WebApplicationFactory<Program>, but Minimal APIs make the unit-test level noticeably easier when handlers are named static methods, because they're plain functions you call directly with fakes, with no controller instantiation, ControllerContext or action-invoker machinery to stand up first.

C#
public class OrderEndpointTests(WebApplicationFactory<Program> factory)
    : IClassFixture<WebApplicationFactory<Program>>
{
    [Fact]
    public async Task GetById_returns_NotFound_for_unknown_order()
    {
        // Unit-level: call the static handler directly, no host required.
        var result = await OrderEndpoints.GetById(Guid.NewGuid(), new InMemoryOrderService(), default);
        Assert.IsType<NotFound>(result.Result);
    }

    [Fact]
    public async Task Post_customer_with_invalid_body_returns_400()
    {
        // Integration-level: exercises routing, binding, validation and filters together.
        var client = factory.CreateClient();
        var response = await client.PostAsJsonAsync("/customers",
            new { Name = "", Email = "not-an-email" });
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }
}

Controller unit tests are still very workable, but instantiating a controller with mocked dependencies and, if the test needs ModelState or Url, wiring up enough of ControllerContext to make properties like HttpContext non-null, adds ceremony that a plain static method never needs. Integration tests are effectively identical between the two, since WebApplicationFactory exercises the full pipeline regardless of which model produced the endpoint. Before .NET 10 you had to add public partial class Program { } for the test project to reference the top-level statement entry point; .NET 10 generates that declaration automatically.

What interviewers look for: the distinction between unit and integration testing stated clearly, and the specific reason named static handlers are easier to unit test, not a vague "Minimal APIs are easier to test."

Common mistakes: only knowing integration testing and not being able to describe a true unit test for either style; not knowing the Program partial class requirement predates .NET 10.

Follow-up questions:

  • How would you unit test an endpoint filter in isolation?
  • What does WebApplicationFactory give you that a raw handler call doesn't?

Q8 You're asked to migrate a large MVC controller API to Minimal APIs incrementally. What's your plan, and what do you deliberately not migrate first?#

Short answer: Because both models register endpoints into the same routing table and share middleware, authentication and DI, you can run them side by side indefinitely and migrate one controller at a time, starting with simple, low-risk, high-traffic read endpoints to validate the pattern and tooling before touching anything with complex model binding, custom formatters or heavy filter chains.

A realistic plan: first, pick a small vertical slice, such as a single read-only resource, and port it to a Minimal API group with named handlers, run both old and new routes in parallel behind a feature flag if the risk tolerance demands it, and confirm OpenAPI output, authorization behavior and response shapes match exactly. Then expand to write endpoints once the team is comfortable with TypedResults, filters and the new validation model. Deliberately leave for later, or leave alone entirely, any controller that depends on features with no clean equivalent: XML content negotiation via input/output formatters, or heavy use of resource and exception filters that would need to be redesigned rather than mechanically ported. A generic but common outcome is a permanently mixed application, where a stable legacy area stays on controllers and all new development happens in Minimal APIs, which is a perfectly reasonable end state, not a stalled migration.

What interviewers look for: a staged, risk-ordered plan rather than "rewrite it all," and explicit acknowledgment that a permanent mixed state is often the right outcome, not a failure to finish.

Common mistakes: proposing a big-bang rewrite; forgetting that formatter-dependent or filter-heavy controllers need redesign, not a mechanical port.

Follow-up questions:

  • How would you keep OpenAPI documentation consistent while both models are in play?
  • What's your rollback plan if a migrated endpoint regresses in production?

Q9 As a tech lead, how do you decide whether a new service should use Minimal APIs or controllers, and how do you keep the team consistent afterward?#

Short answer: Start from concrete constraints, not preference: does the service need Native AOT or fast cold starts, does it need XML or other non-JSON content negotiation, does the team already have a large, well-understood MVC filter pipeline it would have to duplicate; then write the decision down as a one-page convention with a couple of code examples, so the choice doesn't get re-litigated on every pull request.

In practice, most new, JSON-only services default to Minimal APIs today, since Microsoft's own guidance and the webapi template default that way as of .NET 8, and the deciding factor for choosing controllers instead is usually a concrete technical requirement like custom formatters, not team familiarity alone. Consistency afterward comes from three things: a short written convention that names the default and the exceptions that justify controllers, a shared feature-folder template new services are scaffolded from, and code review that treats "which model did you pick and why" as a normal architectural question rather than a matter of taste. Revisit the convention periodically rather than treating it as permanent; a service that started as Minimal APIs for AOT reasons may need controllers later if a genuinely new formatter requirement shows up.

What interviewers look for: decision criteria grounded in real constraints (AOT, formatters, existing filter investment) and a concrete mechanism for consistency, such as a written convention and a template, not just "communicate well as a team."

Common mistakes: picking based on which model is newer or more fashionable; leaving the decision undocumented so every new service re-derives it independently.

Follow-up questions:

  • What would make you override the team's default and choose the other model for one service?
  • How do you handle a service that needs both JSON and XML clients?

Q10 Can Minimal APIs and controllers coexist in one app? What do they share, and where do they diverge?#

Short answer: Yes; calling AddControllers() (or AddControllersWithViews()) alongside Map* calls registers both into the same endpoint routing table, so they share middleware, authentication, authorization, DI and OpenAPI generation, and diverge mainly in content negotiation, where controllers support pluggable input and output formatters, such as XML, and Minimal APIs are JSON-first by default.

C#
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();          // For the controller-based endpoints
builder.Services.AddOpenApi();

var app = builder.Build();

app.MapControllers();                        // Legacy or formatter-heavy endpoints
app.MapGroup("/api/orders").MapOrderEndpoints(); // New Minimal API endpoints

app.Run();

Because both feed the same routing table, a request to either kind of endpoint goes through identical middleware, and authorization policies, rate limiting and output caching all apply the same way regardless of which model produced the matched endpoint. What doesn't automatically carry over is filter type: an MVC action filter doesn't run for a Minimal API endpoint and vice versa, so shared cross-cutting logic, such as an audit trail, needs either two small adapters or a piece of shared middleware placed after routing instead of a filter, if it truly needs to apply to both models identically.

What interviewers look for: confirmation that this is a fully supported, common pattern during migrations, plus the specific things that don't transfer across the boundary (filters, formatters).

Common mistakes: assuming mixing the two models is unusual or unsupported; assuming one filter type automatically protects both kinds of endpoints.

Follow-up questions:

  • How would you apply one authorization policy consistently to both models?
  • What's the simplest way to share cross-cutting logic across both without duplicating filters?

Quick-Fire Round#

QuestionAnswer
Which model supports Native AOT?Minimal APIs only
Which model has built-in XML formatter support?Controllers
When did Minimal API validation via DataAnnotations arrive?.NET 10, with AddValidation()
What generates compiled binding code for Minimal APIs?RequestDelegateFactory, or the RDG at compile time
Can the two models share one routing table?Yes, they register into the same endpoint routing table
Do MVC action filters run on Minimal API endpoints?No, without an explicit adapter
What keeps a large Minimal API app organized?MapGroup plus one extension method per feature
What must Program.cs include for controller tests pre-.NET 10?public partial class Program { }
What's the .NET 8+ template default for new Web APIs?Minimal APIs (--use-controllers switches back)
Where does the biggest per-request overhead gap come from?Compiled per-endpoint binding vs. general model binding

How to Prepare#

  • Be ready to name the actual mechanism behind the performance difference, not just repeat "Minimal APIs are faster."
  • Practice writing a feature-folder MapGroup pattern from memory; it's a common live-coding exercise.
  • Know exactly what's required to make a service Native AOT-ready: source-generated JSON and RDG-visible handlers.
  • Rehearse a staged migration plan you could describe in two minutes, including what you'd deliberately migrate last.
  • Have a real or realistic story about choosing one model over the other for a specific technical reason, not preference.