FluentValidation + Endpoint Filters: Validation That Enforces Itself

FluentValidation + Endpoint Filters: Validation That Enforces Itself

17 Sep 2026

FluentValidation + Endpoint Filters: Validation That Enforces Itself

Validation is one of those things every application needs, but few implement consistently. Some endpoints validate, some do not. Some use attributes, some use manual checks, some rely on the database to throw an exception. Brand Website V3 standardizes validation with FluentValidation validators and enforces them automatically through endpoint filters. Every request DTO has a corresponding validator. Every validator is auto-discovered and runs before the endpoint handler executes.

The Validator

// (Example from any module's Features assembly)
public class UpsertBlogPostCommandValidator : AbstractValidator<UpsertBlogPostCommand>
{
    public UpsertBlogPostCommandValidator()
    {
        RuleFor(request => request.Title).NotEmpty();
        RuleFor(request => request.CoverPhotoUrl).NotEmpty();
    }
}

Clean and expressive. The validator declares what the request must satisfy. Title must not be empty. CoverPhotoUrl must not be empty. These are business rules, not just data annotations. FluentValidation gives you the full power of a fluent API for complex validation scenarios.

Endpoint Filter Application

// Blog.Api/UpsertBlogPost/UpsertBlogPostEndpoint.cs
builder.MapPut("/", UpsertBlogPostAsync)
    .RequirePermission(nameof(Blog), (int)Permissions.ManagePosts)
    .AddEndpointFilter<EndpointValidationFilter<UpsertBlogPostRequest>>();

One line. The EndpointValidationFilter<TRequest> runs the corresponding FluentValidation validator before the endpoint handler executes. If validation fails, it returns 400 Bad Request automatically. The handler never sees an invalid request.

Global Exception Handling

Each route group also registers an ApiExceptionFilter for unhandled exceptions:

var routeGroup = routeBuilder
    .MapGroup(ApiRoutes.BlogPosts)
    .AddEndpointFilter<ApiExceptionFilter>();

This catches any exceptions that slip through (database errors, unexpected nulls) and returns structured error responses. Combined with the validation filter, you get a robust, layered error handling strategy.

Why It Works

  • Validation runs before the handler, not inside it. The handler receives only valid requests. It does not need to check for nulls or empty strings.
.AddEndpointFilter<EndpointValidationFilter<UpsertBlogPostRequest>>();
  • Validators are auto-discovered via assembly scanning. Add a new validator to any Features assembly and it is picked up automatically. No registration boilerplate.
RuleFor(request => request.Title).NotEmpty();
  • Consistent 400 responses across all endpoints. Every validation failure returns the same structured error response. Clients know exactly what to expect.
.AddEndpointFilter<ApiExceptionFilter>();
  • Validation is declarative and testable. The validator class is a plain C# class. You can unit test it without any HTTP pipeline.
var validator = new UpsertBlogPostCommandValidator();
var result = await validator.ValidateAsync(command);
Assert.IsTrue(result.IsValid);

Brand Website V3 enforces FluentValidation across all request DTOs with automatic endpoint filters. Every endpoint validates, every failure is consistent. See the full pattern at kiss-code.com.


Join the community

I continuously build, learn and experiment with innovative technology. Allow me to share what I learn, with you.

Newsletter

Allow me to share what I learn, with you.

Share

Support

An error has occurred. 🗙