ASP.NET Core Minimal APIs in 2026: Architecture, Performance and Interview Questions

Master ASP.NET Core Minimal APIs with this deep dive covering route groups, endpoint filters, Native AOT compilation, typed results, and common interview questions.

ASP.NET Core Minimal APIs architecture and performance visualization

ASP.NET Core Minimal APIs eliminate the ceremony of traditional MVC controllers, offering a streamlined approach to building HTTP endpoints with significantly less boilerplate. Introduced in .NET 6 and refined through .NET 8, 9, and now .NET 10, Minimal APIs have matured into a production-ready choice for microservices, serverless functions, and lightweight web services.

Minimal APIs vs Controllers

Minimal APIs use top-level route handlers defined directly in Program.cs, while MVC controllers require class definitions, attributes, and convention-based routing. For simple CRUD operations or microservices with fewer than 20 endpoints, Minimal APIs typically reduce code by 40-60%.

Minimal API Architecture and Request Pipeline

The ASP.NET Core request pipeline processes HTTP requests through middleware components before reaching endpoint handlers. Minimal APIs integrate seamlessly with this pipeline while offering a more declarative syntax for route definition.

Program.cscsharp
var builder = WebApplication.CreateBuilder(args);

// Register services for dependency injection
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddScoped<IProductRepository, ProductRepository>();

var app = builder.Build();

// Middleware pipeline configuration
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseAuthorization();

// Minimal API endpoint definitions
app.MapGet("/products", async (IProductRepository repo) =>
    Results.Ok(await repo.GetAllAsync()));

app.MapGet("/products/{id:int}", async (int id, IProductRepository repo) =>
    await repo.GetByIdAsync(id) is Product product
        ? Results.Ok(product)
        : Results.NotFound());

app.Run();

This pattern centralizes route definitions while maintaining full access to the dependency injection container. The IProductRepository parameter demonstrates constructor-less injection directly into handler delegates.

Route Groups and Endpoint Organization

As applications grow, organizing endpoints becomes critical. Route groups, introduced in .NET 7, provide namespacing and shared configuration without sacrificing the minimal approach.

ProductEndpoints.cscsharp
public static class ProductEndpoints
{
    public static void MapProductEndpoints(this WebApplication app)
    {
        var group = app.MapGroup("/api/products")
            .WithTags("Products")
            .RequireAuthorization();

        group.MapGet("/", GetAllProducts);
        group.MapGet("/{id:int}", GetProductById);
        group.MapPost("/", CreateProduct)
            .Accepts<CreateProductRequest>("application/json")
            .Produces<Product>(StatusCodes.Status201Created);
        group.MapPut("/{id:int}", UpdateProduct);
        group.MapDelete("/{id:int}", DeleteProduct)
            .RequireAuthorization("AdminOnly");
    }

    private static async Task<IResult> GetAllProducts(
        IProductRepository repo,
        CancellationToken ct)
    {
        var products = await repo.GetAllAsync(ct);
        return Results.Ok(products);
    }

    private static async Task<IResult> GetProductById(
        int id,
        IProductRepository repo,
        CancellationToken ct)
    {
        var product = await repo.GetByIdAsync(id, ct);
        return product is not null
            ? Results.Ok(product)
            : Results.NotFound();
    }

    private static async Task<IResult> CreateProduct(
        CreateProductRequest request,
        IProductRepository repo,
        IValidator<CreateProductRequest> validator,
        CancellationToken ct)
    {
        var validation = await validator.ValidateAsync(request, ct);
        if (!validation.IsValid)
            return Results.ValidationProblem(validation.ToDictionary());

        var product = await repo.CreateAsync(request.ToProduct(), ct);
        return Results.Created($"/api/products/{product.Id}", product);
    }
}

Calling app.MapProductEndpoints() in Program.cs registers all product routes with shared authorization requirements and OpenAPI metadata. This structure scales well for applications with hundreds of endpoints.

Parameter Binding and Validation

Minimal APIs support multiple binding sources: route parameters, query strings, headers, request bodies, and services from DI. Understanding the binding precedence prevents common interview pitfalls.

Program.cs - Parameter binding examplescsharp
app.MapGet("/search", (
    [FromQuery] string? query,           // Explicit query string
    [FromQuery] int page = 1,            // Default value
    [FromQuery] int pageSize = 20,       // Default value
    [FromHeader(Name = "X-Correlation-Id")] string? correlationId,
    ILogger<Program> logger) =>
{
    logger.LogInformation("Search request: {Query}, Page: {Page}, CorrelationId: {CorrelationId}",
        query, page, correlationId);

    return Results.Ok(new { query, page, pageSize, correlationId });
});

