DEV Community

Bruno Silva
Bruno Silva

Posted on

Minimal API endpoints getting messy? Let's get organized

I was a loud advocate and early adopter of dotnet's Minimal API framework for managing endpoints. It solves the problem of Controllers being verbose and cluttered, heavily geared for the MVC architecture and frequently being responsible for returning a fully rendered view from the backend. While also provides a much more flexible structure for your Rest methods. If you don't know what I'm talking about, make sure to check my other article, which should help you switch from dotnet controllers to endpoints.

We went from this:

using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using sandboxing.Models;

namespace sandboxing.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        public HomeController(ILogger<HomeController> logger)
        {
            _logger = logger;
        }

        public IActionResult Index()
        {
            return View();
        }

        public IActionResult Privacy()
        {
            return View();
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

To this:

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

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

app.Run();
Enter fullscreen mode Exit fullscreen mode

Now, I've been hearing people complain that it gets cluttered and messy once you start having more than a few routes to specify. You don't want to be touching your Program.cs all the time and nobody wants to clutter a critical file with 100 lines that look more or less the same. I personally know the pain, as one of the microservices in the app we're building at my job has over 100 endpoints. It's easy to get lost, but there's ways to work around the mess.

Here's the solution we came up with: create an interface, separate each logical domain into a different file with its own routes and each of these files implements the interface. Then during the application startup, we iterate through every single implementation to get the routes registered.

Sounds tricky? Check this example and you'll see it's pretty straightforward.

First, the interface with the RegisterRoutes signature, that receives an IEndpointRouteBuilder

public interface IEndpoint
{
    void RegisterRoutes(IEndpointRouteBuilder app);
}
Enter fullscreen mode Exit fullscreen mode

If you had separate controllers in the past, now you should have separate endpoints with a group of routes. The implementation will be much closer to the bare bones of Minimal APIs than with old school MVC Controllers.

This is a part of DeckEndpoint, one of the endpoints in my playing cards API Croupier (check it on my Github)

public class DeckEndpoint : IEndpoint    
{
    public void RegisterRoutes(IEndpointRouteBuilder app)
    {
        app.MapGet("/new-game", NewSession);
        app.MapGet("/draw-card", DrawCard);
        app.MapGet("/see-deck", SeeDeck);
        app.MapGet("/shuffle-deck", ShuffleDeck);
    }
}
Enter fullscreen mode Exit fullscreen mode

The second parameter in the MapGet method is a delegate. NewSession, DrawCard, SeeDeck and ShuffleDeck are the methods in that endpoint class.

Okay, now we just need to make sure every single instance of RegisterRoutes function is being called during the program startup. For that you'll need to add a line for each new endpoint in your dependency inversion definition, like this:

services.AddTransient<IEndpoint, DeckEndpoint>();
services.AddTransient<IEndpoint, TestEndpoint>();
Enter fullscreen mode Exit fullscreen mode

You can also achieve the same result using Reflection! But it's a little more tricky so I'll save that for another time.

Now that our endpoints are registered, we just need to iterate through the implementations of IEndpoint from the DI provider and make sure the RegisterRoutes method is called:

var app = builder.Build();

var endpoints = app.Services.GetServices<IEndpoint>().ToList();

endpoints.ForEach(e => e.RegisterRoutes(app));
Enter fullscreen mode Exit fullscreen mode

And we're done! Functionality-wise, every route from multiple endpoints is registered as if they were all listed in Program.cs. For reference, this is what my Program class looks like in the Croupier API:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDIServices();

var app = builder.Build();

app.UseOpenApi();
app.UseEndpoints();
app.Run();
Enter fullscreen mode Exit fullscreen mode

Slim!

I hope this helps you decide to let go of old Controller ways and embrace the new dotnet features. Drop a question if you feel like so, check out the Croupier API on my Github and make sure to follow me here and on my LinkedIn. Thanks and have a good one.

Top comments (1)

Collapse
 
shawnwildermuth profile image
Shawn Wildermuth

I have a blog post about this too, one thing I'd change is to use reflection to find the IEndpoints instead of needing to register every single one.