9 minutes read

Three ways to structure .NET Minimal APIs

Any new project since .NET 6 feels like a slap in the face for seasoned .NET developers. Not a single Controller in sight, only experienced developers scrambling to find a reasonable way to structure their projects now that their architecture is no longer dictated by the familiar WebAPI Controllers.

Scrabble words reading "Order" and "Chaos"

Gone are the days where we didn’t have to make any tough decisions ourselves. This isn’t just about project structures; we’re left in a landscape where guidance has left the chat and the rules are yet to be written.

Let me guide you through some of the options you have in taming all the “freedom” Minimal APIs gave us — whether you’re managing a beast of a system or a more modest API. We’ll explore:

  • Carter: When you want to embrace all the good Minimal APIs has to offer without turning your back on controller-like organization.
  • FastEndpoints: For when you want your structure to stay lean or when Vertical Slice and Clean Architecture are your way to go.
  • Integration with MediatR: Especially great when your old WebAPI controllers served merely as conduits to MediatR already!

By the end of this article you’ll have the tools to turn any Minimal API project into a piece of art that can only be crafted by a master developer that doesn’t shy away from making tough decisions! Let’s debunk the myth that Minimal APIs are only for prototyping or tiny APIs and equip ourselves with strategies to structure your project like the experienced developer you are!

Carter

You’re ready to embrace Minimal APIs, but not yet ready to ditch the organization you got from controllers. The library Carter, created by one of the core maintainers of NancyFX, provides us with a thin layer of extension methods that allow us to separate our endpoint into Modules.

A module is nothing more than a file that implements the ICarterModule interface and allows us to register our endpoints in the AddRoutes method:


_12
public class HelloWorldModule : ICarterModule
_12
{
_12
public void AddRoutes(IEndpointRouteBuilder app)
_12
{
_12
app.MapGet("/", SayHello);
_12
}
_12
_12
public string SayHello()
_12
{
_12
return "Hello World";
_12
}
_12
}

It isn’t hard to see the similarities between the old WebAPI Controllers and Carter’s modules.

Starting off much like the old Controllers isn’t such a bad idea. Many APIs start off as simple CRUD routes for a couple of resources. So putting all your GET POST PUT and DELETE routes for your Student resource in a StudentModule makes a lot of sense.

Carter really shines in how much of the flexibility provided by barebones Minimal APIs it keeps around. Even when some of your endpoints grow beyond the point where keeping them all in the same file stops making sense, Carter has you covered.

For example, when your application gets multiple ways to update a Student record, it’s stupidly easy to move all these endpoints to a StudentUpdateModule and keep coding. You’re not bound to any conventions that force you to use partial controllers to keep everything under the same resource URL.

Getting started

Getting started with Carter is as straightforward as it gets. As it’s a thin layer around the already existing Minimal APIs you only have to add the package and register Carter on the WebApplication.

First, add the package like this:


_10
dotnet add package carter

And add everything Carter needs in your Program.cs like this:


_10
var builder = WebApplication.CreateBuilder(args);
_10
builder.Services.AddCarter();
_10
_10
var app = builder.Build();
_10
_10
app.MapCarter();
_10
app.Run();

Now you can create modules at will and structure your projects much in the way you were used to. The beauty, however, is that you still retain the flexibility Minimal APIs provide you.

If you’re not sure how your project will evolve but still want to start your project organization off strong I’d suggest starting with Carter. Its many times better than throwing everything in one file, let’s you evolve the structure alongside your features, and if or when you find a better suited solution it won’t take much effort to gradually convert your code base since your modules are still barebones Minimal API endpoints!

FastEndpoints

Please buckle up, because where Carter gave us a nice gradual introduction to the flexibility of Minimal APIs, FastEndpoints throws everything MVC straight out of the window and replaces it with the REPR Pattern. No more Models, Views or Controllers, your API now consists of a Request, Endpoint and Response for each route you need.

Comparison between Controllers and FastEndpoints, showing a single file with multiple endpoints vs multiple files with a single endpoint each.
Fig 1: Controllers vs FastEndpoints

As you can see, in FastEndpoints each endpoint is in its own file. How you exactly structure your folders and name your endpoints is entirely up to you!

The REPR pattern really shines when you combine it with Vertical Slices, since your endpoints can be located next or in each specific slice it belongs too. I personally compare it with the Handlers in MediatR, but instead of the call coming from the mediator, it comes from someone calling our API. Suddenly, your presentation layer is not a bunch of specialized files and code, but a simple inheritance of the Endpoint base class.

Let’s take a look how we implement an endpoint in FastEndpoints!


_22
// First we create our Request and Response
_22
public record CreateTodoRequest(string Name, string Description, DateTime DueDate);
_22
public record CreateTodoResponse(string Message);
_22
_22
//Next, we inherit the base type Endpoint and implement our logic
_22
public class CreateTodoEndpoint: Endpoint<CreateTodoRequest, CreateTodoResponse>
_22
{
_22
// FastEndpoints supports property injection
_22
public ITodoService TodoService { get; set; }
_22
_22
public override void Configure()
_22
{
_22
Post("/api/todo/create");
_22
AllowAnonymous();
_22
}
_22
_22
public override async Task HandleAsync(CreateTodoRequest req, CancellationToken ct)
_22
{
_22
// Do something with the CreateTodoRequest.
_22
await SendAsync(new CreateTodoResponse("Todo created"));
_22
}
_22
}