// Complex model binding with validation
app.MapPost("/orders", async (
    [FromBody] CreateOrderRequest request,
    [FromServices] IValidator<CreateOrderRequest> validator,
    [FromServices] IOrderService orderService,
    HttpContext context,
    CancellationToken ct) =>
{
    var validationResult = await validator.ValidateAsync(request, ct);

    if (!validationResult.IsValid)
    {
        return Results.ValidationProblem(
            validationResult.Errors
                .GroupBy(e => e.PropertyName)
                .ToDictionary(
                    g => g.Key,
                    g => g.Select(e => e.ErrorMessage).ToArray()));
    }

    var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
    var order = await orderService.CreateOrderAsync(request, userId!, ct);

    return Results.Created($"/orders/{order.Id}", order);
});

The [FromBody] attribute is optional for complex types but improves readability. FluentValidation integrates naturally through dependency injection, keeping validation logic separate from endpoint handlers.

Binding Source Priority

When no attribute is specified, Minimal APIs infer binding sources: route parameters first, then query strings for simple types, and request body for complex types. Explicit attributes like [FromQuery] or [FromBody] override this behavior.

Performance Optimization with Native AOT

.NET 8 introduced Native AOT (Ahead-of-Time) compilation support for Minimal APIs, producing self-contained executables with sub-millisecond startup times. This capability makes Minimal APIs ideal for serverless deployments where cold start latency matters.

Program.cs - AOT-compatible configurationcsharp
var builder = WebApplication.CreateSlimBuilder(args);

// AOT-friendly JSON serialization
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});

var app = builder.Build();

app.MapGet("/health", () => Results.Ok(new HealthResponse("Healthy", DateTime.UtcNow)));

app.Run();

// Source-generated JSON serializer context
[JsonSerializable(typeof(HealthResponse))]
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
internal partial class AppJsonContext : JsonSerializerContext { }

public record HealthResponse(string Status, DateTime CheckedAt);

The CreateSlimBuilder method excludes unnecessary framework features, while the source-generated JsonSerializerContext eliminates runtime reflection for JSON serialization. Published AOT binaries for simple APIs typically measure 10-15 MB compared to 80+ MB for standard self-contained deployments.

Typed Results and Response Metadata

.NET 7 introduced TypedResults for compile-time verification of response types, improving OpenAPI documentation accuracy and catching type mismatches during development.

csharp
// Strongly-typed results with OpenAPI metadata
app.MapGet("/products/{id:int}", async Task<Results<Ok<Product>, NotFound, ProblemHttpResult>> (
    int id,
    IProductRepository repo,
    CancellationToken ct) =>
{
    try
    {
        var product = await repo.GetByIdAsync(id, ct);

        return product is not null
            ? TypedResults.Ok(product)
            : TypedResults.NotFound();
    }
    catch (Exception ex)
    {
        return TypedResults.Problem(
            detail: "An error occurred retrieving the product",
            statusCode: StatusCodes.Status500InternalServerError);
    }
})
.WithName("GetProductById")
.WithOpenApi(operation =>
{
    operation.Summary = "Retrieves a product by ID";
    operation.Description = "Returns the product details or 404 if not found";
    return operation;
});

The Results<T1, T2, T3> union type declares all possible response types, which Swagger/OpenAPI generators use to produce accurate documentation. This pattern is particularly valuable when preparing for ASP.NET Core interview questions about API design.

Ready to ace your .NET interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Endpoint Filters for Cross-Cutting Concerns

Endpoint filters provide middleware-like functionality scoped to specific endpoints or groups, handling concerns such as logging, caching, and request transformation.

ValidationFilter.cscsharp
public class ValidationFilter<T> : IEndpointFilter where T : class
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var validator = context.HttpContext
            .RequestServices
            .GetService<IValidator<T>>();

        if (validator is null)
            return await next(context);

        var argument = context.Arguments
            .OfType<T>()
            .FirstOrDefault();

        if (argument is null)
            return await next(context);

        var validationResult = await validator.ValidateAsync(argument);

        if (!validationResult.IsValid)
        {
            return Results.ValidationProblem(
                validationResult.Errors
                    .GroupBy(e => e.PropertyName)
                    .ToDictionary(
                        g => g.Key,
                        g => g.Select(e => e.ErrorMessage).ToArray()));
        }

        return await next(context);
    }
}

// Usage in Program.cs
app.MapPost("/products", CreateProduct)
    .AddEndpointFilter<ValidationFilter<CreateProductRequest>>();

// Global filter registration via route group
var api = app.MapGroup("/api")
    .AddEndpointFilter(async (context, next) =>
    {
        var logger = context.HttpContext
            .RequestServices
            .GetRequiredService<ILogger<Program>>();

        var stopwatch = Stopwatch.StartNew();
        var result = await next(context);
        stopwatch.Stop();

        logger.LogInformation(
            "Endpoint {Method} {Path} completed in {ElapsedMs}ms",
            context.HttpContext.Request.Method,
            context.HttpContext.Request.Path,
            stopwatch.ElapsedMilliseconds);

        return result;
    });

