Keep the endpoint boring
The best API endpoint does almost nothing. How keeping endpoints minimal — and finessing it with the Result pattern at Covertime — made every handler read the same way.
The best API endpoint does almost nothing. Take a request, maybe validate it, call into the application layer, hand back what it returns. That's the whole job.
I first ran into this at a previous company. The rule we set ourselves was simple: keep the endpoints minimal. An endpoint takes in a request, maybe runs some validation or a bit of mapping, calls into the application layer where the actual logic lives, and then — based on the result — returns either a problem or a success. Nothing more.
We cared because we'd lived with the opposite. We had endpoints where a lot of the logic had crept in, calling into several different application layers, branching all over the place. They were bloated, hard to read, and every so often genuinely hard to debug — because the thing that was meant to be a thin entry point had quietly become the place where all the work happened.
The endpoint's only job is to translate
HTTP comes in, HTTP goes out. Everything in between belongs somewhere else. The application layer owns the logic and, crucially, owns the outcome — it decides whether the thing succeeded. The endpoint's job is just to turn that outcome into a status code. The moment an endpoint starts deciding things, it's doing someone else's job.
Finessing it with the Result pattern
At Covertime we took it further. Instead of the application layer throwing for expected failures, it returns a Result — either a success value, or a populated error the endpoint can hand straight back as ProblemDetails. “Couldn't find it”, “not allowed”, “that's already been used” aren't exceptional — they happen constantly, by design — so they shouldn't travel as exceptions. They're just the other half of the return type.
app.MapPost("/quotes", async (CreateQuoteRequest request, IQuoteService quotes) =>
{
var result = await quotes.Create(request.ToCommand());
return result.IsSuccess
? Results.Ok(result.Value)
: Results.Problem(result.Error); // a populated ProblemDetails
});Clean and consistent, every time
The payoff is that every endpoint now reads exactly the same way. A new engineer can open any handler in the codebase and know its shape before they've read a line: request in, delegate down, translate the result out. Nothing hiding, nothing to untangle at 3am.
And exceptions go back to meaning what they should — something genuinely went wrong — instead of being control flow for outcomes we already knew were coming.