Structuring .NET Minimal APIs in a cleaned and well maintainable way

In the .NET world, a minimal API is a simplified way to create web services with less code and complexity. With minimal APIs, developers can build functional web APIs using just a few lines of code, focusing on the essential parts of their project. It’s a user-friendly approach in ASP.NET Core that reduces unnecessary details, making it easier for beginners to grasp and work on their API projects without getting overwhelmed by boilerplate code.
However, for beginners, structuring minimal APIs in a project can often be frustrating. Therefore, in this article, I aim to provide my personal opinions on how you can structure your minimal API code. While there are many ways to approach this, the suggestions presented here reflect my own perspective. I welcome your thoughts and comments on this blog post.
To start this article, let’s assume we have a project that manages the endpoints related to products.
If you create a new .NET Web API project, inside your Program.cs file by default, they have created a minimal API endpoint, something like the example below.

But since this is inside the Program.cs file, this approach won’t be scalable, as when you add more and more endpoints, the Program.cs file starts to grow. Therefore, we should move this endpoint into a separate file. Therefore we first we should move this out from the program.cs file.
To do that, in the root directory, I create a separate folder called ‘features’. I’m choosing to create a ‘features’ folder assuming that I have multiple features like Product Management, Orders Management, and Auth Management, etc.
Inside the ‘features’ folder, I create another subfolder called ‘Products’ and within that, create a new file called ‘Routes.cs’. Inside this file, I’m going to place my Products-related routes. To achieve this, we will have to create an extension method. This is because otherwise, we don’t have access to the WebApplication instance and in order to register these routes to the application we need the access to the WebApplication instance. To do that, create a static method called ‘RegisterProductsRoutes’ and pass this WebApplication app
as the parameter. Your code should look like the example below.
namespace StructuringMinimalApis.Features.Products
{
public static class Routes
{
public static void RegitserProductsRoutes(this WebApplication app)
{
}
}
}
Now, inside this method, you can the minimal API’s MapPost, MapGet, MapPut, and MapDelete methods using the app
. Therefore, since I need to register a Products create endpoint, my endpoint will look like the example below.
namespace StructuringMinimalApis.Features.Products
{
public static class Routes
{
public static void RegitserProductsRoutes(this WebApplication app)
{
app.MapGet("/products", () => Results.Ok(new[]
{
new Product { Id = 1, Name = "Product 1" },
new Product { Id = 2, Name = "Product 2" },
new Product { Id = 3, Name = "Product 3" },
}));
}
}
}
But you’ll have to take one additional step to get this to work. That means you need to go to your Program.cs file and add the following code.
app.RegitserProductsRoutes();
So when you save the changes and run the server, you will see your new endpoint has been registered. However, this approach has one overhead, which is that every time a new feature is added, you need to include this method in your Program.cs file, potentially causing it to grow. To avoid this, we can take one step further.
Doing an assembly search to autmatically register the endpoints
In this approach, I will create an interface which we can inherit with our routes classes, and after that, perform an assembly search to register the endpoints automatically. I know that sounds complicated, but it’s easier than you think.
To do that, I create a new folder named “Interfaces”. Inside this folder, I create an interface named IEndpointDefinition
. The IEndpointDefinition
interface should contain a single method named DefineEndpoints
that takes a WebApplication
instance as an argument. This method will be responsible for defining the endpoints for a particular feature.
Here’s how the IEndpointDefinition
interface looks like:
namespace StructuringMinimalApis.Interfaces
{
public interface IEndpointDefinition
{
void DefineEndpoints(WebApplication app);
}
}
This interface provides a standard method for each feature to define its endpoints. For each feature, there will be a class that implements IEndpointDefinition
. In the DefineEndpoints
method of this class, specific endpoints for that feature will be defined. Subsequently, in the Routes file, I will inherit that class with this interface. Once you do that, the previous routes file will look like this:
namespace StructuringMinimalApis.Features.Products
{
public class Routes : IEndpointDefinition
{
public void DefineEndpoints(WebApplication app)
{
// Define product-related endpoints here
}
}
}
Now, we should write an extension method that searches for classes inherited from this interface and registers them as routes.
To do that at the root level, create a folder called ‘extensions’. Inside that folder, create a file called ‘MinimalApiExtensions.cs’. After that you can write that extension method as follows.
using StructuringMinimalApis.Interfaces;
namespace StructuringMinimalApis.Extensions
{
public static class MinimalApiExtensions
{
public static void RegisterEndpointDefinitions(this WebApplication app)
{
var endpointDefinitions = typeof(Program).Assembly
.GetTypes()
.Where(t => t.IsAssignableTo(typeof(IEndpointDefinition)) && !t.IsAbstract
&& !t.IsInterface)
.Select(Activator.CreateInstance).Cast<IEndpointDefinition>();
foreach (var endpointdef in endpointDefinitions)
{
endpointdef.RegisterEndpoints(app);
}
}
}
}
A brief description about this code is it defines an extension method for WebApplication
called RegisterEndpointDefinitions
. When called, it searches for classes in the same assembly as Program
that implement IEndpointDefinition
. It instantiates these classes and calls their RegisterEndpoints
method with the WebApplication
instance as an argument.
Now we have written the extension method. As last step you should register this RegisterEndpointDefinitions
method inside the Program.cs
file. In order to do that go to the Program.cs
file and register it as below.
app.RegisterEndpointDefinitions();
Once you do that, you won’t need to register the endpoints one by one in the Program.cs file. Instead, once you inherit your Routes class with the IEndpointDefinition
interface we created, the application itself will search through the assembly and register your routes. In this way, you will be able to achieve a scalable routing layer inside your application.
Grouping endpoints inside Swagger Documentation
Another structuring technique I wanted to share is how you can organize your endpoints in Swagger based on categories.
Let’s consider the products routes file. Below, you can add the method called WithTags
at the end, and with that method, you can specify the category the endpoint belongs to. In this case, the category is ‘Products’. Once you do that and generate the Swagger documentation, you will see your endpoints categorized under the ‘Products’ category. This way, you can have organized Swagger documentation.
public void RegisterEndpoints(WebApplication app)
{
app.MapGet("/products/with-i-endpoints-definition", () => Results.Ok(new[]
{
new Product { Id = 1, Name = "Product 1" },
new Product { Id = 2, Name = "Product 2" },
new Product { Id = 3, Name = "Product 3" },
})).WithTags("Products");
}
This is how the swagger looks like now.

