# 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. - Published: 2026-07-24 - Updated: 2026-07-24 - Author: SharpSkill - Tags: aspnet-core, minimal-apis, dotnet, csharp, web-api, interview - Reading time: 12 min --- 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. ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // Register services for dependency injection builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddScoped(); 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. ```csharp // ProductEndpoints.cs 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("application/json") .Produces(StatusCodes.Status201Created); group.MapPut("/{id:int}", UpdateProduct); group.MapDelete("/{id:int}", DeleteProduct) .RequireAuthorization("AdminOnly"); } private static async Task GetAllProducts( IProductRepository repo, CancellationToken ct) { var products = await repo.GetAllAsync(ct); return Results.Ok(products); } private static async Task 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 CreateProduct( CreateProductRequest request, IProductRepository repo, IValidator 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. ```csharp // Program.cs - Parameter binding examples 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 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 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. ```csharp // Program.cs - AOT-compatible configuration 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))] 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, 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` 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](/blog/dotnet/aspnet-core-interview-questions-middleware-di-minimal-apis) about API design. ## 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. ```csharp // ValidationFilter.cs public class ValidationFilter : IEndpointFilter where T : class { public async ValueTask InvokeAsync( EndpointFilterInvocationContext context, EndpointFilterDelegate next) { var validator = context.HttpContext .RequestServices .GetService>(); if (validator is null) return await next(context); var argument = context.Arguments .OfType() .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>(); // Global filter registration via route group var api = app.MapGroup("/api") .AddEndpointFilter(async (context, next) => { var logger = context.HttpContext .RequestServices .GetRequiredService>(); 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. ```csharp // Program.cs - JWT authentication setup 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](/technologies/dotnet/interview-questions/authentication-authorization). ## 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. ```csharp // ProductEndpointsTests.cs public class ProductEndpointsTests : IClassFixture> { private readonly HttpClient _client; private readonly WebApplicationFactory _factory; public ProductEndpointsTests(WebApplicationFactory factory) { _factory = factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => { // Replace real repository with mock services.RemoveAll(); services.AddScoped(); }); }); _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>(); 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](/blog/dotnet/clean-architecture-dotnet-practical-guide) 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/dotnet/aspnet-core-minimal-apis-architecture-performance-interview