Taking FastEndpoints for a Test Drive

Building Efficient APIs with the REPR Pattern

Discover how FastEndpoints revolutionizes API development in .NET by embracing the REPR pattern. This comprehensive guide walks through building a complete Person Management API with minimal boilerplate and maximum performance, showcasing modern development practices with .NET 9.

View the Complete Source Code:

The full FastEndpoints Demo project is available on GitHub for hands-on exploration and learning. GitHub Repository

What is FastEndpoints?

FastEndpoints is an ASP.NET library designed to minimize boilerplate while maximizing performance and productivity. By embracing the REPR (Request-Endpoint-Response) design pattern, it offers an alternative to traditional MVC setups and Minimal APIs, focusing on clarity and efficiency.

This guide introduces you to the FastEndpoints framework through a practical Person Management demo application. By walking through this project, you'll discover how FastEndpoints can help you build clean, maintainable APIs with minimal boilerplate code.

2025 Update: FastEndpoints Demo Modernization
  • Upgraded to .NET 9: The demo now targets .NET 9 for improved performance and long-term support.
  • Latest NuGet Packages: Uses FastEndpoints v6.0+ , FastEndpoints.Swagger , and FluentValidation for validation and OpenAPI docs.
  • Enhanced CI/CD: Automated GitHub Actions for build, test, and Azure deployment.
  • Static Documentation: Project now includes a static docs site for API and architecture reference.
  • Best Practices: The codebase and this article follow the latest FastEndpoints best practices for structure, mapping, and error handling.

Introduction

Minimal APIs

Minimal APIs in .NET have marked a significant shift in the way developers approach API development within the ASP.NET Core framework. These APIs offer a streamlined, more efficient method for building HTTP APIs, requiring minimal code and configuration while still allowing for the fully functioning REST endpoints necessary for modern web applications.

Introduced in .NET 6, Minimal APIs were designed to reduce the overhead and boilerplate code traditionally associated with setting up new API endpoints in ASP.NET Core. By simplifying the API development process, they enable developers to focus more on the business logic rather than the scaffolding required to set up controllers and actions in a typical MVC application.

One of the core features of Minimal APIs is the ability to define endpoints with concise lambda expressions directly within the Program.cs file, doing away with the need for a separate Startup.cs. This approach not only simplifies the project structure but also improves the readability and maintainability of the codebase.

Model binding and dependency injection are also integral parts of Minimal APIs, allowing developers to work with complex data types and services seamlessly. Model binding in Minimal APIs is designed to be straightforward, primarily relying on the deserialization of JSON request bodies to POCO (Plain Old CLR Objects) types. Dependency injection is built on ASP.NET Core's robust DI system, enabling the injection of services directly into endpoint definitions, thus promoting cleaner and more modular code.

Despite their simplicity, Minimal APIs are not just for basic CRUD operations; they are fully capable of supporting complex applications. Features like model validation, middleware integration, and custom response types can be easily implemented, making Minimal APIs suitable for production-grade applications.

The introduction and rapid adoption of Minimal APIs reflect a broader industry trend towards more lightweight, performant, and modular web frameworks. They offer an attractive alternative for developers looking to build APIs in a .NET environment, providing the necessary tools and flexibility for both small-scale projects and complex applications. The future of API development in ASP.NET Core looks promising with Minimal APIs, as they continue to evolve and integrate more features with each new .NET release.

The REPR Pattern

While looking at FastEndpoints, I read up on the REPR pattern. REPR, standing for Request, Endpoint, Response, encapsulates a streamlined approach for structuring APIs that's not only intuitive but also aligns with the minimalist ethos of .NET's minimal APIs.

The REPR pattern simplifies the process of creating and managing endpoints by breaking them down into three core components. The Request part defines the incoming data structure, capturing client inputs in a structured format. The Endpoint acts as the core where the business logic resides, processing requests and preparing responses. Finally, the Response component deals with sending back the processed data to the client.

This pattern was notably implemented and extended in my FastEndpoints project. By adopting the REPR pattern, I was able to create a clear separation of concerns within the API, making endpoints more understandable and easier to maintain. This approach also facilitated a cleaner integration with services like PersonService, allowing us to focus on implementing business logic without the overhead of managing the API plumbing.

Frameworks like Reaper and Reprise have further embraced and extended the REPR pattern, offering developers additional tools and methodologies for building efficient and maintainable APIs within the ASP.NET Core ecosystem. Reaper, for instance, is designed with performance and simplicity in mind, providing a middle ground between the barebones minimal APIs and the more feature-rich FastEndpoints. On the other hand, Reprise introduces the REPR pattern into ASP.NET Core Minimal APIs, aiming to provide a thin layer of abstractions that encourages the creation of modular and convention-based implementations.

