FastEndpoints vs Controllers vs Minimal APIs in .NET 10
You're 40 controllers deep, each with six or more actions, and your project structure looks like a filing cabinet from 1997. Or maybe you went all-in on Minimal APIs and your Program.cs has become a 600-line god file that nobody wants to touch. Either way, you're paying a tax you don't need to pay.
I've shipped production APIs with all three approaches: traditional MVC controllers, Minimal APIs, and FastEndpoints. This isn't a theoretical comparison. It's a verdict. For most new .NET 10 projects, FastEndpoints wins.
Let me show you why with real code, real benchmarks, and a migration path you can follow this week.
The Same Endpoint, Three Ways
The best way to compare these approaches is to build the same thing three times. Here's a "Create Order" endpoint. Same validation, same response, three different styles.
MVC Controller
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
public OrdersController(IOrderService orderService)
=> _orderService = orderService;
[HttpPost]
[ProducesResponseType(typeof(OrderResponse), 201)]
[ProducesResponseType(400)]
public async Task<ActionResult<OrderResponse>> Create(
[FromBody] CreateOrderRequest request)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var order = await _orderService.CreateAsync(request);
return CreatedAtAction(nameof(Get), new { id = order.Id }, order);
}
}
Five attributes. A base class. Constructor injection ceremony. ModelState checks you'll forget half the time. This is what we've normalised over the past decade.
Minimal API
app.MapPost("/api/orders", async (
CreateOrderRequest request,
IOrderService orderService) =>
{
var order = await orderService.CreateAsync(request);
return Results.Created($"/api/orders/{order.Id}", order);
})
.WithName("CreateOrder")
.Produces<OrderResponse>(201)
.ProducesValidationProblem();
Fewer lines. No class ceremony. But where does validation live? Where do you put this when you have 50 endpoints? 100? The answer is "scattered across extension methods and partial classes," and it gets ugly fast.
FastEndpoints
public class CreateOrderEndpoint : Endpoint<CreateOrderRequest, OrderResponse>
{
public override void Configure()
{
Post("/api/orders");
AllowAnonymous();
}
public override async Task HandleAsync(CreateOrderRequest req, CancellationToken ct)
{
var order = await orderService.CreateAsync(req);
await SendCreatedAtAsync<GetOrderEndpoint>(
new { id = order.Id }, order, cancellation: ct);
}
}
Zero attributes. One class, one responsibility. The REPR pattern (Request-Endpoint-Response) gives you the isolation of a controller action with the performance of a Minimal API. Validation lives in a separate Validator class using FluentValidation, auto-discovered and applied before your handler ever runs.
The line count difference matters less than the cognitive load difference. With MVC, you need to know about 15 different attributes and their interactions. With FastEndpoints, you configure behaviour in a single Configure() method using plain method calls. IntelliSense does the rest.
Performance Under Load
Talk is cheap. Here are the numbers from benchmark tests running identical "hello world" style endpoints on the same hardware.
| Approach | Requests/sec | Mean Latency | Memory/Request |
|---|---|---|---|
| Minimal APIs | 257,730 | 24.7us | 14.8 KB |
| FastEndpoints | 254,103 | 26.8us | 15.2 KB |
| MVC Controllers | 224,799 | 40.6us | 22.1 KB |
FastEndpoints sits within 1.4% of raw Minimal APIs. That gap is essentially noise in any real application where you're hitting a database, calling external services, or doing any actual work.
MVC Controllers pay a 13% throughput tax and allocate roughly 50% more memory per request. For what? Attribute routing resolution, model binding middleware, action filter pipelines, and result execution overhead that most APIs never use.
At scale, the performance difference compounds. If your API handles 10 million requests per day, that 13% gap means provisioning extra infrastructure. That's real money for zero business value.
But I'll be honest. Performance alone isn't why I recommend FastEndpoints. The maintainability story is where it really pulls ahead.
Where Each Approach Actually Fits
I'm opinionated here, but I've earned it through shipping all three to production.
MVC Controllers: The Legacy Default
Use MVC controllers when:
- You have an existing Razor Pages app that also serves a few API endpoints
- Your team has 10 years of muscle memory around the pattern and no appetite for change
- You're maintaining a legacy .NET project and a rewrite isn't justified
Don't pick MVC for new API-only projects in 2026. The ceremony-to-value ratio is awful.
Minimal APIs: The Prototype Special
Use Minimal APIs when:
- You're building a tiny microservice with 1 to 3 endpoints
- You need a quick prototype or proof of concept
- The entire API fits comfortably in a single file
Minimal APIs are fantastic for small things. The problem is that small things grow. Once you pass about 10 endpoints, you're inventing your own organisational patterns (endpoint classes, extension methods for grouping, custom filters). At that point, you're building a worse version of FastEndpoints by hand.
FastEndpoints: The Default Choice
Use FastEndpoints when:
- Your API has (or will have) more than 10 endpoints
- You want built-in validation without wiring up FluentValidation manually
- You care about testability (each endpoint is a class, trivially unit-testable)
- You want endpoint discovery and registration without maintaining route maps
The auto-discovery system scans your assembly at startup, finds all endpoint classes, and registers them. No app.MapPost(...) for every route. No [Route] attributes scattered across controllers. You add a class, it works.
FastEndpoints also gives you built-in JWT auth configuration, pub/sub events for domain event handling, and model binding from route params, query strings, JSON body, headers, claims, and form data. All without extra NuGet packages.
The "real project" test is simple: what happens at 100 endpoints? With MVC, you have 20 controller files averaging 5 actions each, bloated dependency injection, and action filters that affect things you didn't intend. With Minimal APIs, you have a sprawling web of extension methods or a Program.cs that makes interns cry. With FastEndpoints, you have 100 small, focused classes. Each one is readable in isolation. Each one is testable in isolation. That's it.
Migration Path: Controllers to FastEndpoints in 5 Minutes
You don't need a big bang migration. FastEndpoints runs alongside MVC controllers in the same project. Here's how to start.
Step 1: Install the package
dotnet add package FastEndpoints
Step 2: Wire up in Program.cs
using FastEndpoints;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFastEndpoints();
// Keep your existing controller registration
builder.Services.AddControllers();
var app = builder.Build();
app.UseFastEndpoints();
app.MapControllers(); // existing controllers still work
app.Run();
Both systems run side by side. No conflicts. Migrate one endpoint at a time.
Step 3: Convert one controller action
Take your simplest controller action. Here's a typical "Get by ID" before and after.
Before (Controller):
[HttpGet("{id}")]
public async Task<ActionResult<OrderResponse>> Get(int id)
{
var order = await _orderService.GetByIdAsync(id);
if (order is null) return NotFound();
return Ok(order);
}
After (FastEndpoints):
public class GetOrderEndpoint : Endpoint<GetOrderRequest, OrderResponse>
{
public override void Configure()
{
Get("/api/orders/{Id}");
AllowAnonymous();
}
public override async Task HandleAsync(GetOrderRequest req, CancellationToken ct)
{
var order = await orderService.GetByIdAsync(req.Id);
if (order is null)
{
await SendNotFoundAsync(ct);
return;
}
await SendOkAsync(order, ct);
}
}
public record GetOrderRequest
{
public int Id { get; init; }
}
Delete the action from your controller. If the controller is now empty, delete the controller. Repeat.
Step 4: Add validation (optional but free)
public class GetOrderValidator : Validator<GetOrderRequest>
{
public GetOrderValidator()
{
RuleFor(x => x.Id).GreaterThan(0);
}
}
Drop this class anywhere in your project. FastEndpoints discovers it automatically and runs validation before your handler executes. No [Required] attributes. No ModelState.IsValid checks. No manual wiring.
My Verdict
For any new .NET 10 API with more than a handful of endpoints, FastEndpoints is the default choice. Full stop.
You get Minimal API performance (within 1.4%), real project structure that scales to hundreds of endpoints, built-in validation, and a testing story that doesn't require spinning up an entire WebApplicationFactory just to test one route.
Stop paying the MVC tax. The 13% performance hit. The 50% extra memory allocation. The attribute soup. The bloated controllers. None of it is buying you anything in 2026.
Here's what I'd do this week: pick one controller endpoint (your simplest one), migrate it to FastEndpoints, and run both side by side. It takes 5 minutes. Once you feel the difference in clarity, you won't go back.
That's the 75% easier path. Not a framework rewrite. Not a risky migration. Just one endpoint at a time, getting simpler as you go.