ASP.NET Core MVC Controller vs. minimal API vs. FastEndpoints — What’s the Best for Performance ?
At least, there are three different ways to build APIs — but which one is the best of the best?
Could it be the legacy MVC Controller, the modern Minimal API, or maybe the external FastEndpoints library?
Let’s figure it out.
To do that, I’m going to create a simple Web API using each approach and measure performance using k6.
First of all, I’ll create an entity that will be used in the DbContext
.
It’s a very simple class with just two properties:
public class User
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(100)]
public string Email { get; set; } = null!;
}
The entity will be added to my DbContext
:
public sealed class UsersDbContext (DbContextOptions options)
: DbContext(options)
{
public DbSet<User> Users { get; set; } = null!;
}
On top of the DbContext
, I’ll create a common service that will be used in all Web APIs.
The service will have methods to get, create, and delete users:
public sealed class UsersService(UsersDbContext dbContext)
: IUsersService
{
public async Task<IReadOnlyCollection<User>> GetAsync()
{
var users = await dbContext.Users.ToListAsync();
return users;
}
public async Task CreateAsync(string email)
{
var user = new User
{
Id = Guid.CreateVersion7(),
Email = email
};
await dbContext.Users.AddAsync(user);
await dbContext.SaveChangesAsync();
}
public async Task DeleteFirstAsync()
{
var firstUser = await dbContext.Users.FirstAsync();
dbContext.Users.Remove(firstUser);
await dbContext.SaveChangesAsync();
}
}
Next, let’s create a controller and inject the service we just created into it:
[Route("api/v1/[controller]")]
[ApiController]
public sealed class HomeController(IUsersService service) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetUsers()
{
var users = await service.GetAsync();
return Ok(users);
}
[HttpPost]
public async Task<IActionResult> CreateUser(CreateUserRequest request)
{
await service.CreateAsync(request.Email);
return NoContent();
}
[HttpDelete]
public async Task<IActionResult> DeleteUser()
{
await service.DeleteFirstAsync();
return NoContent();
}
}
CreateUserRequest
is a record with the following source:
public sealed record CreateUserRequest(string Email);
Next up, I’m going to create endpoints for the Minimal API:
app.MapGet("api/v1/home", async ([FromServices] IUsersService service) =>
{
var users = await service.GetAsync();
return TypedResults.Ok(users);
});
app.MapPost("api/v1/home", async (
[FromServices] IUsersService service,
[FromBody] CreateUserRequest request) =>
{
await service.CreateAsync(request.Email);
return TypedResults.NoContent();
});
app.MapDelete("api/v1/home", async ([FromServices] IUsersService service) =>
{
await service.DeleteFirstAsync();
return TypedResults.NoContent();
});
The last one is going to be FastEndpoints.
Here you can find the GetUserEndpoint
:
public sealed class GetUsersEndpoint(IUsersService service)
: EndpointWithoutRequest<Ok<IReadOnlyCollection<User>>>
{
public override void Configure()
{
Get("api/v1/home");
AllowAnonymous();
}
public override async Task<Ok<IReadOnlyCollection<User>>> ExecuteAsync(
CancellationToken ct)
{
var users = await service.GetAsync();
return TypedResults.Ok(users);
}
}
Next is the CreateUserEndpoint
:
public sealed class CreateUserEndpoint(IUsersService service)
: Endpoint<CreateUserRequest, NoContent>
{
public override void Configure()
{
Post("api/v1/home");
AllowAnonymous();
}
public override async Task HandleAsync(
CreateUserRequest req,
CancellationToken ct)
{
await service.CreateAsync(req.Email);
await SendAsync(TypedResults.NoContent(), cancellation: ct);
}
}
And finally, the DeleteUserEndpoint
:
public sealed class DeleteUserEndpoint(IUsersService service)
: EndpointWithoutRequest<NoContent>
{
public override void Configure()
{
Delete("api/v1/home");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken ct)
{
await service.DeleteFirstAsync();
await SendAsync(TypedResults.NoContent(), cancellation: ct);
}
}
If you’re interested in learning more about FastEndpoints, feel free to check out my video: https://youtu.be/clLtFxomzv8
At the beginning of the test, I reset the database to its initial state — with 10k entities in it.
Each test was run one by one to ensure consistent and isolated results.
Get Users Case:
- MVC Controller
request duration: avg=445.52ms
memory usage: avg = ~194–230 MB
- Minimal API
request duration: avg=430.31ms
memory usage: avg-~184–220 MB
- FastEndpoints
request duration: avg=436.17ms
memory usage: avg: ~190–225 MB
As we can see, Minimal API has the lowest average request duration and memory usage.
FastEndpoints comes in second, followed by the MVC Controller as the slowest.
Create User Case:
- MVC Controller
request duration: avg=75.75ms
memory usage: avg =~70–75 MB
- Minimal API
request duration: avg=72.98ms
memory usage: avg=~60–70MB
- FastEndpoints
request duration: avg=91.09ms
memory usage: avg= ~62–72 MB
The results show that Minimal API once again delivered the best performance.
Notably, FastEndpoints had the lowest performance in this test, however, memory usage is better that for MVC Controller.
Delete User Case:
- MVC Controller
request duration: avg=1.53s
memory usage: avg = ~86–90 MB
- Minimal API
request duration: avg=1.38s
memory usage: avg= ~80–85 MB
- FastEndpoints
request duration: avg=1.51s
memory usage: avg= ~80-84 MB
Overall, Minimal API demonstrated the best performance.
FastEndpoints came in second, while the MVC Controller showed the weakest results.
Across all test cases, the Minimal API consistently delivered the best performance, making it the most efficient option overall.
FastEndpoints showed solid results in most scenarios.
The familiar MVC Controller had the lowest performance.
As alway you can find the code in the following link.