The adoption of the REPR pattern, coupled with FastEndpoints, represents a significant leap towards more efficient, maintainable, and scalable API development in .NET. It showcases the power of minimalism in API design, proving that simplicity does not necessarily come at the expense of functionality or performance.

The Problem Set

For our demo, I am addressing a common but crucial problem set in the realm of web APIs: managing a simple database of entities—in this case, Person entities—via a web interface. This involves creating a CRUD (Create, Read, Update, Delete) API, a foundational component in many web applications, allowing users to interact with a database in a structured way.

The core of our problem set revolves around managing Person entities, each representing an individual with properties such as name, age, and email. The API will need to provide functionality to:

  1. Create a new Person: Add a new entry to our database with the provided details.
  2. Read/Retrieve Persons: Fetch one or more Person entries from the database, either all at once or based on specific criteria like ID.
  3. Update an existing Person: Modify the details of an existing Person entry.
  4. Delete a Person: Remove an entry from the database.

This CRUD functionality forms the backbone of many web services, from social networks to e-commerce platforms, making it an essential skill set for developers.

The Person Class

The Person class is a simple data model representing individuals in the system. Here's what I created for our Person entity:

public class Person
{
  public Guid Id { get; set; } // A unique identifier for each person
  public string FirstName { get; set; } // The person's first name
  public string LastName { get; set; } // The person's last name
  public int Age { get; set; } // The person's age
  public string Email { get; set; } // The person's email address
}

Each Person has a unique Id, along with basic information like FirstName, LastName, Age, and Email. This class structure is straightforward yet flexible enough to be expanded for more complex applications.

For the full CRUD API, this Person class will be central to the operations created with Fast Endpoints. When creating a new Person, the API will accept details like name, age, and email to construct a Person object to be stored. For reading, the API will fetch and return Person objects, either individually by Id or as a collection. Updating will involve modifying the properties of an existing Person, and deleting will remove a Person from our storage based on their Id.

This demo demonstrates not just the mechanics of building a CRUD API with FastEndpoints but also the underlying principles of web API development, such as RESTful design, HTTP methods, status codes, and more, providing a solid foundation for tackling more complex problem sets in the future. section#setting-up.mb-5

Setting Up

Starting with a basic setup, I added FastEndpoints to a new .NET project and configured the necessary services in `Program.cs`. The ease of getting started was impressive, immediately making the development process smoother.

using FastEndpointApi.services.person;
using FastEndpoints;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFastEndpoints();
builder.Services.AddEndpointsApiExplorer();
builder.Services.SwaggerDocument(o =>
{
  o.ShortSchemaNames = true;
  o.DocumentSettings = s =>
  {
    s.DocumentName = "v1";
  };
});
builder.Services.AddSingleton<IPersonService, PersonService>();

var app = builder.Build();

app.UseFastEndpoints(c =>
{
  c.Endpoints.ShortNames = true;
});
app.UseSwaggerGen();
app.UseSwaggerUi();

app.Run();

Creating the Person Service

The `PersonService` class encapsulated the business logic for managing Person entities. By abstracting the data layer from the API endpoints, we achieved a clean separation of concerns, promoting code reusability and maintainability. Here is the interface for the `PersonService` class.

