I stopped writing controllers. Then I stopped writing Minimal APIs too.
Controllers were fine until they weren’t. One endpoint’s logic ended up spread across attributes on the action, the action method itself, and filter classes living somewhere else entirely. When something broke, I went hunting across three files to understand one request.
So I tried Minimal APIs. No controllers, no ceremony, it felt like the framework finally got out of my way. Then the project grew. My Program.cs quietly became a 400-line mess of routing, validation, and error handling all blurred together. The “minimal” API had become a maximal mess.
Both roads led to the same place: endpoints scattered where you’d least expect them.
What I use now
These days I build my endpoints with FastEndpoints. It’s a .NET library built on the REPR pattern, Request, Endpoint, Response. One class per endpoint. Here’s the whole shape:

And with a request body:

Route, verb, and handler, all in one findable class. And wiring it up takes two lines in Program.cs:

That’s it. Program.cs goes back to being a menu.
Three things that sold me
1. One class per endpoint. Everything about an endpoint, its route, its verb, its auth, its logic, lives in one file with a predictable name. When a request misbehaves at 2am, I know exactly where to look. This is the same instinct I’ve applied on every legacy migration: draw the seams first, then fill in the details.
2. Validators are separate classes. FastEndpoints has FluentValidation-style validation built into the pipeline. You write a validator class for the request, and it runs before your handler ever sees the input:

Invalid input never reaches your handler code. It just doesn’t get that far. No more defensive null-checks at the top of every action.
3. Pre/post processors. Cross-cutting concerns, logging, auth checks, caching behaviour, get their own processor classes that run before or after the endpoint:

No middleware soup, no action filters scattered across the project. The pipeline for an endpoint reads top to bottom in one place.
Why it fits how I build: modular monoliths
This is the real reason I chose FastEndpoints, and it’s architectural, not cosmetic.
I build with modular monolith architecture. Each module is a bounded context, its own endpoints, its own validators, its own use cases, with hard boundaries between modules and no tangling across them. It’s the structure that lets a monolith stay maintainable as it grows: break the system down into modules, and each module breaks down into use cases.
FastEndpoints maps onto that shape almost perfectly. One class per endpoint means a module is just a folder of endpoints, each owning its request, its validation, and its handler. A use case like “create order” is a self-contained unit: the endpoint class, its validator, its processors, all living together inside the module boundary. Nothing leaks across.
With controllers, I’d have to impose that structure through discipline and code reviews. With Minimal APIs, I’d have to invent it from scratch. With FastEndpoints, the library’s natural shape and my architecture’s natural shape agree with each other. The right structure is the path of least resistance instead of something I have to fight for.
That’s what “better structure” actually means to me: not more folders, but boundaries the code can’t accidentally cross.
And it’s genuinely fast
This part matters to me: FastEndpoints is built for throughput, without the MVC overhead. It’s consistently among the fastest ways to serve HTTP in .NET. When you’ve spent years optimising page loads and API response times, you notice when a library doesn’t make you pay a tax for its abstractions.
The honest caveats
I’ll say what I’d tell my team: it’s a community library, not a Microsoft framework. That means a smaller ecosystem, fewer Stack Overflow answers, and your team has to learn the REPR pattern before they’re productive. Hiring-wise, “FastEndpoints experience” isn’t a checkbox candidates arrive with.
For me, the trade paid off in the first week. I could find every endpoint in the codebase by filename. Onboarding a new developer meant pointing at one folder and saying “every file here is one endpoint; they all look the same.” That consistency is worth more than framework familiarity.
The real lesson
The code doesn’t rot because the tech is wrong. It rots because nobody protected the structure.
Controllers, Minimal APIs, FastEndpoints, the tool matters less than the discipline. But I’ll take every advantage I can get, and a library that makes the right structure the default shape of the code is an advantage I’ll keep taking.
Same lesson as every migration I’ve done: structure is a choice. FastEndpoints just makes the right choice the easy one.
Controllers, Minimal APIs, or FastEndpoints, what are you building endpoints with?
