# 7 Common ASP.NET Core Minimal API Mistakes (And How to Fix Them)

Minimal APIs are genuinely good now. The performance is excellent, the tooling caught up, and on .NET 10 the last few gaps that used to justify reaching for controllers have closed. What has not changed is that the low ceremony makes it very easy to write something that works today and becomes unpleasant in six months. Most ASP.NET Core minimal API mistakes are not bugs at all - they are structural choices that nothing warns you about until the file is two thousand lines long and nobody wants to touch it.

I've inherited a few of those. Every mistake below is one I've either shipped myself or spent a sprint unwinding. The refactored reference project, with the endpoint grouping, the filter pipeline, and the tests that pin the response contracts, is on [Patreon](https://www.patreon.com/CodingDroplets).

Most of these come down to structure decided on day one, which is the hardest thing to retrofit. [Chapter 1 of the Zero to Production course](https://aspnetcoreapi.codingdroplets.com/) works through the controllers-versus-minimal-APIs decision and what a clean `Program.cs` actually looks like, alongside route constraints and response metadata, before any of it has a chance to calcify.

[![ASP.NET Core Web API: Zero to Production](https://newsletter.codingdroplets.com/images/aspnet-core-api-course-banner-1.jpg align="center")](https://aspnetcoreapi.codingdroplets.com/)

## 1\. Putting Business Logic Inside the Endpoint Lambda

The mistake starts small and always ends the same way:

```csharp
app.MapPost("/orders", async (OrderRequest req, AppDbContext db, IEmailSender mail) =>
{
    // 60 lines of validation, mapping, persistence and notification
});
```

**Why it is wrong:** you cannot unit test a lambda in isolation, you cannot reuse it, and the endpoint's contract is now buried inside its implementation. The `Program.cs` grows without limit and every merge touches it.

**The fix:** keep the delegate as routing plus dispatch, and let a real method own the work. Method group references keep the registration readable:

```csharp
app.MapPost("/orders", OrderEndpoints.Create);
```

The handler becomes an ordinary static or instance method with explicit parameters, which is testable without spinning up the host. This is the single change that makes every other item on this list easier.

## 2\. Returning Raw Objects Instead of TypedResults

Returning a plain object works, and that is the problem:

```csharp
app.MapGet("/orders/{id}", async (int id, IOrderService svc) => await svc.GetAsync(id));
```

**Why it is wrong:** you get a 200 with a serialised null when the order does not exist, and the generated OpenAPI document has no idea which status codes the endpoint can produce. Clients then code against a contract your document does not describe.

**The fix:** return `TypedResults`, and declare the full set of outcomes with a `Results<>` union so the metadata is inferred rather than hand-written:

```csharp
static async Task<Results<Ok<Order>, NotFound>> Get(int id, IOrderService svc)
    => await svc.GetAsync(id) is { } order
        ? TypedResults.Ok(order)
        : TypedResults.NotFound();
```

[`TypedResults`](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/responses) is also strongly typed in tests, so you can assert on the result type instead of parsing a response body.

## 3\. Assuming Model Validation Runs Automatically

Developers coming from controllers expect data annotations to be enforced and `ModelState` to be checked. In minimal APIs that never happened by default, so annotated request records sailed straight through to the handler.

**Why it is wrong:** invalid input reaches your domain logic, and the resulting failure is a 500 rather than a 400 with a useful body.

**The fix depends on your target framework.** ASP.NET Core 10 added built-in validation for minimal APIs, opted into explicitly:

```csharp
builder.Services.AddValidation();   // ASP.NET Core 10+
```

On earlier versions, or when you need rules that annotations cannot express, use an endpoint filter that runs FluentValidation before the handler. Our [minimal API validation decision guide](https://codingdroplets.com/aspnet-core-minimal-api-validation-dataannotations-fluentvalidation-endpoint-filters-enterprise) compares the three approaches, and the [what's new in ASP.NET Core 10 minimal APIs](https://codingdroplets.com/whats-new-aspnet-core-10-minimal-api-validation-sse) post covers the built-in option in detail.

Whichever you pick, the important part is that it is a deliberate decision. The default is no validation at all.

## 4\. One Flat Program.cs Instead of Route Groups

Forty endpoints registered in sequence, each repeating the same prefix, the same authorization call, and the same OpenAPI tag.

**Why it is wrong:** repetition drifts. One endpoint gets `RequireAuthorization()` and the next one does not, and nothing tells you.

**The fix:** `MapGroup` applies shared configuration once:

```csharp
var orders = app.MapGroup("/api/orders")
                .RequireAuthorization()
                .WithTags("Orders")
                .AddEndpointFilter<ValidationFilter>();

orders.MapGet("/{id:int}", OrderEndpoints.Get);
orders.MapPost("/",        OrderEndpoints.Create);
```

Then move each group into its own extension method so `Program.cs` stays a table of contents rather than an implementation. Note the `:int` route constraint too: without it, a request to `/api/orders/abc` produces a less useful failure than a clean 404.

## 5\. Not Accepting the CancellationToken

Minimal APIs bind `CancellationToken` automatically, so there is no reason to omit it - and almost everyone does:

```csharp
static async Task<Ok<List<Order>>> List(IOrderService svc, CancellationToken ct)
    => TypedResults.Ok(await svc.ListAsync(ct));
```

**Why it is wrong:** when a client disconnects mid-request, your query keeps running. Under load that is a meaningful amount of wasted database and thread capacity spent on results nobody will receive.

**The fix:** accept the token, and pass it all the way down to EF Core and `HttpClient`. One caveat worth knowing: once you do this properly you will start seeing `OperationCanceledException` in your logs, which is expected rather than a new bug. Our guide to [OperationCanceledException causes and fixes](https://codingdroplets.com/operationcanceledexception-aspnet-core-causes-and-fixes) covers how to log it as information rather than error.

## 6\. Expecting MVC-Style Exception Filters

There are no action filters and no exception filters in minimal APIs. Teams migrating from controllers discover this when their global exception filter simply stops running.

**Why it is wrong:** without a replacement, unhandled exceptions produce an empty 500 in production and inconsistent error shapes everywhere else.

**The fix:** handle it in the pipeline instead, using `IExceptionHandler` with Problem Details:

```csharp
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<DomainExceptionHandler>();
app.UseExceptionHandler();      // first in the pipeline
```

Endpoint filters cover the per-endpoint cross-cutting concerns that action filters used to, but exception handling belongs in middleware. Our post on [ASP.NET Core middleware mistakes](https://codingdroplets.com/aspnet-core-middleware-mistakes-and-fixes) covers the ordering rules that make this work.

## 7\. Treating Authorization as Opt-In Per Endpoint

Decorating each endpoint with `RequireAuthorization()` individually means security depends on nobody forgetting.

**Why it is wrong:** the failure mode is silent and severe. A new endpoint ships without the call and is publicly reachable, and no test fails because no test asserts the negative.

**The fix:** make authorization the default and make anonymous access explicit:

```csharp
builder.Services.AddAuthorizationBuilder()
    .SetFallbackPolicy(new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build());
```

Then mark the genuinely public endpoints with `AllowAnonymous()`. Now the risky choice is the one that requires a visible line of code. Add an integration test that walks the endpoint data source and asserts every endpoint has either an authorization policy or an explicit anonymous marker, and the guarantee holds as the API grows.

## Summary

| Mistake | Fix |
| --- | --- |
| Logic in the lambda | Method group reference to a testable handler |
| Raw object returns | `TypedResults` with a `Results<>` union |
| Assuming validation runs | `AddValidation()` on .NET 10, or an endpoint filter |
| Flat `Program.cs` | `MapGroup` plus per-area extension methods |
| Missing `CancellationToken` | Accept it and thread it through |
| Expecting exception filters | `IExceptionHandler` and `AddProblemDetails()` |
| Per-endpoint authorization | Fallback policy plus explicit `AllowAnonymous` |

None of these are arguments against minimal APIs. They are the conventions that controllers imposed on you for free and that minimal APIs leave you to choose. If you are still weighing the two, our comparison of [minimal APIs versus controllers](https://codingdroplets.com/minimal-apis-vs-controllers-aspnet-core) covers the decision itself. Microsoft's [minimal APIs overview](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/overview) is the reference for the framework details.

## FAQ

### Are minimal APIs suitable for large production applications?

Yes, provided you impose the structure that controllers used to impose for you. Route groups, handler methods outside the registration, a filter pipeline, and a default-deny authorization policy give you the same organisation with less ceremony. Minimal APIs scale badly only when every endpoint is a lambda in one file.

### Do minimal APIs validate request models automatically?

Not by default. ASP.NET Core 10 added built-in validation that you opt into with `AddValidation()`, and before that there was nothing equivalent to MVC's automatic `ModelState` checking. On earlier versions, or for rules beyond data annotations, run FluentValidation through an endpoint filter.

### What replaces action filters and exception filters in minimal APIs?

Endpoint filters replace action filters for per-endpoint cross-cutting concerns such as validation and logging, and they compose in the order registered. Exception filters have no direct equivalent; use `IExceptionHandler` with `AddProblemDetails()` and `UseExceptionHandler()` in the middleware pipeline instead.

### Why should I use TypedResults instead of returning objects directly?

Two reasons. Returning objects gives you a 200 for every outcome, including missing resources, so clients cannot distinguish success from absence. And the OpenAPI document is generated from endpoint metadata, so without declared result types your published contract does not describe the status codes you actually return. `Results<>` unions solve both at once.

### How do I stop a minimal API endpoint from shipping without authorization?

Set a fallback authorization policy requiring an authenticated user, so every endpoint is protected unless it explicitly opts out with `AllowAnonymous`. Then add an integration test that enumerates the endpoint data source and fails if any endpoint has neither an authorization policy nor an explicit anonymous marker.

### Should I migrate existing controllers to minimal APIs?

Rarely worth doing for its own sake. The performance difference is unlikely to be your bottleneck, and a rewrite risks contract changes for no user-visible benefit. Adopt minimal APIs for new endpoints and new services, and let the two coexist in the same application, which is fully supported.

* * *

## About the Author

I'm Celin Daniel, Co-founder of [Coding Droplets](https://codingdroplets.com/). I've been building .NET and ASP.NET Core systems in production for 13+ years - APIs, distributed backends, enterprise platforms. Everything I write here comes from real shipping experience: patterns that held up, trade-offs that bit us, and lessons learned the hard way.

*   GitHub: [codingdroplets](http://github.com/codingdroplets/)
    
*   YouTube: [Coding Droplets](https://www.youtube.com/@CodingDroplets)
    
*   Website: [codingdroplets.com](https://codingdroplets.com/)
