DEV Community

Cover image for Validating Minimal APIs: Best Practices and Approaches.
Spyros Ponaris
Spyros Ponaris

Posted on • Edited on

Validating Minimal APIs: Best Practices and Approaches.

In my previous article, I focused on benchmarking Minimal APIs against Controllers.

Want to learn more about Minimal API Performance Benchmark? Read my post: Minimal API Performance Benchmark .

GitHub Repository

You can find the full project and source code on GitHub.

The journey continues, and the next step is to explore how to implement validation in Minimal APIs.

Key Consideration: No ModelState

One important thing to note is that Minimal APIs do not have a ModelState like controllers do. This raises the question: how can we validate a simple DTO?

A DTO (Data Transfer Object) is a simple object used to transfer data between different layers or parts of an application. It is primarily used to encapsulate data and reduce dependencies between components

Approach 1: Data Annotations

A common and straightforward approach is to decorate the model with DataAnnotation attributes. For example:

public class UserDto
{
    [Required]
    public string Name { get; set; }

    [Required]
    [Range(18, 99)]
    public int Age { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

In the Minimal API, you can check for validation manually:

app.MapPost("minimalapi/create", ([FromBody] UserDto user) =>
        {
            var validationResults = new List<ValidationResult>();

            var context = new ValidationContext(user);

            if (!Validator.TryValidateObject(user, context, validationResults, true))
            {
                var errors = validationResults.ToDictionary(
                            v => v.MemberNames.FirstOrDefault() ?? "Error",
                            v => new string[] { v.ErrorMessage! });

                return Results.BadRequest(new { Message = "Validation failed", Errors = errors });
            }

            return Results.Ok($"User {user.Name} created successfully!");
        });
Enter fullscreen mode Exit fullscreen mode

Approach 2: Middleware Validation

Another approach is to use middleware for validation. This allows us to centralize validation logic and keep endpoint handlers clean.

Example middleware:

Image description

Explanation of the ValidationMiddleware Code
This ASP.NET Core middleware validates checks if the request is POST or PUT, enables buffering, reads and **deserializes **the body into UserDto, validates it, and proceeds if valid. If validation fails, it returns a 400 Bad Request response with detailed validation errors in JSON format.

What is Middleware?

Middleware in ASP.NET Core is a component that processes HTTP requests and responses in the application's request pipeline. Middleware can:

  • Handle authentication, logging, and error handling.
  • Modify or reject requests before they reach controllers.
  • Execute business logic before sending a response.

*How to Register Middleware in ASP.NET Core : *

app.UseMiddleware<ValidationMiddleware>();

Approach 3: FluentValidation

For a more robust and scalable validation mechanism, we can use FluentValidation.

Install FluentValidation:

dotnet add package FluentValidation.AspNetCore
Enter fullscreen mode Exit fullscreen mode

Define a validator:

public class UserDtoValidator : AbstractValidator<UserDto>
{
    public UserDtoValidator()
    {
        RuleFor(u => u.Name).NotEmpty().WithMessage("Name is required.");
        RuleFor(u => u.Age).NotEmpty().InclusiveBetween(18, 99).WithMessage("Age must be between 18 and 99.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Register FluentValidation in DI:

builder.Services.AddValidatorsFromAssemblyContaining<UserDtoValidator>();
Enter fullscreen mode Exit fullscreen mode

Use the validator in the Minimal API:

app.MapPost("minimalapi/createV1", ([FromBody] UserDto user, IValidator<UserDto> validator) =>
        {
            var validationResult = validator.Validate(user);

            if (!validationResult.IsValid)
            {
                var errors = validationResult.Errors
                    .GroupBy(e => e.PropertyName)
                    .ToDictionary(
                        g => g.Key,
                        g => g.Select(e => e.ErrorMessage).ToArray());
                return Results.BadRequest(new { Message = "Validation failed", Errors = errors });
            }

            return Results.Ok($"User {user.Name} created successfully!");
        });
Enter fullscreen mode Exit fullscreen mode

Conclusion

Minimal APIs do not provide ModelState, so validation needs to be handled differently. Here are three approaches:

DataAnnotations: Simple but requires manual validation.

Middleware: Centralized validation but requires custom implementation.

FluentValidation: Scalable and clean, recommended for larger applications.

Each approach has its use case, but for a maintainable and scalable solution, FluentValidation is the best choice.

What are your thoughts? Have you tried different validation approaches in Minimal APIs?

References

1. DataAnnotations

2. Middleware

3. FluentValidation

Top comments (3)

Collapse
 
manchicken profile image
Mike Stemle

I really would have preferred to see more code explanation. Also, it's difficult to read code when it's presented as an image. Consider leveraging syntax highlighting features in Markdown rather than using images.

Collapse
 
stevsharp profile image
Spyros Ponaris

Okay, I will do that soon. You can also take a look at the GitHub repository.

Collapse
 
stevsharp profile image
Spyros Ponaris

Hey Mike, I’ve updated this article with a short explanation . I hope it helps!
If it's still unclear, I can provide a more detailed article on Middleware and Fluent Validation.
Have a nice day!