DEV Community

Cover image for Getting Started with FastEndpoints for Building Web APIs in .NET
Anton Martyniuk
Anton Martyniuk

Posted on • Originally published at antondevtips.com on

Getting Started with FastEndpoints for Building Web APIs in .NET

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.

On my website: antondevtips.com I share .NET and Architecture best practices.
Subscribe to become a better developer.
Download the source code for this blog post for free.

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:

dotnet add package FastEndpoints
Enter fullscreen mode Exit fullscreen mode

Here is how you can create an API Endpoint using FastEndpoints:

public 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"));
    }
}
Enter fullscreen mode Exit fullscreen mode

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 - 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 - 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 - 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:

public class Endpoint : Endpoint<EmptyRequest,EmptyResponse> { }
Enter fullscreen mode Exit fullscreen mode

Sending Responses in FastEndpoints

FastEndpoints offers multiple ways to send responses, let's explore them.

  1. Directly assigning Response property of the base Endpoint class, for example:
public 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;
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Returning Response type directly:
public 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"));
    }
}
Enter fullscreen mode Exit fullscreen mode

Here you need to pass a response model directly to the base SendAsync method.

  1. Using TypedResults in HandleAsync method:
public 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));
    }
}
Enter fullscreen mode Exit fullscreen mode

Here you need to pass a corresponding TypedResults response model to the base SendResultAsync method.

  1. Using TypedResults as Union-Type in ExecuteAsync method:
public 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

public 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Here FastEndpoints automatically binds the request's JSON body to the CreateShipmentRequest model:

{
    "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
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

For returning response I use TypedResults.Conflict and TypedResults.Ok that I specified in my endpoint:

public 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)
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

public 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());
    }
}
Enter fullscreen mode Exit fullscreen mode

When the "Create" endpoint is called, FastEndpoint will automatically perform model validation and return BadRequest in the following format:

{
    "StatusCode": 400,
    "Message": "One or more errors occured!",
    "Errors": {
        "ReceiverEmail": ["Email is required!", "Email is invalid!"],
        "Carrier": ["Carrier is required!"]
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's explore the GET "Get Shipment by Number" endpoint implementation:

public 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Here FastEndpoints automatically binds the route parameter to the GetShipmentByNumberRequest model:

GET /api/shipments/74119066
Enter fullscreen mode Exit fullscreen mode

Now let's explore how to map this POST "Update Shipment Status" request:

POST /api/shipments/update-status/74119066
Content-Type: application/json
{
    "status": "WaitingCustomer"
}
Enter fullscreen mode Exit fullscreen mode

ShipmentStatus is a part of request's JSON body that maps to UpdateShipmentStatusRequest:

public sealed record UpdateShipmentStatusRequest(ShipmentStatus Status);
Enter fullscreen mode Exit fullscreen mode

Route parameter "ShipmentNumber" you can get inside an ExecuteAsync or HandleAsync method:

public 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")!;
}
Enter fullscreen mode Exit fullscreen mode

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.

On my website: antondevtips.com I share .NET and Architecture best practices.
Subscribe to become a better developer.
Download the source code for this blog post for free.

Top comments (0)