How to structure your Minimal API in .NET?

The Minimal API is a new approach in .NET 6 for creating REST APIs that prioritizes simplicity, reducing code verbosity, configuration, and formalities.

7 months ago   •   10 min read

By Pavle Davitkovic
Table of contents

In today's software architecture landscape, APIs (Application Programming Interfaces) have evolved into the vital connectors of applications, facilitating seamless communication, data sharing, and collaboration.

With the debut of .NET 6, Microsoft has not only introduced tools that boost productivity, performance, and flexibility but has also pioneered the concept of Minimal APIs, which revolutionizes how we create APIs by offering a streamlined and intuitive approach.

By staying until the end, you'll gain insights into:

  • What is the Minimal API?
  • How to create a Minimal API?
  • How to structure it to be clean and maintainable

Now, let's delve into the realm of minimalism!

Prerequisites

To follow along you will need the following:

For those who prefer a lighter setup, Visual Studio Code is a suitable alternative.

What is the Minimal API?

Minimal API is a streamlined approach to building REST APIs in .NET, focusing on brevity in code, minimal configuration, and a significant reduction in the usual formalities associated with traditional methods. With just three lines of code, you can bring an API to life!

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

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

app.Run();

What are the differences between Minimal API and controller-based API?

In a functional context, both approaches serve the purpose of creating REST APIs effectively. However, Minimal API shines in several aspects where it outperforms the controller-based approach:

  1. Simplicity: Minimal API prioritizes clear and concise code, simplifying understanding and enabling a sharper focus on core functionality.
  2. Reduced Overhead: With fewer configuration requirements and less boilerplate code, Minimal API allows for a more concentrated effort on writing the essential logic of your application.
  3. Performance: The reduced overhead in Minimal API can lead to enhanced performance, as there's less unnecessary code to execute or process.
  4. Better fit for Vertical slice architecture: Minimal API aligns perfectly with vertical slice architecture, which centers around building applications around specific features, often involving just one endpoint. This focus on feature logic aligns seamlessly with Minimal API's principles.

However, it's essential not to dismiss controllers entirely because:

  1. Larger Feature Set: Controllers offer a broader feature set, including support for model binding, validation, built-in view rendering, and more.
  2. Better fit for Enterprise Applications: In complex applications with numerous dependencies, endpoints, authorization, authentication, middlewares, filters, and other advanced features, the controller-based approach may be more manageable and suitable.

The best use cases for each approach depend on the specific requirements. Minimal API excels in simple CRUD apps or lightweight services like logging, whereas controllers shine in complex, enterprise-grade applications.

Treblle can enhance both Minimal API and controller-based approaches by streamlining the security and monitoring aspects of REST API development. For Minimal API setups, where simplicity and performance are key, Treblle integrates seamlessly, adding robust security checks and real-time threat assessments without burdening the lightweight architecture. This ensures your streamlined API remains efficient while being protected against vulnerabilities and attacks.


Ready to Transform Your API Development?

Step into the future with Treblle, where managing and monitoring APIs becomes a breeze. Simplify your workflow, enhance security, and gain insightful analytics with just a few clicks. Elevate your API projects and see the difference today. Why wait when efficiency is at your fingertips?

Talk to our API expert

How to create a Minimal API?

Creating a Minimal API closely mirrors the traditional approach, so you should encounter no significant challenges. It is a straightforward procedure that can be accomplished in just a few easy steps.

Let's get started:

  1. Open Visual Studio and select the ASP.NET Core Web API
Web API template

2. Provide a preferred name for your project and select the location where you wish to store it

Providing project name

3. For the final step, choose the targeted framework (you can leave it as the latest version), ensure that the "Configure for HTTPS" and "Enable OpenAPI support" checkboxes are checked, and, most importantly, leave the checkbox "Use controllers (uncheck to use Minimal API)" unchecked. Then, click the "Create" button.

Chose framework, and API type

Let's modify the Program.cs file to resemble a basic CRUD application:

