Title Banner
By Gary Butler 8 min read


🚩 Introduction

Minimal APIs; first introduced in .NET 6 have become a powerful tool for building lightweight, efficient, and clean web applications. Unlike traditional MVC endpoints, Minimal APIs focus on simplicity, allowing developers to achieve more with less boilerplate while maintaining flexibility and testability. In this blog, I will show how

to structure a Minimal API project using:

  • 🟒 Fluent-style Dependency Injection (DI)
  • 🟦 Handlers with associated services/repositories
  • 🟑 Endpoint filters for reusable logic; available from .NET 7 onwards

Finally, we’ll compare this approach to popular alternatives like FastEndpoints and traditional MVC endpoints to understand its unique advantages.


πŸ‘‹ Hello World

Using top-level commands and minimal APIs a Hello World example can be created using only a few lines of code:

var app = WebApplication
   .CreateBuilder(args)
   .Build();

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

app.Run();


🚧 Feature Based Example

Let’s explore how this works in a real-world scenario. We need to create an API for managing various features, focusing on the Products feature. The requirements are:

  • πŸ“Œ Endpoints
    • A GetProducts endpoint that returns a List<Product>.
  • πŸ›‘οΈ Security
    • All requests must include a valid API key.
  • πŸ—„οΈ Data Source
    • The data will be retrieved from a ProductRepository.
    • The ProductRepository can source data from multiple backends, such as:
      • A database.
      • An external API.

The endpoints for a feature will be split into a number of distinct layers to allow for a clear separation of concerns and enhancing maintainability. These layers are:

  • πŸ‘‰ Handler
    • Acts as the entry point for HTTP requests.
    • Validates inputs and maps request data to models.
    • Delegates business logic to the Service layer.
    • Returns appropriate HTTP responses with status codes.
  • βš™οΈ Service
    • Contains core business logic and applies rules/validations.
    • Orchestrates data retrieval and transformation processes.
    • Ensures modularity by interacting with repositories instead of data sources directly.
  • πŸ—ƒοΈ Repository
    • Manages data access and communication with databases or external APIs.
    • Encapsulates complex queries to abstract data layer details.
    • Provides a clean interface for the Service layer.


Step 1️⃣: Feature-Based Handlers

Each feature will have a handler that maps endpoints for its specific domain. This keeps routing logic modular and feature-focused.

ProductFeature/Handlers/ProductsHandler.cs

public static class ProductsHandler
{
   private static async Task<IResult> GetAllProducts(IProductService productService)
   {
      var products = await productService.GetAllProductsAsync();
      return Results.Ok(products);
   }

   public static IEndpointRouteBuilder MapProductEndpoints(this IEndpointRouteBuilder endpoints)
   {
      endpoints.MapGet("/products", () => GetAllProducts)
         .AddEndpointFilter<ProductsEndpointFilter>()
         .Produces<List<Product>>()
         .Produces(StatusCodes.Status400BadRequest)
         .WithName("GetProducts")
         .WithOpenApi();

      return endpoints;
   }
}


Step 2️⃣: Feature Service

Services encapsulate business logic with each feature maintaining its own service for a clear separation of responsibilities.

ProductFeature/Services/IProductService.cs

public interface IProductService
{
   Task<IEnumerable<Product>> GetAllProductsAsync();
}

ProductFeature/Service/ProductService.cs

public class ProductService(IProductRepository repository) : IProductService
{
   public async Task<IEnumerable<Product>> GetAllProductsAsync() =>
      await repository.GetAllProductsAsync();
}


Step 3️⃣: Feature Repository

Repositories handle data access. Each feature maintains its own repository for a clear separation of responsibilities. The repository will also adapt the underlying data to its own model so as to not leak the underlying data structures.

ProductFeature/Repository/IProductRepository.cs

public interface IProductRepository
{
   Task<IEnumerable<Product>> GetAllProductsAsync();
}

ProductFeature/Repository/IProductRepository.cs

public interface IProductRepository
{
   Task<IEnumerable<Product>> GetAllProductsAsync();
}

ProductFeature/Repository/ProductRepository.cs

