When Not to Use FastEndpoints
After sixteen parts inside the source, the honest retrospective - what the library gets right, the sharp edges that will cut you (singletons, statics, magic), and the projects where I would leave it on the shelf. Part 17, the conclusion of FastEndpoints in Depth.
Sixteen parts ago I promised a deep dive written by someone who had both shipped FastEndpoints and read it. The reading is done — pipeline, binder, policies, three messaging systems, the generators — and the debt that remains is judgment. Not a review score; a decision aid. Here is what I now believe about when this library earns its place, and when it doesn't.
What it gets right
The endpoint class is the correct unit. After years of both alternatives, I consider this settled for CRUD-and-beyond APIs of nontrivial size. Vertical slices — request, endpoint, validator, mapper, tests, one folder — survive team growth and refactoring in a way neither controller families nor MapPost forests do. The REPR structure is copyable without the library; the library makes it the path of least resistance.
It drives the platform instead of replacing it. The recurring discovery of this series: the routing integration is a real MapMethods call (Part 3), security compiles to real policies (Part 7), idempotency rides output caching (Part 16). Your ASP.NET Core knowledge, middleware, and tooling all remain valid. Frameworks that wrap platforms rot; frameworks that drive them age with the platform.
Failures move to startup. Duplicate routes, ambiguous validators, missing handlers, conflicting configuration — crash at boot with the type name (Part 2). This philosophy is worth adopting even if you never adopt the library.
Testing is a first-class output. Route-less typed clients, fixture caching, fakeable command handlers (Part 14) — the test seams are designed, not discovered. My integration suites on FastEndpoints projects are the least brittle I have owned.
The breadth is real, and consistently designed. Commands that execute in-process, remotely, or from a persistent queue with one interface (Parts 11–13) replace two or three dependencies with one coherent vocabulary.
The sharp edges
Every one of these cut me or a teammate at least once. None is hidden; all are underestimated.
Singleton lifetimes are the tax on the performance. Validators (Part 5), event handlers, mappers, binders — the hot path avoids allocation by making nearly everything a singleton, and the cost lands on whoever injects a scoped DbContext where it doesn't belong. The failure mode is cruel: it works in the demo, corrupts under concurrency. Teams adopting FastEndpoints need the lifetime table in their onboarding docs, full stop.
Global static configuration. Config and its option groups are static; the serializer is configured once per process; the service resolver is a static instance. It is a deliberate performance trade, visible throughout the source — and it means two differently-configured FastEndpoints apps cannot share a process, and some test isolation scenarios need care. Mostly invisible; occasionally a wall.
Convention magic has a learning cliff. Binding order surprises (“why is my body value ignored?” — a route value overwrote it, Part 4), pre-processors running after validation (Part 6), versioned routes materializing with suffixes (Part 9). Everything has a reason and a doc page; nothing is guessable. Budget real ramp-up time for the second and third developer, not just the enthusiast who introduced it.
It is a framework commitment, not a library dependency. Endpoints inherit from its base class, DTOs carry its attributes, tests use its fixtures. Leaving is a rewrite of your HTTP layer (a mechanical one — back to Minimal APIs — but a rewrite). Weigh that the way you would weigh any framework, because that is what it is.
Bus factor. The project is driven by a small number of maintainers with an excellent track record across many years — but “excellent track record” and “small number” are both facts. The mitigations are genuine: MIT license, unusually readable source (this series is evidence), and standard-platform underpinnings that limit the blast radius if you ever had to fork or leave.
Where I would not use it
- Five-endpoint services. Minimal APIs, no contest. The structure tax of a class per endpoint pays off with dozens of endpoints, not a handful. A webhook receiver does not need a framework.
- Teams fluent and happy in MVC with no pain. Migration has to cure something. If your controllers are disciplined and your filters shallow, the delta isn't worth the churn.
- Heavy content negotiation, views, OData-first, or “enterprise REST” requirements. The library is JSON-API-shaped. Razor pages, XML negotiation, HATEOAS ceremonies — wrong tool.
- Polyglot service meshes where .NET is a guest. The distributed goodies (Part 12) assume shared .NET types end to end. In a schema-first, multi-language environment, use schema-first tools and let FastEndpoints be an implementation detail behind a real contract, if you use it at all.
- Anywhere the team will not read the docs. I mean this practically, not as a scold: the library's value is conventions, and conventions unshared are landmines. One enthusiast plus five people pattern-matching on Stack Overflow MVC answers produces the worst of both worlds.
Where I reach for it without hesitation
The all-.NET product API — tens to hundreds of endpoints, a SPA or mobile clients, one to three services, a team that owns the whole thing. There it is the strongest option in the ecosystem, and the compounding is what sells it: the endpoint classes give you discovery, discovery gives you route-less tests and generated permission constants, metadata gives you release-versioned swagger docs, docs give you generated typed clients, commands give you seams that later become services or jobs without call-site changes. Each part of this series described one link; the chain is the product.
What reading the source taught me
Beyond the library itself, three lessons I have already carried into other work:
- Detection by attribute-absence (the
[NotImplemented]trick from Part 2) — the cheapest way to know if a virtual method was overridden. - The captured-exception
finally(Part 6) — post-hooks that always run, see failures, and can claim them, with original stack traces preserved viaExceptionDispatchInfo. Every pipeline I write now ends this way. - Move ambiguity to startup. Every convention system owes its users loud, early, named-type failures. Silence is the enemy of magic.
A well-built codebase is a free education, and this one — MIT-licensed at github.com/FastEndpoints/FastEndpoints — is one of the better .NET reads available. My thanks to its maintainers, who built the thing; I just read it carefully and shipped on it gladly.
If you start anywhere, start with Part 1 for the shape, Part 3 for the pipeline, and Part 4 before your first binding surprise. The rest will be there when the drawer needs opening.