public static List<User> Users = new()
{
    new User()
    {
        Id = 1,
        FirstName = "Callie",
        LastName = "Hackforth",
        BirthDate = new DateOnly(1995, 10, 3)
    },
    new User()
    {
        Id = 2,
        FirstName = "Odell",
        LastName = "Blowes",
        BirthDate = new DateOnly(1984, 4, 7)
    },
    new User()
    {
        Id = 3,
        FirstName = "Callie",
        LastName = "Corrett",
        BirthDate = new DateOnly(1991, 3, 4)
    }
};

var builder = WebApplication.CreateBuilder(args);

builder.Services
       .AddEndpointsApiExplorer()
       .AddSwaggerGen();
       
var app = builder.Build();

app.UseHttpsRedirection();

app.MapGet("", () => Collections.Users);

app.MapGet("/{id}", (int id) => Collections.Users
                                           .FirstOrDefault(user => user.Id == id));

app.MapPost("", (User user) => Collections.Users.Add(user));

app.MapPut("/{id}", (int id, User user) =>
{
    User currentUser = Collections.Users
                                  .FirstOrDefault(user => user.Id == id);
    
    currentUser.FirstName = user.FirstName;
    currentUser.LastName = user.LastName;
    currentUser.BirthDate = user.BirthDate;
});

app.MapDelete("/{id}", (int id) =>
{
    var userForDeletion = Collections.Users
                                     .FirstOrDefault(user => user.Id == id);
    
    Collections.Users.Remove(userForDeletion);
});

app.Run();

public class User
{
    public int Id { get; init; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateOnly BirthDate { get; set; }
}

As evident, everything resides within that single location, including registered services, endpoints, and even models. However, this approach remains acceptable only up to a certain point.

Consider this scenario: You decide to incorporate a repository pattern, followed by the introduction of background jobs, a handful of middleware, and an additional set of endpoints catering to various resources.

Ultimately, Program.cs can transform into a tangled mass of code, leading to issues such as:

  1. Challenging Maintainability: It becomes increasingly difficult to maintain.
  2. Unreadable Code: The code becomes less readable and comprehensible.
  3. Violations of the Single Responsibility Principle: It violates the Single Responsibility Principle by trying to manage too many responsibilities within one file.

However, two solutions can greatly enhance the cleanliness and maintainability of Minimal API:

  1. Endpoint Grouping (Introduced in .NET 7): This allows you to logically group your endpoints, offering a cleaner structure.
  2. Carter (External Library): By leveraging the Carter library, you can achieve improved organization and maintainability in your Minimal API project.

I will provide a comprehensive discussion of these solutions and guide you through their implementation in your current project.

Endpoint grouping

The first approach to achieving cleaner and more organized endpoints involves grouping them by feature.

In .NET 6, this could be accomplished using extension methods on the IEndpointRouteBuilder class. However, with the release of .NET 7, a new feature known as "Endpoint grouping" has been introduced. This feature enables you to apply constraints and rules to a collection of Minimal API endpoints simultaneously.

No more theory – let's streamline this API!

Before delving into endpoint modifications, let's tidy up the Program.cs file.

Create a directory named "Extensions" and within it, establish a static class named "Configuration.cs". This class will house two methods in this context:

  • RegisterServices - This method will encompass the registration of all services in the dependency injection container.
  • RegisterMiddlewares - This method will handle the registration of all middleware.
public static class Configuration
{
    public static void RegisterServices(this WebApplicationBuilder builder)
    {
        builder.Services
               .AddEndpointsApiExplorer()
               .AddSwaggerGen();
    }