public class ProductRepository : IProductRepository
{
   public async Task<IEnumerable<Product>> GetAllProductsAsync()
   {
      // Mocked data return
      var products = new List<Product>
      {
         new() { Id = 1, Name = "Laptop", Price = 1200.24m },
         new() { Id = 2, Name = "Smartphone", Price = 800.56m }
      };

      return await Task.FromResult(products);
   }
}

ProductFeature/Repository/Models/Product.cs

public record Product
{
   public required int Id { get; init; }
   public required string Name { get; init; }
   public required decimal Price { get; set; }
}


Step 4️⃣: Using Endpoint Filters

Endpoint filters enable reusable logic for Minimal APIs, such as validation or authentication.

EndpointFilters/ValidationFilter.cs

public class ProductsEndpointFilter : IEndpointFilter
{
   public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) =>
      context.HttpContext.Request.Query.ContainsKey("apiKey")
         ? await next(context) // Proceed to the next filter or endpoint
         : Results.BadRequest("API Key is missing");
}


Step 5️⃣: Fluent Endpoint Registration

Register each feature’s handler in Program.cs for a clean, modular setup.

Program.cs

var app = WebApplication
   .CreateBuilder(args)
   .AddCoreConfiguration()
   .AddUserSecretsIfNeeded()
   .AddProductConfiguration()
   .Build()
   .UseMiddleware()
   .MapEndpoints();

app.Run();


Step 6️⃣: Fluent Dependency Injection

Create an extension for each features dependency injection configuration.

Configuration/ProductConfigurationExtensions.cs

public static WebApplicationBuilder AddProductConfiguration(this WebApplicationBuilder builder)
{
   builder.Services.AddScoped<IProductRepository>(
      _ => new ProductRepository()
   );

   builder.Services.AddScoped<IProductService>(
      x => new ProductService(
         x.GetRequiredService<IProductRepository>())
   );

   return builder;
}

πŸ—‚οΈ Project Structure

Here’s how a feature-based folder structure might look:

src/
β”œβ”€β”€ πŸ“ Configuration/
β”‚   β”œβ”€β”€ πŸ“ CoreConfigurationExtensions.cs
β”‚   β”œβ”€β”€ πŸ“ EndPoints.cs
β”‚   β”œβ”€β”€ πŸ“ UserSecretsExtensions.cs
β”‚   β”œβ”€β”€ πŸ“ WebApplicationBuilderExtensions.cs
β”‚
β”œβ”€β”€ πŸ“ Features/
β”‚   β”œβ”€β”€ πŸ“ Configuration/
β”‚        β”œβ”€β”€ πŸ“ FeaturesConfiguration.cs
β”‚
β”œβ”€β”€ πŸ“ HealthCheckFeature/
β”‚   β”œβ”€β”€ πŸ“ Handlers/
β”‚        β”œβ”€β”€ πŸ“ HealthCheckHandler.cs
β”‚
β”œβ”€β”€ πŸ“ ProductFeature/
β”‚   β”œβ”€β”€ πŸ“ Configuration/
β”‚        β”œβ”€β”€ πŸ“ ProductsConfiguration.cs
β”‚   β”œβ”€β”€ πŸ“ EndpointFilters/
β”‚        β”œβ”€β”€ πŸ“ ProductsEndpointFilter.cs
β”‚   β”œβ”€β”€ πŸ“ Handlers/
β”‚        β”œβ”€β”€ πŸ“ ProductsHandler.cs
β”‚   β”œβ”€β”€ πŸ“ Services/
β”‚        β”œβ”€β”€ πŸ“ IProductService.cs
β”‚        β”œβ”€β”€ πŸ“ ProductService.cs
β”‚   β”œβ”€β”€ πŸ“ Repository/
β”‚        β”œβ”€β”€ πŸ“ Models/
β”‚              β”œβ”€β”€ πŸ“ Product.cs
β”‚        β”œβ”€β”€ πŸ“ IProductRepository.cs
β”‚        β”œβ”€β”€ πŸ“ ProductRepository.cs
β”‚
β”œβ”€β”€ πŸ“ Program.cs