As you can see, FastEndpoints allows you to have fine-grained scoped API endpoints.

The case for FastEndpoints only becomes more alluring when we look beyond simple API endpoints. FastEndpoints is a whole framework on its own, supplyingus with a host of useful features that all integrate with each other in the same streamlined manner.

FastEndpoints also gives us:

  • Extensive model binding
  • Integrated validation
  • File handling
  • Response caching
  • Rate limiting

And even a whole in-process event bus, for all the features you get with FastEndpoints I suggest you check out the docs.

In conclusion, while opinionated, FastEndpoints gives us a very reasonable alternative to the old MVC Controllers with the REPR pattern. This pattern especially shines in the popular architectural patterns like Vertical Slices and Clean/Hexagonal Architecture!

Definitely use FastEndpoints if you’re looking for a solution that focuses on granular endpoints instead of endpoints that simply pass some request to the next service.

MediatR

We’ve discussed two very useful libraries thus far. But Minimal APIs also really shine when we need to create a custom solution that fits our specific project with our specific criteria.

MediatR is a very popular package, downloaded an average of 50.000 a day, that I’ve seen used in projects big and small. When combined with our dear old controllers, the controllers were reduced to large files with single-line methods to simply map our endpoints to our MediatR handlers.

A developer inexperienced with Minimal APIs might be afraid they’ll have to fill their Program.cs with every variation of:


_10
app.MapGet("/hello/{name}", (string name, IMediator mediator) =>
_10
{
_10
var request = new HelloRequest(name);
_10
return mediator.Send(request);
_10
});

Luckily, by combining a couple of features in Minimal APIs with extension methods, we can reduce this to a beautiful one-liner. Let me show you how:

Easy Minimal API to MediatR mapping

Extension methods should be your second nature as a .NET developer, and they will be our choice of weapon to reduce any boilerplate when we try to pass our API requests to our Handlers.

Instead of having to register a handler for each endpoint, I think we should aim for something like this:


_10
app.MapMediatrGet<HelloRequest>("/hello/{name}");

This looks a lot better, and isn’t that hard to achieve. We can simply extract the previous call to MapGet to an extension method like so:


_11
public static class MediatrApiExtensions
_11
{
_11
public static RouteHandlerBuilder MapMediatrGet<TRequest>(
_11
this WebApplication app,
_11
string pattern)
_11
where TRequest : IWebRequest=>
_11
app.MapGet(pattern, async ([AsParameters]TRequest request, IMediator mediator) =>
_11
{
_11
return await mediator.Send(request);
_11
});
_11
}

There are two things of particular note here.

First, instead of expecting the IRequest<TResponse>,I created an interface called IWebRequest. The IWebRequest implements the IRequest and sets the TResponse as IResult: public interface IWebRequest : IRequest<IResult> . This way, the handler HAS to return a valid Http response like 200 OK .

The second thing you should note is the use of the AsParametersAttribute. The AsParametersAttribute, introduced in .NET 7, allows us to use a class or struct to define all parameters required by the delegate. This includes all other parameter binding attributes like [FromBody], [FromForm], [FromRoute] etc. This works particularly well in this case since our little extension method doesn’t allow us to change the route handler for more granular parameters. Luckily, we can still get this behavior with the AsParametersAttribute:


_10
public class HelloRequest : IWebRequest<Ok<string>>
_10
{
_10
[FromRoute] //Binds with the name segment in the /hello/{name} route
_10
public required string Name { get; init; }
_10
_10
[FromQuery] //Binds with the optional query parameter ?Personalize=True|False
_10
public bool? Personalize { get; init; }
_10
}

And any of you who like to use Swagger will quickly notice that all the Parameter binding attributes still get documented as neatly as if you defined the delegate yourself:

Swagger documentation of the HelloRequest class with the Name and Personalize properties.
Fig 2: MediatR request working with Minimal APIs and Swagger.

Conclusion: Customize Minimal APIs to fit your project

Minimal APIs are extremely flexible. From controller-like modules to a tight integration with a popular library like MediatR, Minimal APIs allow you to create the solution you need.

As I discussed in Should you use WebApi or Minimal APIs?, Minimal APIs for most use cases on par with the features we get with the old Controllers. But instead of having to modify our project structure to fit some convention, we can create our own conventions to fit our project.

Demonstrated by the way we integrated MediatR directly into Minimal APIs, you can easily create an extension method to remove some of the boilerplate you normally have to write.

And with the use of the more advanced features like Reflection and Attributes you can come up with some wild ideas. If you’ve never worked with custom Attributes before, make sure to check out A 10 minute introduction to C# Attributes.

If you still need some inspiration on how to bend Minimal APIs to your will, here are some things I can think off:

  • Your own module registration that works with MapGroup
  • An extension method that registers all public methods on a class as endpoint
  • A way to automatically map between a DTO and the application model

What to read next:

I really hope you enjoyed this article. If you did, you might want to check out some of these articles I've written on similar topics.