    public static void RegisterMiddlewares(this WebApplication app)
    {
        if (app.Environment.IsDevelopment())
        {
            app.UseSwagger()
               .UseSwaggerUI();
        }

        app.UseHttpsRedirection();
    }
}

The registration in Program.cs appears as follows:

var builder = WebApplication.CreateBuilder(args);

builder.RegisterServices();

var app = builder.Build();

app.RegisterMiddlewares();

app.Run();

For the sake of simplicity, as I am using an in-memory collection, I will also relocate the static collection to another folder and file.

Establish a "Data" folder comprising a static class named "Collections" and a static list titled "Users."

public static class Collections
{
    public static List<User> Users = new List<User>()
    {
        new User()
        {
            Id = 1,
            FirstName = "Callie",
            LastName = "Hackforth",
            BirthDate = new DateOnly(1995, 10, 3)
        },
        new User()
        {
            Id = 2,
            FirstName = "Odell",
            LastName = "Blowes",
            BirthDate = new DateOnly(1984, 4, 7)
        },
        new User()
        {
            Id = 3,
            FirstName = "Callie",
            LastName = "Corrett",
            BirthDate = new DateOnly(1991, 3, 4)
        },
        new User()
        {
            Id = 4,
            FirstName = "Channa",
            LastName = "McKeggie",
            BirthDate = new DateOnly(1985, 11, 13)
        },
        new User()
        {
            Id = 5,
            FirstName = "Angelita",
            LastName = "Jubert",
            BirthDate = new DateOnly(1990, 1, 9)
        }
    };
}

Within the same folder, include the "User" model

public class User
{
    public int Id { get; init; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    internal DateOnly BirthDate { get; set; }
}

The initial phase of cleanup is complete; now, let's address the endpoints:

  1. Create a folder at the project's root called "Endpoints"
Endpoint folder for grouped endpoints

2. Inside the "Endpoints" folder, create a class with the corresponding name, which in our case will be "Users." This class must be static since we will be including an extension method that utilizes the endpoint grouping feature

public static class Users
{
    public static void RegisterUserEndpoints(this IEndpointRouteBuilder routes)
    {
        // Grouped endpoint will go here
    }
}

It's good practice to name methods by appending the feature name followed by "Endpoints" (e.g., UserEndpoints).

3. Include grouped endpoints within the previously added method

public static class Users
{
    public static void RegisterUserEndpoints(this IEndpointRouteBuilder routes)
    {
      var users = routes.MapGroup("/api/v1/users");
      
      users.MapGet("", () => Collections.Users);
      
      users.MapGet("/{id}", (int id) => Collections.Users
                                                   .FirstOrDefault(user => user.Id == id));
                                                   
      users.MapPost("", (User user) => Collections.Users.Add(user));

      users.MapPut("/{id}", (int id, User user) =>
      {
        User currentUser = Collections.Users
                                      .FirstOrDefault(user => user.Id == id);

        currentUser.FirstName = user.FirstName;
        currentUser.LastName = user.LastName;
        currentUser.BirthDate = user.BirthDate;
     });
     
     users.MapDelete("/{id}", (int id) =>
     {
        var userForDeletion = Collections.Users
                                         .FirstOrDefault(user => user.Id == id);

        Collections.Users.Remove(userForDeletion);
     });
   }
}

The MapGroup method is applied to an IEndpointRouteBuilder variable, which contains all the application's routes. It accepts prefixes as its sole parameter. Consequently, all endpoints added to this group will have the specified prefixes prepended, along with any additional information provided.

To complete the refactoring process, simply register the method in Program.cs, similar to how you registered services and middlewares. That's all there is to it!

var builder = WebApplication.CreateBuilder(args);

builder.RegisterServices();

var app = builder.Build();

app.RegisterMiddlewares();

app.RegisterUserEndpoints(); // <-- Add this line

What are the advantages of this approach?

  • Supported out-of-the-box by the framework
  • Promotes cleaner and more maintainable code
  • Consolidates all changes within a single file
  • Cross-cutting concerns can be directly attached to a group

One drawback of this approach is that the codebase can expand significantly when numerous endpoint groups are involved.
However, if you prefer a more flexible approach and enjoy working with modules, then Carter is the choice for you.

Carter

As per the GitHub readme file:

Carter is a framework that is a thin layer of extension methods and functionality over ASP.NET Core allowing the code to be more explicit and most importantly more enjoyable.

Essentially, it follows a similar approach to the first one, but with some additional features such as:

  1. Automatic registration of all interface implementations for Carter components into ASP.NET Core DI.
  2. Inclusion of FluentValidation extensions to validate incoming HTTP requests, a feature not found in ASP.NET Core Minimal APIs.

The setup can be completed in three straightforward steps:

  • Add the library from NuGet.
  • Register the Carter service into the DI container: builder.Services.AddCarter();
  • Add the middleware: app.MapCarter();
 public static void RegisterServices(this WebApplicationBuilder builder)
 {
     builder.Services
            .AddEndpointsApiExplorer()
            .AddCarter() // <-- Add this line
            .AddSwaggerGen();
 }

 public static void RegisterMiddlewares(this WebApplication app)
 {
     if (app.Environment.IsDevelopment())
     {
         app.UseSwagger()
            .UseSwaggerUI();
     }

     app.MapCarter(); // <-- And this
 }

The AddCarter method handles the necessary services provided by Carter, while MapCarter scans for all endpoints that implement the ICarterModule interface and integrates them with the existing endpoints.

As mentioned earlier, Carter operates with modules, and all endpoints will be encapsulated within their respective modules.

Carter folder structure

The Module class should inherit the ICarterModule interface, which exposes only one method (AddRoutes). However, it is advisable to inherit from the CarterModule class, which provides additional features.

public class Endpoints : CarterModule
{
    public override void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapGet("", () => Collections.Users);

        app.MapGet("/{id}", (int id) => Collections.Users
                                                   .FirstOrDefault(user => user.Id == id));

        app.MapPost("", (User user) => Collections.Users.Add(user));

        app.MapPut("/{id}", (int id, User user) =>
        {
            User currentUser = Collections.Users
                                          .FirstOrDefault(user => user.Id == id);

            currentUser.FirstName = user.FirstName;
            currentUser.LastName = user.LastName;
            currentUser.BirthDate = user.BirthDate;
        });

        app.MapDelete("/{id}", (int id) =>
        {
            var userForDeletion = Collections.Users
                                             .FirstOrDefault(user => user.Id == id);

            Collections.Users.Remove(userForDeletion);
        });
    }
}

Features from the base class are incorporated through the constructor within the module, and they pertain to the endpoints contained within it.
Some of these features encompass:

  1. Rate limiting
  2. Authorization
  3. CORS
  4. Endpoint grouping
  5. Pre and Post endpoint actions
 public Endpoints() : base("api/v1/users")
 {
     this.RequireRateLimiting("fixedWindow")
         .WithCacheOutput("CacheOutput");
 }

Upon executing the application following the completion of the setup, you will achieve the same result as with the initial approach.

What to do next after the successful restructuring of the API?

By employing this approach, you've addressed just one aspect of the puzzle. What about documenting your API, including all endpoints and their associated contracts? How about ensuring security and optimizing performance? There's a multitude of considerations for crafting a robust API.

Fortunately, the solution to your challenges lies in Treblle.


Treblle offers comprehensive coverage, spanning from observability to automated API documentation. The best part is that it seamlessly integrates with Minimal API.


Ready to Transform Your API Development?

Step into the future with Treblle, where managing and monitoring APIs becomes a breeze. Simplify your workflow, enhance security, and gain insightful analytics with just a few clicks. Elevate your API projects and see the difference today. Why wait when efficiency is at your fingertips?

Try Treblle for free!

Conclusion

In this article, we've explored how to structure your Minimal API in .NET, discussing two distinct approaches: endpoint grouping and the use of Carter. Regardless of your choice, it remains crucial to organize your Minimal API in a manner that promotes ease of comprehension and maintenance. By adhering to the guidance presented in this article, you can develop a Minimal API that is both efficient and effective.

I appreciate both approaches, and the choice often hinges on the specific requirements of the current project. For a straightforward and simplified approach, endpoint grouping proves to be a viable option. On the other hand, if flexibility and additional features are desired, Carter is an excellent selection. Nevertheless, it's worth noting that Carter is a library built upon .NET features already present in the framework, so it's plausible that they will eventually find their way into Minimal APIs.

To conclude, I'd like to offer a few more tips for structuring your Minimal API:

  1. Implement dependency injection to mitigate tight coupling
  2. Adhere to the Single Responsibility Principle
  3. Utilize feature folders for improved organization

Spread the word

Keep reading