Building Clean and Scalable APIs with .NET 8 Minimal API and Vertical Slice Architecture
A lean template that combines Minimal API, MediatR, FluentValidation, and Vertical Slice for fast, testable services.
Modern .NET 8 simplifies API development with Minimal API while staying production-ready. By pairing it with Vertical Slice Architecture, MediatR, and FluentValidation, each feature lives in its own folder, stays testable, and avoids bloated service layers.
Why Vertical Slice over classic layers?
- Each feature is self-contained (request, validation, handler, endpoint).
- No swollen service layer; changes are localized.
- Natural CQRS: commands and queries have separate handlers.
- Easier testing: validators, handlers, and endpoints can be tested in isolation.
Folder sketch:
VerticalApi
└── Features
└── Users
└── CreateUser
CreateUserRequest.cs
CreateUserValidator.cs
CreateUserHandler.cs
CreateUserEndpoint.cs
└── Program.cs
Project setup
dotnet new webapi -n VerticalApi
cd VerticalApi
dotnet add package MediatR
dotnet add package FluentValidation
1) Request
public record CreateUserRequest(string Email, string Password) : IRequest<IResult>;
2) Validation (FluentValidation)
using FluentValidation;
public class CreateUserValidator : AbstractValidator<CreateUserRequest>
{
public CreateUserValidator()
{
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress();
RuleFor(x => x.Password)
.NotEmpty()
.MinimumLength(6);
}
}
3) Handler (MediatR)
using MediatR;
public class CreateUserHandler : IRequestHandler<CreateUserRequest, IResult>
{
public async Task<IResult> Handle(CreateUserRequest request, CancellationToken ct)
{
// Simulated persistence
await Task.Delay(50, ct);
return Results.Ok(new
{
message = "User successfully created",
user = new { request.Email }
});
}
}
4) Endpoint (Minimal API extension)
public static class CreateUserEndpoint
{
public static void MapCreateUserEndpoint(this IEndpointRouteBuilder app)
{
app.MapPost("/users", async (
CreateUserRequest req,
IMediator mediator,
IValidator<CreateUserRequest> validator) =>
{
var validation = await validator.ValidateAsync(req);
if (!validation.IsValid)
return Results.BadRequest(validation.Errors);
return await mediator.Send(req);
})
.WithName("CreateUser")
.WithOpenApi();
}
}
5) Program.cs
using FluentValidation;
using MediatR;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMediatR(typeof(Program));
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
var app = builder.Build();
app.MapCreateUserEndpoint();
app.Run();
Testing the API
Request:
POST /users
Content-Type: application/json
{
"email": "[email protected]",
"password": "secret123"
}
Response:
{
"message": "User successfully created",
"user": { "email": "[email protected]" }
}
Why this is production-friendly
- Feature-based development: each slice is independent and easy to evolve.
- Controller-free: endpoints stay close to their handlers and validation.
- CQRS-ready: commands and queries can stay separate without ceremony.
- Testable: validator tests, handler tests, and endpoint tests are straightforward.
- Lean but clean: avoids heavy layers while keeping responsibilities clear.
Next steps you can add
- Persistence: EF Core repositories with PostgreSQL or SQL Server.
- Auth: JWT bearer authentication and authorization policies.
- Observability: Serilog + OpenTelemetry for logs, metrics, and traces.
- Resilience: exception middleware, validation problem details, health checks.
- Separation: dedicated Query handlers alongside Command handlers for read models.