Skip to content
Kumar Chandrachooda
.NET

A Class Per Endpoint: Why FastEndpoints Exists

Controllers grow sideways and Minimal APIs grow into soup. FastEndpoints bets on a third shape - one class per endpoint - and this series reads its source to see how the bet is implemented. Part 1 of FastEndpoints in Depth.

By Kumar Chandrachooda 06 Aug 2025 5 min read
One request, one endpoint class, one response - the REPR shape

Every ASP.NET Core API I have inherited had the same two failure modes. The MVC ones had controllers that grew sideways: OrdersController with fourteen actions, six injected services (each action using two), and a private helper region nobody dared touch. The Minimal API ones had the opposite problem: Program.cs had become a thousand-line lambda soup, and the “group endpoints into extension methods” refactor everyone promises was still on the backlog.

FastEndpoints — the open-source library by Đĵ ΝιΓΞΗΛψΚ and contributors at github.com/FastEndpoints/FastEndpoints — is a bet that the right unit of organisation is neither the controller nor the lambda, but a class per endpoint: the REPR pattern (Request, Endpoint, Response). I have used it in production and, more unusually, I have read a large chunk of its source. This series is the write-up of both: what the library does, and — because the repository is genuinely well-built — what its implementation teaches about ASP.NET Core itself.

To be clear up front: I did not write FastEndpoints. I am a power user and a source-reader. Where the implementation is the story I will quote it, attributed; everything else is fresh example code.

The shape of one endpoint

Here is the whole pattern:

public class CreateOrderRequest
{
    public string CustomerId { get; set; } = "";
    public List<OrderLine> Lines { get; set; } = [];
}

public class CreateOrderResponse
{
    public Guid OrderId { get; set; }
}

public class CreateOrderEndpoint : Endpoint<CreateOrderRequest, CreateOrderResponse>
{
    public override void Configure()
    {
        Post("/orders");
        Roles("sales");
    }

    public override async Task HandleAsync(CreateOrderRequest req, CancellationToken ct)
    {
        var id = await orderService.CreateAsync(req.CustomerId, req.Lines, ct);
        await Send.OkAsync(new() { OrderId = id });
    }
}

No attribute routing, no [FromBody], no IActionResult guessing game. Configure() declares where and under what conditions this endpoint listens; HandleAsync does the work. The request DTO, the endpoint, and the response DTO form a vertical slice you can put in one folder next to its validator and its tests.

Three things about this shape earn their keep at scale:

  1. Dependencies are honest. The class declares exactly what this one operation needs — constructor or property injection, both supported. There is no controller-wide constructor accumulating everyone's dependencies.
  2. Discovery replaces registration. You never write app.MapPost(...) for it. At startup the library scans your assemblies, finds every IEndpoint, and wires it up. (How that scan works — and how a source generator replaces it for Native AOT — is Part 2.)
  3. The class is the test seam. The testing package can create this endpoint directly for unit tests, or drive it over HTTP without knowing its route — typed by the class name (Part 14).

What Minimal APIs don't give you

I like Minimal APIs. For a five-route service they are the right answer, and this series will say so again in its closing part. But the moment a service grows, you start hand-building the things FastEndpoints ships:

  • A validation pipeline. Minimal APIs have no built-in request validation; you end up writing an endpoint filter that resolves a FluentValidation validator and formats a 400. FastEndpoints has that pipeline built in, with a defined error envelope (Part 5).
  • A binding story beyond parameters. Minimal API binding is parameter-oriented. FastEndpoints binds one DTO from up to eight sources — JSON body, form, route, query, claims, headers, cookies, even permission checks — in a documented order (Part 4).
  • Declarative security. Permissions("order:create") on an endpoint compiles into a real ASP.NET Core authorization policy at startup — one per endpoint (Part 7).
  • Cross-cutting hooks. Pre/post processors with a defined position in the pipeline, versioning, response caching, rate limiting, idempotency.

Against MVC the pitch is different: you keep the structure but lose the weight. There is no model-binding-to-action-parameters reflection dance, no filter pipeline you aren't using, no ApiController conventions. The project's own benchmarks (BenchmarkDotNet plus Bombardier load tests, in the repo's Benchmark folder, comparing the same handler as FastEndpoints, Minimal API, and MVC controller) back the claim on the project's site: performance on par with Minimal APIs and noticeably better than MVC controllers in synthetic tests. I treat synthetic benchmarks as directional, not gospel — but the architecture makes the direction believable, because as we will see in Part 3, a FastEndpoints request rides the Minimal API routing infrastructure and then takes over before any of the expensive parts.

Not a framework on top — a framework beside

The detail that made me trust the library enough to read all of it: it does not wrap ASP.NET Core, it composes with it. Each endpoint is registered through the standard IEndpointRouteBuilder.MapMethods(...) call — the same mechanism MapPost uses. Auth is standard AuthorizeAttribute + policies. Swagger is NSwag fed by standard endpoint metadata. Your middleware pipeline is untouched. If FastEndpoints disappeared tomorrow, the concepts (and most of the code inside your handlers) would port straight back to Minimal APIs, because underneath, that is what they are.

The library is also much bigger than the REPR core. Reading the source, the package list is almost a distributed-systems starter kit: an in-process event bus and command bus with middleware (Part 11), commands over gRPC with no .proto files (Part 12), persistent job queues (Part 13), a first-class testing framework, source generators, JWT/refresh-token services, and — my favourite discovery in the whole repository — an implementation of the x402 HTTP micro-payments protocol (Part 16).

Where the series goes

  1. A class per endpoint — this post.
  2. One scanner, five interfaces — how discovery and registration actually work.
  3. The endpoint that pretends to be a result — the routing sleight of hand and the per-request execution pipeline.
  4. Eight sources, one DTO — model binding under the hood.
  5. Validators are singletons — the FluentValidation integration and its lifetime rules.
  6. Pre-processors, post-processors, and the exception that waits — the cross-cutting hooks.
  7. A policy per endpoint — how declarative security compiles.
  8. JWTs, refresh tokens, and revocation — the Security package.
  9. Versioning at the end of the route — release-group versioning vs Asp.Versioning.
  10. Teaching NSwag what your endpoints mean — Swagger generation.
  11. An event bus and a command bus — in-process messaging.
  12. Commands over gRPC, no proto files — remote messaging.
  13. A job queue is a command with a tracking ID — persistent background work.
  14. Route-less tests — the testing story.
  15. Trading reflection for Roslyn — source generators and Native AOT.
  16. Throttling, idempotency, and an HTTP 402 surprise — the cross-cutting features.
  17. When not to use FastEndpoints — the honest retrospective.

If your Program.cs is a thousand lines, or your controllers have a #region helpers, the next sixteen parts are for you. We start where the library starts: at application boot, with a scanner and five interfaces.