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.
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:
_12public 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:
_10dotnet add package carter
And add everything Carter needs in your Program.cs like this:
_10var builder = WebApplication.CreateBuilder(args);_10builder.Services.AddCarter();_10_10var app = builder.Build();_10_10app.MapCarter();_10app.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.
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_22public record CreateTodoRequest(string Name, string Description, DateTime DueDate);_22public record CreateTodoResponse(string Message);_22_22//Next, we inherit the base type Endpoint and implement our logic_22public 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:
_10app.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:
_10app.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:
_11public 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
:
_10public 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:
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.
- Read Should you use WebApi or Minimal APIs?— 7 minutes readShould you use WebApi or Minimal APIs?
- Read Gracefully handling exceptions in ASP.NET Core Minimal APIs— 8 minutes readGracefully handling exceptions in ASP.NET Core Minimal APIs
- Read Generate OpenAPI documentation for dynamic query parameters in .NET 7— 7 minutes readGenerate OpenAPI documentation for dynamic query parameters in .NET 7