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.