Building REST APIs with ASP.NET Minimal APIs

Building REST APIs with ASP.NET Minimal APIs

Minimal APIs in ASP.NET Core provide a streamlined approach to building HTTP APIs with minimal code and ceremony.

Getting Started

Here's a complete minimal API setup:

var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddScoped<IProductService, ProductService>();

var app = builder.Build();

// Configure middleware
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// Map endpoints
app.MapGet("/", () => "Hello World!");

app.Run();

CRUD Operations

Let's implement a complete CRUD API for products:

// Models
public record Product(int Id, string Name, decimal Price, int Stock);
public record CreateProductRequest(string Name, decimal Price, int Stock);
public record UpdateProductRequest(string Name, decimal Price, int Stock);

// Endpoints
app.MapGet("/api/products", async (IProductService service) =>
{
    var products = await service.GetAllAsync();
    return Results.Ok(products);
})
.WithName("GetProducts")
.WithOpenApi();

app.MapGet("/api/products/{id:int}", async (int id, IProductService service) =>
{
    var product = await service.GetByIdAsync(id);
    return product is not null
        ? Results.Ok(product)
        : Results.NotFound();
})
.WithName("GetProduct")
.WithOpenApi();

app.MapPost("/api/products", async (CreateProductRequest request, IProductService service) =>
{
    var product = await service.CreateAsync(request);
    return Results.Created($"/api/products/{product.Id}", product);
})
.WithName("CreateProduct")
.WithOpenApi();

app.MapPut("/api/products/{id:int}", async (int id, UpdateProductRequest request, IProductService service) =>
{
    var updated = await service.UpdateAsync(id, request);
    return updated ? Results.NoContent() : Results.NotFound();
})
.WithName("UpdateProduct")
.WithOpenApi();

app.MapDelete("/api/products/{id:int}", async (int id, IProductService service) =>
{
    var deleted = await service.DeleteAsync(id);
    return deleted ? Results.NoContent() : Results.NotFound();
})
.WithName("DeleteProduct")
.WithOpenApi();

Route Groups

Organize related endpoints using route groups:

var productsGroup = app.MapGroup("/api/products")
    .WithTags("Products");

productsGroup.MapGet("/", GetAllProducts);
productsGroup.MapGet("/{id:int}", GetProductById);
productsGroup.MapPost("/", CreateProduct);
productsGroup.MapPut("/{id:int}", UpdateProduct);
productsGroup.MapDelete("/{id:int}", DeleteProduct);

// With authentication
var adminGroup = app.MapGroup("/api/admin")
    .RequireAuthorization("AdminPolicy")
    .WithTags("Admin");

Validation with FluentValidation

Add request validation:

public class CreateProductValidator : AbstractValidator<CreateProductRequest>
{
    public CreateProductValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty()
            .MaximumLength(100);

        RuleFor(x => x.Price)
            .GreaterThan(0)
            .LessThan(10000);

        RuleFor(x => x.Stock)
            .GreaterThanOrEqualTo(0);
    }
}

// Validation filter
public class ValidationFilter<T> : IEndpointFilter where T : class
{
    private readonly IValidator<T> _validator;

    public ValidationFilter(IValidator<T> validator)
    {
        _validator = validator;
    }

    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var argument = context.Arguments.OfType<T>().FirstOrDefault();

        if (argument is null)
            return Results.BadRequest("Invalid request body");

        var result = await _validator.ValidateAsync(argument);

        if (!result.IsValid)
        {
            return Results.ValidationProblem(
                result.ToDictionary());
        }

        return await next(context);
    }
}

// Apply filter
app.MapPost("/api/products", CreateProduct)
    .AddEndpointFilter<ValidationFilter<CreateProductRequest>>();

Error Handling

Global error handling with problem details:

app.UseExceptionHandler(exceptionApp =>
{
    exceptionApp.Run(async context =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;

        var problem = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "An error occurred",
            Detail = app.Environment.IsDevelopment()
                ? exception?.Message
                : "Please try again later"
        };

        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(problem);
    });
});

Conclusion

Minimal APIs offer a clean, performant way to build APIs in ASP.NET Core. They're perfect for microservices, small APIs, and when you want to reduce boilerplate code.