As you can see, there’s a group called Products
, and all of your endpoints will be listed under it. However, when you have several endpoints like this, you will need to call WithTags
at the end of each endpoint. To simplify this, you can write the code as shown below.
public void RegisterEndpoints(WebApplication app)
{
var products = app.MapGroup("/api/products").WithTags("Products");
products.MapGet("/with-i-endpoints-definition", () => Results.Ok(new[]
{
new Product { Id = 1, Name = "Product 1" },
new Product { Id = 2, Name = "Product 2" },
new Product { Id = 3, Name = "Product 3" },
}));
products.MapGet("/with-i-endpoints-definition/{id}", (int id) =>
{
var product = new Product { Id = id, Name = $"Product {id}" };
return Results.Ok(product);
});
}
}
Here, you first create a group with the path of your endpoints, and for that group, you call the WithTags
method. Once you do that, instead of directly mapping the endpoints to the app, you map them to that group.
In addition to the WithTags
method, there are some other methods that you can use to add more metadata to your Swagger documentation. For example, withDescription
. I highly encourage you to experiment with them.
Separating routes and implementations
To further simplify this code, you can keep the endpoints’ definition in this file and move the implementation to another file. To do that create two files called ProductsEndpointDefinition.cs
and ProductsEndpointsHandler.cs
Then you can seprate the routes and implementation like below.
ProductsEndpointDefinition.cs
using MaterialFlow.Api.Features.Products;
using StructuringMinimalApis.Interfaces;
namespace StructuringMinimalApis.Features.Products.WithIEndpointDefinition
{
public class ProductEndpointsDefintion : IEndpointDefinition
{
public void RegisterEndpoints(WebApplication app)
{
var products = app.MapGroup("/api/products").WithTags("Products");
products.MapGet("/with-i-endpoints-definition", ProductEndpointsHandler.GetProducts);
products.MapGet("/with-i-endpoints-definition/{id}", ProductEndpointsHandler.GetProduct);
}
}
}
ProductsEndpointsHandler.cs
using StructuringMinimalApis.Models;
namespace MaterialFlow.Api.Features.Products;
public class ProductEndpointsHandler
{
public static async Task<List<Product>> GetProducts()
{
var products = new List<Product>
{
new Product { Id = 1, Name = "Product 1" },
new Product { Id = 2, Name = "Product 2" },
new Product { Id = 3, Name = "Product 3" },
};
return products;
}
public static async Task<Product> GetProduct(int id)
{
var product = new Product { Id = id, Name = $"Product {id}" };
return product;
}
}
Now there’s a clear separation between your routes definitions and routes implementations. So, you will end up with a cleaned, maintainable routing layer. Those were the key points I wanted to demonstrate in this article. If you need access to this code, you can check out the GitHub Repo through the following link. Feel free to post your feedbacks or questions about this article as a comment.
Repo: https://github.com/pasindu-pr/structuring-minimal-apis