public interface IPersonService
{
  PersonEntity CreatePerson(PersonEntity person);
  void DeletePerson(Guid id);
  PersonEntity ReadPerson(Guid id);
  List<PersonEntity> ReadPersons();
  PersonEntity UpdatePerson(Guid id, PersonEntity updatedPerson);
}
public class PersonService : IPersonService
{
  private readonly List<PersonEntity> _people = new();  // In-memory storage for Person entities
  public PersonEntity CreatePerson(PersonEntity person)
  {
    person.Id = Guid.NewGuid();
    _people.Add(person);
    return person;
  }

Integrating the `PersonService` through dependency injection demonstrated FastEndpoints' seamless integration with ASP.NET's core features. This allowed us to focus on business logic without worrying about the plumbing. Here is the code to register the service in the `Program.cs` file.

builder.Services.AddSingleton<IPersonService, PersonService>();

Separate concerns with Domain Mapper

The domain mapper functionality in FastEndpoints simplifies the manual mapping process between request DTOs (Data Transfer Objects) and domain entities. By introducing a dedicated CreatePersonMapper class, I can efficiently transform data from entities to response DTOs and vice versa. This separation of concerns ensures cleaner and more maintainable code, making FastEndpoints an appealing choice for those who prefer manual mapping over auto-mapping libraries.

FastEndpoints Documentation - Mapping Logic in a Separate Class

public class CreatePersonMapper : Mapper<CreatePersonRequest, PersonResponse, PersonEntity>
{
  public override PersonEntity ToEntity(CreatePersonRequest r) => new()
  {
    FirstName = r.FirstName,
    LastName = r.LastName,
    Age = r.Age,
    Email = r.Email
  };
  public override PersonResponse FromEntity(PersonEntity e) => new()
  {
    FullName = $"{e.FirstName} {e.LastName}",
    IsOver18 = e.Age >= 18,
    PersonId = e.Id.ToString()
  };
}

My First Fast Endpoint

Time to put it all together. I created my first Fast Endpoint for the Create Person method. The `CreatePersonEndpoint`, was succinct yet expressive, showcasing FastEndpoints' capability to handle complex scenarios with minimal fuss.

public class CreatePersonEndpoint(IPersonService _personService) : Endpoint<CreatePersonRequest, PersonResponse, CreatePersonMapper>
{
  public override void Configure()
  {
    Post("/api/person/create");
    AllowAnonymous();
  }
  public override Task<PersonResponse> HandleAsync(CreatePersonRequest request)
  {
    PersonEntity entity = Map.ToEntity(req);
    entity = _personService.CreatePerson(entity);
    Response = Map.FromEntity(entity);
    return SendAsync(Response, cancellation: ct);
  }
}

Next Steps and Conclusion

The next steps was to finish out the CRUD operations for the Person entity. I created the Read, Update, and Delete endpoints, each following the same pattern as the Create endpoint. The result was a clean, efficient API that showcased the power of FastEndpoints and the REPR pattern. section#key-features.mb-5

Key Features of the Demo Project
  • CRUD Operations: Complete set of endpoints for creating, reading, updating, and deleting Person entities
  • In-Memory Data Store: A simple in-memory collection to demonstrate data persistence without database complexity
  • Person Service Layer: A clean abstraction of business logic through service interfaces and implementations
  • Smart Data Mapping: Built-in mapping capabilities for transforming between request models, domain entities, and response DTOs
  • Dependency Injection: Practical examples of how DI is used in FastEndpoints to provide services where needed
  • HATEOAS Links: Implementation of hypermedia links in API responses for improved client navigation
  • Interactive Documentation: Integration with Swagger/OpenAPI for easy API exploration and testing

Running the Project

Ready to dive in? Here's how to get the project running on your machine:

Prerequisites
  • .NET 9.0 SDK or later installed on your machine
  • An IDE of your choice: Visual Studio, VS Code, or Rider
Steps to Run the Project
  1. Clone the repository to your local machine:
    git clone https://github.com/MarkHazleton/FastEndpointDemo.git
    cd FastEndpointDemo
  2. Build and run the project:
    cd FastEndpointApi
    dotnet build
    dotnet run
  3. Open your browser and navigate to https://localhost:7000/swagger to explore the API using the interactive Swagger UI.

I added support for Swagger, enabling interactive documentation for the API.

I created a GitHub action to build and deploy the API to and Azure Web App on each push to the main branch. Here is the link to FastEndpoints Demo on Azure: fastendpointsdemo.azurewebsites.net

FastEndpoints Demo Swagger UI showing Person API endpoints
FastEndpoints Demo Home Page with interactive Swagger documentation

Taking FastEndpoints for a test drive was a fun project to quickly get an understanding of the Fast Endpoint library. Its adherence to the REPR pattern, combined with ASP.NET's robust ecosystem, presents a compelling approach to API development. Whether you're building a complex system or a simple microservice, FastEndpoints offers the tools and flexibility needed to deliver high-quality APIs efficiently.

FastEndpoints shines in its simplicity and effectiveness. The project we embarked on served as a practical demonstration of how quickly one can get up and running with high-performing APIs in .NET. For developers looking to streamline their API development process, taking FastEndpoints for a test drive might just be the answer. section#project-structure.mb-5

Project Structure

The demo project follows a clean, organized structure that aligns with the REPR pattern. This structure promotes separation of concerns, with each endpoint having its own dedicated folder containing all related files. This organization makes it easy to navigate and maintain the codebase as it grows.

File Organization
FastEndpointApi/
├── Program.cs                       # Application entry point and configuration
├── endpoints/                       # All API endpoints
│   ├── LinkResource.cs              # HATEOAS link representation
│   ├── PersonResponse.cs            # Shared response DTO
│   ├── create/                      # Create person endpoint
│   │   ├── CreatePersonEndpoint.cs
│   │   ├── CreatePersonMapper.cs
│   │   └── CreatePersonRequest.cs
│   ├── read/                        # Read person endpoint(s)
│   │   ├── ReadPersonEndpoint.cs
│   │   ├── ReadPersonMapper.cs
│   │   ├── ReadPersonRequest.cs
│   │   └── ReadPersonsEndpoint.cs
│   ├── update/                      # Update person endpoint
│   │   ├── UpdatePersonEndpoint.cs
│   │   └── UpdatePersonRequest.cs
│   └── delete/                      # Delete person endpoint
│       ├── DeletePersonEndpoint.cs
│       └── DeletePersonRequest.cs
└── services/                        # Business logic layer
  ├── IPersonService.cs            # Service interface
  ├── PersonEntity.cs              # Domain model
  └── PersonService.cs             # Service implementation

API Endpoints

Let's explore the available endpoints in our Person Management API. When you run the application and navigate to the Swagger UI, you'll be able to test each of these endpoints interactively.

Method Endpoint Description
POST /person/create Create a new person
GET /person/{id} Get a person by ID
GET /persons Get all persons
PUT /person/{id} Update a person
DELETE /person/{id} Delete a person

Advanced Usage

HATEOAS Implementation

HATEOAS (Hypermedia as the Engine of Application State) improves API discoverability by including relevant links in responses. This demo shows how easy it is to implement with FastEndpoints:

// From ReadPersonEndpoint.cs
Response = new PersonResponse
{
  FullName = $"{person.FirstName} {person.LastName}",
  IsOver18 = person.Age > 18,
  PersonId = person.Id.ToString(),
  Links =
    [
      new LinkResource { Rel = "self", Href = $"{baseUrl}/{person.Id}", Method = "GET" },
      new LinkResource { Rel = "delete", Href = $"{baseUrl}/{person.Id}", Method = "DELETE" }
    ]
};

By including these links, clients can dynamically discover available actions rather than having to know them in advance. This makes your API more self-documenting and flexible.

Error Handling

Well-designed APIs need robust error handling. This demo project implements a global exception handler that provides consistent, client-friendly error responses:

// From Program.cs
app.UseExceptionHandler(errorApp =>
{
  errorApp.Run(async context =>
  {
    context.Response.StatusCode = 500;
    context.Response.ContentType = "application/json";
    var error = context.Features.Get<IExceptionHandlerFeature>()?.Error;
    await context.Response.WriteAsJsonAsync(new { error = error?.Message ?? "An error occurred." });
  });
});

This approach ensures that even unexpected exceptions are caught and returned in a consistent JSON format, improving the API's reliability and developer experience. section#why-fastendpoints.mb-5

Why Use FastEndpoints?

Advantages over MVC Controllers and Minimal APIs
  • Clean Architecture: Promotes the REPR pattern for organized, maintainable code
  • Performance: Comparable to Minimal APIs and faster than MVC Controllers
  • Reduced Boilerplate: Simplified endpoint creation with minimal setup code
  • Auto-Discovery: Automatic registration of endpoints during application startup
  • Built-in Validation: Seamless integration with FluentValidation
  • Security Support: Easy implementation of authentication and authorization
  • Swagger Integration: Simple API documentation with the FastEndPoints.Swagger package

Glossary of Terms

New to RESTful APIs or .NET development? Here's a handy glossary of terms used in this project:

Request-Endpoint-Response Pattern - An architectural approach where each endpoint has its own request model, handler, and response model, promoting separation of concerns.

Data Transfer Object - A simple object used to transfer data between processes or layers of an application.

Representational State Transfer - An architectural style for designing networked applications, emphasizing stateless operations and standard HTTP methods.

Hypermedia as the Engine of Application State - A REST constraint that provides hyperlinks in API responses, allowing clients to dynamically navigate the API.

A specification and set of tools for documenting and exploring RESTful APIs.

A technique where one object supplies the dependencies of another object, helping to achieve loose coupling between classes.

References

I could not have completed this project without the help of many teachers and mentors both in person and online. Here are some of the resources I used to complete this project:

GUIDs vs Auto-Incrementing Integers

Choosing between GUIDs and auto-incrementing integers for primary keys is a common decision in database design. While both have their advantages and drawbacks, the choice often depends on the specific requirements of the application. For scenarios where global uniqueness is essential, such as distributed systems or disconnected data creation, GUIDs offer a robust solution. On the other hand, auto-incrementing integers provide better performance and simplicity, making them suitable for large databases with predictable access patterns. When faced with this decision, developers should carefully evaluate the trade-offs and select the option that best aligns with their project's needs.

For the Person entity, I opted to use GUIDs as the primary key. This choice was driven by the need of keeping the active items in memory and not wanting to relay on a system to create auto-incremented integers.

Using GUID
Pros
  • They are globally unique without the need for a centralized authority to manage key allocation.
  • They are useful in scenarios where data needs to be created offline and then synchronized with a central database, as they eliminate the risk of key collisions.
Cons
  • Larger size may impact database performance and increase index size.
  • Alphanumeric nature can complicate debugging and data analysis.
Using Auto-Incrementing Integers
Pros
  • Better performance and smaller index size, beneficial for large databases.
  • Simplicity and predictability can enhance user experience.
Cons
  • Potential security risks, as sequential patterns can be exploited.
  • Can complicate data integration in distributed database environments.