πŸ† Benefits of This Approach

1️⃣ ⭐ Feature-Based Modularity

Each feature (e.g., Product, Order) is self-contained with its own handlers, services, and repositories. This makes it easier to:

  • Navigate the codebase.
  • Add new features without affecting others.
  • Scale the application as it grows.

2️⃣ πŸ§ͺ Testability

  • Handlers, services, and repositories are decoupled and easily testable.
  • Dependency Injection ensures mock implementations can be injected during unit testing.

3️⃣ 🧩 Flexibility with EndpointFilters

EndpointFilters, such as ProductsEndpointFilter, allows separation of middleware between endpoints.

4️⃣ 🧹 Cleaner Program.cs

Using fluent-style configuration keeps the Program.cs file clean and focused on application-level setup. Feature-specific logic is delegated to handlers.

5️⃣ βš–οΈ Comparison

Below is a comparison of three popular approaches for building APIs in .NET — MVC, Minimal APIs, and FastEndpoints β€” each offering distinct patterns and capabilities.

✨ Feature πŸ”· MVC 🟒 Minimal APIs 🟠 FastEndpoints
🎯 Use Case Full-featured web applications (Views, Controllers, Models) Lightweight, small to medium APIs Lightweight APIs with structured approach
🧩 Framework ASP.NET Core MVC ASP.NET Core Minimal APIs ASP.NET Core with FastEndpoints library
πŸ›£οΈ Routing Attribute-based (e.g., [HttpGet]) or conventional Fluent, inline routing or via Extensions Declarative, endpoint-based with attributes
πŸ“ Boilerplate Code Moderate to high, requires controllers and models Minimal, customizable for separation of concerns Minimal, but enforces endpoint structures
πŸš€ Performance Moderate (slightly more overhead) Extremely high, possibly faster than FastEndpoints Very high, close to Minimal APIs
⚑ Async Support Fully supports async Fully supports async Fully supports async
πŸ”’ Type Safety Strong, compile-time type safety Strong, compile-time type safety Strong, compile-time type safety
🧩 Dependency Injection Built-in, well-integrated Built-in, well-integrated Built-in, well-integrated
βœ… Validation Manual or FluentValidation Manual or FluentValidation Built-in request and response validation
πŸ“š OpenAPI Documentation Requires Swashbuckle/NSwag integration Requires Swashbuckle/NSwag integration Built-in OpenAPI documentation
πŸ”€ Separation of Concerns Enforced via Controllers, Models, Views Fully achievable through custom structure Encouraged via structured endpoints
πŸ“ˆ Scalability High, suitable for large, complex applications Suitable for small to medium-sized applications Suitable for small to medium-sized applications
🌏 Community Large, long-established ASP.NET MVC community Growing popularity with Minimal APIs users Smaller but active and focused community
πŸŽ“ Learning Curve Moderate to high, especially for beginners Low to moderate, customizable for use cases Low to moderate, requires understanding FastEndpoints
πŸ… Key Differentiators Separation of concerns with Views, Controllers, Models Inline, fast, customizable for large-scale APIs Adds validation and structure to Minimal APIs

πŸ”‘ Key Features of Minimal API’s

1️⃣ Separation of Concerns in Minimal APIs:

Minimal APIs allow separation of concerns by moving logic into services, handlers, or dedicated classes, mimicking MVC-like architecture while remaining lightweight.

2️⃣ Performance:

Minimal APIs are extremely fast because of their lightweight architecture and reduced middleware pipeline, often faster than frameworks like FastEndpoints.

3️⃣ Use Cases:

Minimal APIs can scale to more complex applications with proper organization (e.g., using service classes, extensions for modularity).

🎯 Conclusion

By structuring Minimal APIs with a feature-based organization, DI, and reusable endpoint filters, you create a clean and scalable architecture that is easy to maintain and test. This approach leverages the strengths of .NET 8, providing a modern, performant alternative to MVC and competing frameworks like FastEndpoints.

Adopting this modular design ensures your Minimal APIs remain efficient and maintainable as your application grows.