Minimal APIs in .NET: Building Lightweight Web Services
Introduction
[Explain shift from MVC controllers to minimal APIs for simpler, more performant microservices and serverless scenarios.]
Prerequisites
- .NET 8+ SDK
- Basic HTTP/REST knowledge
Core Concepts
| Feature | Minimal API | Controller-based |
|---|---|---|
| Routing | Inline lambda | Attribute routing |
| Dependency Injection | Method parameter | Constructor injection |
| Model Binding | Implicit | Explicit attributes |
| Filters | Endpoint filters | Action/global filters |
Step-by-Step Guide
Step 1: Create Minimal API Project
dotnet new web -n MinimalApiDemo
cd MinimalApiDemo
Step 2: Define Endpoints
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.MapGet("/products", async (IProductRepository repo) =>
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.MapPost("/products", async (Product product, IProductRepository repo) =>
{
await repo.AddAsync(product);
return Results.Created($"/products/{product.Id}", product);
});
app.MapPut("/products/{id:int}", async (int id, Product product, IProductRepository repo) =>
{
if (id != product.Id) return Results.BadRequest();
await repo.UpdateAsync(product);
return Results.NoContent();
});
app.MapDelete("/products/{id:int}", async (int id, IProductRepository repo) =>
{
await repo.DeleteAsync(id);
return Results.NoContent();
});
app.Run();
Step 3: Group Related Endpoints
var products = app.MapGroup("/products");
products.MapGet("/", async (IProductRepository repo) => await repo.GetAllAsync());
products.MapGet("/{id:int}", async (int id, IProductRepository repo) => ...);
products.MapPost("/", async (Product product, IProductRepository repo) => ...);
Step 4: Add Validation & Filters
app.MapPost("/products", async (Product product, IProductRepository repo) =>
{
if (string.IsNullOrWhiteSpace(product.Name))
return Results.ValidationProblem(new Dictionary<string, string[]>
{
{ "Name", new[] { "Name is required" } }
});
await repo.AddAsync(product);
return Results.Created($"/products/{product.Id}", product);
});
Endpoint Filter Example:
products.AddEndpointFilter(async (context, next) =>
{
// Pre-processing
var result = await next(context);
// Post-processing
return result;
});
Step 5: OpenAPI Documentation
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
app.UseSwagger();
app.UseSwaggerUI();
Step 6: Error Handling
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new { error = "Internal server error" });
});
});
Performance Optimization
- Use source generators for JSON serialization
- Enable response caching for GET endpoints
- Leverage output caching (IOutputCacheStore)
builder.Services.AddOutputCache();
app.UseOutputCache();
app.MapGet("/products", async (IProductRepository repo) => await repo.GetAllAsync())
.CacheOutput(policy => policy.Expire(TimeSpan.FromMinutes(5)));
Authentication & Authorization
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/admin", () => "Admin only")
.RequireAuthorization("AdminPolicy");
Testing
[Fact]
public async Task GetProducts_ReturnsOk()
{
await using var application = new WebApplicationFactory<Program>();
var client = application.CreateClient();
var response = await client.GetAsync("/products");
response.EnsureSuccessStatusCode();
}
Troubleshooting
Issue: Route not matching
Solution: Check route constraints and parameter order
Issue: Dependency not injected
Solution: Verify service registration in builder.Services
Issue: JSON serialization error
Solution: Configure JsonSerializerOptions or use [JsonPropertyName]
Best Practices
- Use route groups to organize endpoints
- Apply filters for cross-cutting concerns
- Leverage built-in Results helpers (Ok, NotFound, Created, etc.)
- Document with OpenAPI/Swagger
Key Takeaways
- Minimal APIs reduce boilerplate for simple services.
- Built-in DI, routing, and model binding streamline development.
- Endpoint filters replace traditional middleware for targeted logic.
- Performance improvements make them ideal for serverless/containers.
Next Steps
- Migrate legacy MVC APIs to Minimal APIs
- Add rate limiting with ASP.NET Core middleware
- Deploy to Azure Container Apps
Additional Resources
Which API will you simplify first?