Minimal APIs in .NET: Building Lightweight Web Services

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?