This content originally appeared on Telerik Blogs and was authored by Assis Zang
Fast endpoints allow you to create web APIs with automatic validation, high performance and less code. In this post, we will understand how to use fast endpoints as an alternative to classic approaches and combine the best of both worlds when creating new web APIs in ASP.NET Core.
Creating web APIs is part of most developers’ routines these days. In this sense, it is worth reflecting on how this process can be accelerated to increase productivity and improve performance without sacrificing organization and maintainability. It was with this in mind that the concept of “fast endpoints” emerged.
In this post, we will understand what fast endpoints are, how to implement them in practice and see what advantages they can offer compared to the traditional use of Controllers in ASP.NET Core and the modern minimal APIs.
Understanding Fast Endpoints
The concept of fast endpoints is related to approaches that seek to simplify and accelerate the development of APIs, with a focus on performance, low coupling between components and better code organization.
Fast endpoints emerged as an alternative to the traditional approaches for handling requests in web APIs, such as ASP.NET MVC Controllers and Minimal APIs, seeking to combine performance, organization and simplicity in a single model.
Fast Endpoints in ASP.NET Core
In ASP.NET Core, we have access to fast endpoints through the library of the same name, available at https://fast-endpoints.com. It operates under the MIT license, which means it is open source and can be used in private and commercial projects.
In addition, it follows the Request-Endpoint-Response (REPR) design pattern, which advocates the clear organization of an API’s components, separation of responsibilities, and ease of maintenance and reading.
Before the arrival of fast endpoints, the main ways to create web APIs were through traditional Controllers and modern Minimal APIs. Although both are excellent approaches, they have some gaps:
Controllers: Although they are robust, organized and ready for practically any type of configuration, they can be excessively verbose for simple scenarios. The way of implementing a Controller using classes and attributes often generates unnecessary overhead for lean APIs, and in some cases, it can make it difficult to have a more resource-oriented approach and a clear separation of responsibilities.
Minimal APIs: On the other hand, Minimal APIs offer a much simpler way to create endpoints, ideal for small and fast services. However, as they grow, they can compromise the organization and maintainability of the code. The lack of clear conventions and the mixture of business logic with route definition can make the code difficult to scale and test.
It is precisely in these limitations that fast endpoints stand out as a middle ground between these two approaches. They combine the best of both worlds:
- Performance and simplicity of Minimal APIs, with direct routing and a lean structure (the documentation indicates that the performance is on par with Minimal APIs and is noticeably better than MVC controllers)
- Organization, testability and separation of responsibilities of Controllers, allowing the use of validation, filters, dependency injection and clean encapsulation through specialized classes per endpoint
The great advantage of fast endpoints is that they allow you to write clean, modular and performant code without sacrificing clarity and scalability. This makes them an excellent choice for modern applications that require both agility and a solid foundation for long-term maintenance.
Fast Endpoints in Practice
Now that we have seen what fast endpoints are and what their advantages are compared to traditional approaches, let’s create an ASP.NET Core application to implement them in practice.
As a scenario, let’s imagine that you need to create an activity logging system for the development team. This system must be simple, performant and at the same time support validations without being complex. In this case, we can use fast endpoints to create an API that fits these requirements.
To keep the focus on the main topic, some implementation details will not be shown in this post, but you can check the complete code in this GitHub repository: Work Log – source code.
To create the base application, you can use the command below. This post uses version 9 of .NET, which is currently the latest stable version.
dotnet new web -o WorkLog
Then, to install the NuGet packages used in this example, you can use the following commands:
dotnet add package FastEndpoints
dotnet add package FluentValidation
dotnet add package FastEndpoints.Validation
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Tools
The next step is to create the model class, so create a new folder called “Models” and, inside it, add the class below:
namespace WorkLog.Models;
public class TimeEntry
{
public Guid Id { get; set; }
public string UserName { get; set; } = default!;
public string? ProjectName { get; set; }
public DateTime Date { get; set; }
public decimal Hours { get; set; }
public string? Description { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
The DbContext
class will be used to set the database configurations, so create a new folder called “Data” and, inside it, add the following class:
using Microsoft.EntityFrameworkCore;
using WorkLog.Models;
namespace WorkLog.Data;
public class WorkLogDbContext : DbContext
{
public DbSet<TimeEntry> TimeEntries { get; set; }
public WorkLogDbContext(DbContextOptions<WorkLogDbContext> options) : base(options) { }
}
Creating the Endpoints
The endpoint classes will contain the fast endpoints functionalities. For this, they will implement the Endpoint
class from the FastEndpoints NuGet package. The first endpoint will be used to create new records in the database and then retrieve it, for this we will create some auxiliary classes for request and response. So, create a new folder called “Request” and inside it add the class below:
namespace WorkLog.Requests;
public class CreateTimeEntryRequest
{
public string? UserName { get; set; }
public string? ProjectName { get; set; }
public DateTime Date { get; set; }
public decimal Hours { get; set; }
public string? Description { get; set; }
}
Then, create a new folder called “Response” and add the following classes to it:
- CreateTimeEntryResponse
namespace WorkLog.Responses;
public class CreateTimeEntryResponse
{
public Guid Id { get; set; }
public string? UserName { get; set; }
public string? ProjectName { get; set; }
public DateTime Date { get; set; }
public decimal Hours { get; set; }
public string? Description { get; set; }
public DateTime CreatedAt { get; set; }
}
- GetTimeEntryResponse
namespace WorkLog.Responses;
public class GetTimeEntryResponse
{
public Guid Id { get; set; }
public string? UserName { get; set; }
public string? ProjectName { get; set; }
public DateTime Date { get; set; }
public decimal Hours { get; set; }
public string Description { get; set; } = default!;
public DateTime CreatedAt { get; set; }
}
Finally, let’s create the endpoint class. Create a folder called “Endpoints” and, inside it, add the following class:
using FastEndpoints;
using WorkLog.Data;
using WorkLog.Models;
using WorkLog.Requests;
using WorkLog.Responses;
namespace WorkLog.Endpoints;
public class CreateTimeEntryEndpoint : Endpoint<CreateTimeEntryRequest, CreateTimeEntryResponse>
{
private readonly WorkLogDbContext _dbContext;
public CreateTimeEntryEndpoint(WorkLogDbContext dbContext)
{
_dbContext = dbContext;
}
public override void Configure()
{
Post("/time-entries");
AllowAnonymous();
Summary(s =>
{
s.Summary = "Create a new time entry";
s.Description = "Registers hours worked by a professional on a specific project.";
});
}
public override async Task HandleAsync(CreateTimeEntryRequest req, CancellationToken ct)
{
var timeEntry = new TimeEntry
{
Id = Guid.NewGuid(),
UserName = req?.UserName,
ProjectName = req?.ProjectName,
Date = req.Date,
Hours = req.Hours,
Description = req.Description,
CreatedAt = DateTime.UtcNow
};
_dbContext.TimeEntries.Add(timeEntry);
await _dbContext.SaveChangesAsync(ct);
await SendAsync(new CreateTimeEntryResponse
{
Id = timeEntry.Id,
UserName = timeEntry.UserName,
ProjectName = timeEntry.ProjectName,
Date = timeEntry.Date,
Hours = timeEntry.Hours,
Description = timeEntry.Description,
CreatedAt = timeEntry.CreatedAt
});
}
}
Note that the CreateTimeEntryEndpoint
class represents an HTTP endpoint responsible for creating a new record of hours worked by a professional on a project.
The class inherits from Endpoint<CreateTimeEntryRequest, CreateTimeEntryResponse>
, defining the input and output contract of this endpoint.
The Configure()
method defines the main settings of the endpoint. We specify that it responds to HTTP POST requests on the /time-entries
route and allows anonymous access (without authentication). We also use the Summary()
method to document the purpose of the endpoint, making it easier to generate automatic documentation and for other developers to read.
The most important part is the HandleAsync
method. When a request is received, we create a new instance of TimeEntry
, filling its fields with the data from the request. This new record is then added to the context and persisted in the database with SaveChangesAsync
.
Finally, we send a response to the client with the newly created timer data, encapsulated in the CreateTimeEntryResponse
.
Then, still within the Endpoints folder, add the following class, which represents the endpoint for data recovery:
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using WorkLog.Data;
using WorkLog.Responses;
namespace WorkLog.Endpoints;
public class GetTimeEntriesEndpoint : EndpointWithoutRequest<List<GetTimeEntryResponse>>
{
private readonly WorkLogDbContext _db;
public GetTimeEntriesEndpoint(WorkLogDbContext db)
{
_db = db;
}
public override void Configure()
{
Get("/time-entries");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken ct)
{
var entries = await _db.TimeEntries
.Select(e => new GetTimeEntryResponse
{
Id = e.Id,
UserName = e.UserName,
ProjectName = e.ProjectName,
Date = e.Date,
Hours = e.Hours,
Description = e.Description,
CreatedAt = e.CreatedAt
})
.ToListAsync(ct);
await SendAsync(entries);
}
}
This separation between input, entity and response models keeps the application more robust, decoupled and ready to evolve. In addition, this structure provides clear reading, reduces the boilerplate common in REST APIs, and offers a more fluid experience for both the writer and the maintainer of the code.
Creating the Validators
FastEndpoints has integration with the FluentValidations package, which means we can perform validations without much effort. So, to configure the validations of our API, create a new folder called “Validators” and, inside it, add the class below:
using FastEndpoints;
using FluentValidation;
using WorkLog.Requests;
namespace WorkLog.Validators;
public class CreateTimeEntryValidator : Validator<CreateTimeEntryRequest>
{
public CreateTimeEntryValidator()
{
RuleFor(x => x.UserName).NotEmpty();
RuleFor(x => x.ProjectName).NotEmpty();
RuleFor(x => x.Date).NotEmpty().LessThanOrEqualTo(DateTime.Today);
RuleFor(x => x.Hours).GreaterThan(0).LessThanOrEqualTo(24);
}
}
Here, we create a validation class to check the input data of the record creation request. Note that the class inherits from Validator
, which is a class from the FastEndpoints namespace. In addition, it implements validation methods such as NotEmpty()
, which belongs to FluentValidation. This way, we have separate and organized validations for each endpoint.
The last thing to do is add the necessary settings for the classes created previously to the Program
class. So, replace the Program
class code with the code below:
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using WorkLog.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<WorkLogDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddFastEndpoints();
var app = builder.Build();
app.UseFastEndpoints();
app.Run();
Here, in addition to configuring the WorkLogDbContext
database class, we also added the FastEndpoints configuration through the methods builder.Services.AddFastEndpoints()
and app.UseFastEndpoints()
.
Note how the Program
class is clean and organized, without the presence of endpoints, as happens when we use the Minimal APIs approach. Now we can reserve the Program
class only for configurations, which is its main function .
Running the Fast Endpoints
Now that the implementations are ready, we can test if the endpoints are working correctly. To do this, we will use Progress Telerik Fiddler Everywhere to execute the requests and check the responses.
First, we will execute the HTTP-POST route: http://localhost:5011/time-entries
to save a new record. This example will use the following JSON:
{
"userName": "John Davis",
"projectName": "john.davis@example.com",
"date": "2025-08-01",
"hours": 6.5,
"description": "Worked on implementing the user authentication flow and fixed bugs related to token refresh."
}
The response in Fiddler returned a status of 200 – OK
, which means that the endpoint worked correctly.
Now, let’s run the same endpoint, but this time without sending the required parameters to check if the FluentValidation validations are working. This time, the following JSON will be sent in the request body:
{
"userName": "",
"projectName": "",
"date": "2025-08-01",
"hours": 6.5,
"description": "Worked on implementing the user authentication flow and fixed bugs related to token refresh."
}
Now, as expected, the response was an error indicating that some fields are mandatory.
Finally, we will make a request that will use the GetTimeEntriesEndpoint
to retrieve the previously inserted record. So, executing the HTTP route: GET – http://localhost:5011/time-entries
, we will have the following result:
Conclusion
FastEndpoints offers a modern and versatile way to build APIs in ASP.NET Core, combining the performance and simplicity of minimal APIs with the controller framework. This balanced approach improves maintainability and facilitates scalability and testability.
In this post, we explore its main advantages and create a sample API using the NuGet package, highlighting how its endpoint-centric design results in cleaner code.
If you are looking for an alternative to traditional API patterns, FastEndpoints is worth exploring. In addition to the features covered in this post, there are others, such as validation, filters and testability, that will probably help you in your projects.
This content originally appeared on Telerik Blogs and was authored by Assis Zang