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 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 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.
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.
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.
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.
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.
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.
// 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.
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.
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 |
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.
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
Share
Related articles

C# and .NET Interview Questions: Complete Guide 2026
The 25 most common C# and .NET interview questions. LINQ, async/await, dependency injection, Entity Framework and best practices with detailed answers.

.NET 9 Blazor: Full-Stack Development with Blazor United in 2026
.NET 9 Blazor United combines static SSR, Server, and WebAssembly render modes into one full-stack framework. A practical tutorial covering render modes, streaming rendering, constructor injection, and production-ready patterns.

Entity Framework Core: Performance Optimization and Best Practices in 2026
Master EF Core 10 performance optimization with AsNoTracking, compiled queries, split queries, batch operations, and the new LeftJoin operator. Practical C# examples for production .NET 10 applications.