Filters execute in order of registration, with the innermost filter closest to the endpoint handler. This architecture enables clean separation of validation, logging, and authorization logic.

Authentication and Authorization Patterns

Minimal APIs support the same authentication and authorization mechanisms as MVC controllers, with a more declarative configuration syntax.

Program.cs - JWT authentication setupcsharp
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidAudience = builder.Configuration["Jwt:Audience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
        };
    });

builder.Services.AddAuthorizationBuilder()
    .AddPolicy("AdminOnly", policy =>
        policy.RequireRole("Admin"))
    .AddPolicy("PremiumUser", policy =>
        policy.RequireClaim("subscription", "premium", "enterprise"));

var app = builder.Build();

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

// Protected endpoints
app.MapGet("/admin/users", async (IUserService userService) =>
    Results.Ok(await userService.GetAllUsersAsync()))
    .RequireAuthorization("AdminOnly");

app.MapGet("/profile", async (ClaimsPrincipal user, IUserService userService) =>
{
    var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
    var profile = await userService.GetProfileAsync(userId!);
    return Results.Ok(profile);
})
.RequireAuthorization();

// Anonymous endpoint within protected group
var protectedGroup = app.MapGroup("/api/secure")
    .RequireAuthorization();

protectedGroup.MapGet("/public-info", () => Results.Ok("This is public"))
    .AllowAnonymous();

The RequireAuthorization extension method accepts policy names or can be called without arguments to require any authenticated user. Understanding these patterns is essential for authentication and authorization interview questions.

Interview Questions: Common Patterns

Technical interviews frequently explore the differences between Minimal APIs and traditional controllers. The table below summarizes key distinctions:

| Aspect | Minimal APIs | MVC Controllers | |--------|--------------|----------------| | Boilerplate | Low - direct handler delegates | Higher - class + method + attributes | | Routing | Inline with MapGet, MapPost | Attribute or convention-based | | Model Binding | Automatic with optional attributes | Convention + attributes | | Filters | Endpoint filters | Action filters + middleware | | AOT Support | Full support since .NET 8 | Limited, reflection-heavy | | Testability | Function-based, easy to unit test | Requires controller instantiation | | Best For | Microservices, simple APIs | Large applications, complex workflows |

Interview Tip

When asked "When would you choose controllers over Minimal APIs?", mention complex applications requiring action filters, model binding customization, or existing codebases with established MVC patterns. Minimal APIs excel for greenfield microservices and serverless functions.

Testing Minimal API Endpoints

Integration testing with WebApplicationFactory provides realistic endpoint verification without deploying the application.

ProductEndpointsTests.cscsharp
public class ProductEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly WebApplicationFactory<Program> _factory;

    public ProductEndpointsTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Replace real repository with mock
                services.RemoveAll<IProductRepository>();
                services.AddScoped<IProductRepository, MockProductRepository>();
            });
        });
        _client = _factory.CreateClient();
    }

    [Fact]
    public async Task GetProducts_ReturnsOkWithProductList()
    {
        // Act
        var response = await _client.GetAsync("/api/products");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);

        var products = await response.Content
            .ReadFromJsonAsync<List<Product>>();

        products.Should().NotBeNull();
        products.Should().HaveCountGreaterThan(0);
    }

    [Fact]
    public async Task GetProductById_WithInvalidId_ReturnsNotFound()
    {
        // Act
        var response = await _client.GetAsync("/api/products/99999");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }

    [Fact]
    public async Task CreateProduct_WithValidRequest_ReturnsCreated()
    {
        // Arrange
        var request = new CreateProductRequest("Test Product", 29.99m, "Test Description");

        // Act
        var response = await _client.PostAsJsonAsync("/api/products", request);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.Created);
        response.Headers.Location.Should().NotBeNull();
    }
}

For deeper exploration of clean architecture patterns in .NET applications, the service layer should be tested independently with unit tests, while integration tests verify the full request pipeline.

Conclusion

Minimal APIs represent the modern approach to building HTTP services in ASP.NET Core:

  • Route groups and endpoint organization scale from simple microservices to complex applications
  • Endpoint filters provide clean separation of cross-cutting concerns like validation and logging
  • TypedResults enable compile-time response type verification and accurate OpenAPI documentation
  • Native AOT compilation delivers sub-millisecond startup times for serverless deployments
  • Integration testing with WebApplicationFactory ensures realistic endpoint verification

For interview preparation, focus on articulating when Minimal APIs are appropriate versus traditional controllers, and demonstrate understanding of the request pipeline, dependency injection, and authentication patterns.

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Tags

#aspnet-core
#minimal-apis
#dotnet
#csharp
#web-api
#interview

Share

Related articles