Building Web APIs with ASP.NET Core can involve a lot of boilerplate code, especially when dealing with controllers, routing, and model binding. FastEndpoints is a lightweight library that simplifies this process, allowing you to define endpoints with minimal code with great performance.
In this blog post, we'll explore how to get started with FastEndpoints. I will show you to create API endpoints, handle requests, responses and add validation.
What is FastEndpoints?
FastEndpoints is an open-source library for .NET that simplifies the creation of Web APIs by eliminating the need for controllers and routing attributes. Built on top of ASP.NET Core Minimal APIs, it leverages all the performance benefits while providing a more straightforward programming model.
In the Minimal APIs, you need to define yourself how you want to structure your endpoints, how to group or not group them together in a single file. In FastEndpoints you define each endpoint in a separate class, which results in a Single Responsible and maintainable endpoints.
For me, this concept ideally fits in Vertical Slice Architecture.
FastEndpoints follows REPR Design Pattern (Request-Endpoint-Response) and offers the following advantages for Web API development:
- Simplicity: reduces complexity by allowing you to define endpoints as individual classes
- Performance: optimized for speed, providing better throughput and lower latency
- Maintainability: cleaner code structure makes it easier to maintain and scale your application
- Rapid Development: faster to set up and start building APIs, improving productivity
Getting Started with FastEndpoints
To get started with FastEndpoints you need to create a WebApi project and add the following Nuget package:
bashdotnet add package FastEndpoints
Here is how you can create an API Endpoint using FastEndpoints:
csharppublic record RegisterUserRequest(string Email, string Password, string Name); public record RegisterUserResponse(Guid Id, string Email, string Name); public class CreateUserEndpoint : Endpoint<RegisterUserRequest, RegisterUserResponse> { public override void Configure() { Post("/users/register"); AllowAnonymous(); } public override async Task HandleAsync(RegisterUserRequest request, CancellationToken token) { await SendAsync(new RegisterUserResponse(Guid.NewGuid(), "email", "name")); } }
You need to define request, response models and a class that inherits from the base Endpoint<TRequest, TResponse>
.
In the Configure
method you can specify:
- HTTP method type
- endpoint URL
- extra attributes: like authentication, authorization, allow anonymous, versioning, rate limiting, etc.
Endpoint Types in FastEndpoints
FastEndpoints offers 4 endpoint base types, that you can inherit from:
- Endpoint<TRequest> - use this type if there's only a request DTO. You can, however, send any object to the client that can be serialized as a response with this generic overload.
- Endpoint<TRequest,TResponse> - use this type if you have both request and response DTOs. The benefit of this generic overload is that you get strongly-typed access to properties of the DTO when doing integration testing and validations.
- EndpointWithoutRequest - use this type if there's no request nor response DTO. You can send any serializable object as a response here also.
- EndpointWithoutRequest<TResponse> - use this type if there's no request DTO but there is a response DTO.
It is also possible to define endpoints with EmptyRequest and EmptyResponse if needed:
csharppublic class Endpoint : Endpoint<EmptyRequest,EmptyResponse> { }
Sending Responses in FastEndpoints
FastEndpoints offers multiple ways to send responses, let's explore them.
- Directly assigning
Response
property of the baseEndpoint
class, for example:
csharppublic class CreateUserEndpoint : Endpoint<RegisterUserRequest, RegisterUserResponse> { public override void Configure() { Post("/users/register"); AllowAnonymous(); } public override Task HandleAsync(RegisterUserRequest request, CancellationToken token) { Response = new RegisterUserResponse(Guid.NewGuid(), "email", "name"); return Task.CompletedTask; } }
- Returning
Response
type directly:
csharppublic class CreateUserEndpoint : Endpoint<RegisterUserRequest, RegisterUserResponse> { public override void Configure() { Post("/users/register"); AllowAnonymous(); } public override Task HandleAsync(RegisterUserRequest request, CancellationToken token) { await SendAsync(new RegisterUserResponse(Guid.NewGuid(), "email", "name")); } }
Here you need to pass a response model directly to the base SendAsync
method.
- Using TypedResults in
HandleAsync
method:
csharppublic class CreateUserEndpoint : Endpoint<RegisterUserRequest, RegisterUserResponse> { public override void Configure() { Post("/users/register"); AllowAnonymous(); } public override async Task HandleAsync(RegisterUserRequest request, CancellationToken token) { if (...) { await SendResultAsync(TypedResults.BadRequest("Email already exists")); } var response = new RegisterUserResponse(Guid.NewGuid(), "email", "name"); await SendResultAsync(TypedResults.Ok(response)); } }
Here you need to pass a corresponding TypedResults
response model to the base SendResultAsync
method.
- Using TypedResults as Union-Type in
ExecuteAsync
method:
csharppublic class CreateUserEndpoint : Endpoint<RegisterUserRequest, Results<Ok<RegisterUserResponse>, BadRequest<string>>> { public override void Configure() { Post("/users/register"); AllowAnonymous(); } public override async Task<Results<Ok<RegisterUserResponse>, BadRequest<string>>> ExecuteAsync( RegisterUserRequest request, CancellationToken token) { if (...) { return TypedResults.BadRequest("Email already exists"); } var response = new RegisterUserResponse(Guid.NewGuid(), "email", "name"); return TypedResults.Ok(response); } }
In this case you need to use ExecuteAsync
method instead of HandleAsync
.
You need to specify all TypedResults
your method will be returning.
If you try to return a wrong type - a compilation error will be raised.
Using FastEndpoints in a Real Application
Today I'll show you how to use FastEndpoints for a Shipping Application that is responsible for creating and updating shipments for ordered products.
This application has 3 Web API endpoints:
- Create Shipment
- Update Shipment Status
- Get Shipment by Number
Let's explore the POST "Create Shipment" endpoint implementation:
csharppublic sealed record CreateShipmentRequest( string OrderId, Address Address, string Carrier, string ReceiverEmail, List<ShipmentItem> Items); public class CreateShipmentEndpoint(IShipmentRepository repository, ILogger<CreateShipmentEndpoint> logger) : Endpoint<CreateShipmentRequest, Results<Ok<ShipmentResponse>, Conflict<string>>> { public override void Configure() { Post("/api/shipments"); AllowAnonymous(); } public override async Task<Results<Ok<ShipmentResponse>, Conflict<string>>> ExecuteAsync( CreateShipmentRequest request, CancellationToken cancellationToken) { var shipmentAlreadyExists = await repository.ExistsAsync(request.OrderId, cancellationToken); if (shipmentAlreadyExists) { logger.LogInformation("Shipment for order '{OrderId}' is already created", request.OrderId); return TypedResults.Conflict($"Shipment for order '{request.OrderId}' is already created"); } var shipmentNumber = new Faker().Commerce.Ean8(); var shipment = request.MapToShipment(shipmentNumber); await repository.AddAsync(shipment, cancellationToken); logger.LogInformation("Created shipment: {@Shipment}", shipment); var response = shipment.MapToResponse(); return TypedResults.Ok(response); } }
Here FastEndpoints automatically binds the request's JSON body to the CreateShipmentRequest
model:
json{ "number": "10000001", "orderId": "11100001", "carrier": "Modern Delivery", "receiverEmail": "TODO: SET EMAIL HERE", "address": { "street": "123 Main St", "city": "Springfield", "zip": "12345" }, "items": [ { "product": "Acer Nitro 5", "quantity": 7 } ] }
For returning response I use TypedResults.Conflict
and TypedResults.Ok
that I specified in my endpoint:
csharppublic class CreateShipmentEndpoint(IShipmentRepository repository, ILogger<CreateShipmentEndpoint> logger) : Endpoint<CreateShipmentRequest, Results<Ok<ShipmentResponse>, Conflict<string>>> { public override async Task<Results<Ok<ShipmentResponse>, Conflict<string>>> ExecuteAsync( CreateShipmentRequest request, CancellationToken cancellationToken) { } }
This ensures that you return a correct type from the endpoint; otherwise a compilation error will be raised.
For validation, FastEndpoints has built-in support for FluentValidation.
You need to create the validator that inherits from the base Validator
class:
csharppublic class CreateShipmentRequestValidator : Validator<CreateShipmentRequest> { public CreateShipmentRequestValidator() { RuleFor(shipment => shipment.OrderId).NotEmpty(); RuleFor(shipment => shipment.Carrier).NotEmpty(); RuleFor(shipment => shipment.ReceiverEmail).NotEmpty(); RuleFor(shipment => shipment.Items).NotEmpty(); RuleFor(shipment => shipment.Address) .Cascade(CascadeMode.Stop) .NotNull() .WithMessage("Address must not be null") .SetValidator(new AddressValidator()); } }
When the "Create" endpoint is called, FastEndpoint will automatically perform model validation and return BadRequest
in the following format:
json{ "StatusCode": 400, "Message": "One or more errors occured!", "Errors": { "ReceiverEmail": ["Email is required!", "Email is invalid!"], "Carrier": ["Carrier is required!"] } }
Let's explore the GET "Get Shipment by Number" endpoint implementation:
csharppublic record GetShipmentByNumberRequest(string ShipmentNumber); public class GetShipmentByNumberEndpoint(IShipmentRepository repository, ILogger<GetShipmentByNumberEndpoint> logger) : Endpoint<GetShipmentByNumberRequest, ShipmentResponse> { public override void Configure() { Get("/api/shipments/{ShipmentNumber}"); AllowAnonymous(); } public override async Task HandleAsync(GetShipmentByNumberRequest request, CancellationToken cancellationToken) { var shipment = await repository.GetByNumberWithItemsAsync(request.ShipmentNumber, cancellationToken); if (shipment is null) { logger.LogDebug("Shipment with number {ShipmentNumber} not found", request.ShipmentNumber); await SendNotFoundAsync(cancellationToken); return; } var response = shipment.MapToResponse(); await SendAsync(response, cancellation: cancellationToken); } }
Here FastEndpoints automatically binds the route parameter to the GetShipmentByNumberRequest
model:
HTTPGET /api/shipments/74119066
Now let's explore how to map this POST "Update Shipment Status" request:
HTTPPOST /api/shipments/update-status/74119066 Content-Type: application/json { "status": "WaitingCustomer" }
ShipmentStatus is a part of request's JSON body that maps to UpdateShipmentStatusRequest
:
csharppublic sealed record UpdateShipmentStatusRequest(ShipmentStatus Status);
Route parameter "ShipmentNumber" you can get inside an ExecuteAsync
or HandleAsync
method:
csharppublic override void Configure() { Post("/api/shipments/update-status/{ShipmentNumber}"); AllowAnonymous(); } public override async Task<Results<NoContent, NotFound<string>>> ExecuteAsync( UpdateShipmentStatusRequest request, CancellationToken cancellationToken) { var shipmentNumber = Route<string>("ShipmentNumber")!; }
Summary
FastEndpoints is a great library that simplifies Web API implementation, allowing you to define endpoints with minimal code with great performance.
FastEndpoints offers a ready code structure for your endpoints with a great design, so you don't need to implement your own with Minimal APIs.
For more information on FastEndpoints and various supported features, I recommend reading their official documentation.
Hope you find this blog post useful. Happy coding!