Minimal APIs — Missing Auth Middleware Exposes Endpoints
All /products endpoints expose data because auth middleware runs after endpoint mapping.
20+ years shipping production .NET services in enterprise systems. Lessons pulled from things that broke in production.
- Minimal APIs define HTTP endpoints directly in Program.cs without controllers
- Route groups (MapGroup) keep code organized as you scale
- TypedResults enable compile-time return type checking and accurate OpenAPI schemas
- Endpoint filters (AddEndpointFilter) handle cross-cutting concerns like validation without handler pollution
- Performance advantage: 20-40% faster startup than controller-based APIs in benchmarks
- Production risk: misordered middleware (auth after endpoints) silently exposes all routes, undetected by standard unit tests
Imagine you run a food truck. A full restaurant needs a host stand, a maître d', printed menus, linen tablecloths, and a reservations system — even if you're only selling tacos. A food truck skips all that and just hands you a taco through a window. Minimal APIs are ASP.NET Core's food truck: you get exactly what you need (an HTTP endpoint that responds to requests) without setting up a full restaurant (controllers, action filters, routing attributes, view engines). When all you need is a taco, don't build a restaurant.
REST APIs power almost every app you use today — from the weather widget on your phone to the checkout button on an e-commerce site. In the .NET world, the traditional way to build those APIs meant spinning up controllers, wiring up attributes, following the MVC folder structure, and writing a surprising amount of code before a single byte reached a client. For large, team-built enterprise apps that structure is genuinely useful. But for microservices, lightweight backend-for-frontend services, rapid prototypes, or serverless functions, it's overkill that slows you down.
Minimal APIs, introduced in .NET 6 and significantly matured in .NET 7 and .NET 8, let you define an entire HTTP endpoint in a single line of C#. No controller class. No [HttpGet] attribute. No routing conventions to memorise. The framework infers everything it can and gets out of your way. The result is a Program.cs file that reads almost like plain English, compiles faster, and starts up faster — which matters enormously in containerised, auto-scaling environments where cold-start time costs real money.
By the end of this article you'll know exactly when to choose Minimal APIs over controller-based APIs (and vice versa), how to structure a real-world Minimal API with dependency injection, validation, error handling, and OpenAPI docs, and the gotchas that will bite you if you don't know about them. We'll build a working Product Catalogue API from scratch — one section at a time — so every piece of code you see connects to a real need.
What Minimal APIs Actually Are — And What They Don't Do
Minimal APIs are a lightweight hosting model in ASP.NET Core that lets you define HTTP endpoints with minimal ceremony. Instead of controllers, actions, and attributes, you map lambdas directly to routes using app., MapGet()app., etc. The core mechanic is a single MapPost()WebApplication builder that wires middleware, routing, and configuration in a linear, top-down pipeline — no implicit conventions, no reflection-heavy discovery.
In practice, this means you define endpoints as inline delegates or static methods, and the framework compiles them into a request delegate tree at startup. There's no controller activation, no model binding via attributes (unless you opt in), and no built-in authorization filter pipeline. The middleware you add — or forget to add — is the only security boundary. If you don't call app., every endpoint is public, regardless of UseAuthorization()[Authorize] attributes or policy names you might sprinkle on lambdas.
Use Minimal APIs for small services, health checks, or prototypes where the overhead of controllers isn't justified. But the moment you need authentication or authorization, you must explicitly add the middleware. Teams often assume that because they added AddAuthentication() and AddAuthorization() in DI, the middleware is active — it's not. The middleware must be registered in the pipeline, and its position relative to routing matters. This is where production incidents start.
UseAuthentication() and UseAuthorization() must appear between UseRouting() and UseEndpoints() — otherwise, requests bypass auth entirely.AddJwtBearer() in DI but forgot app.UseAuthentication(). All endpoints returned 200 with data, even without tokens.app.UseAuthentication() and app.UseAuthorization() immediately after app.UseRouting(), and verify with a curl call that omits the token.UseAuthentication() and UseAuthorization() explicitly.Your First Minimal API — What's Actually Happening Under the Hood
Before we build something real, let's understand what the framework is actually doing when you write a Minimal API endpoint. When you call app.MapGet(), you're registering a route handler directly with the endpoint routing middleware. There's no controller class being instantiated, no action method being discovered via reflection at startup, and no IActionResult being unwrapped. The lambda you pass IS the handler — it runs directly.
This matters because it changes the performance profile. Controller-based APIs use model binding and action filters that add overhead on every request. Minimal API handlers use a source-generated parameter binder (from .NET 7 onward) that's resolved at compile time. That means faster startup and slightly lower per-request overhead — which adds up at scale.
The builder/app split in Program.cs is also worth understanding. WebApplication.CreateBuilder() sets up the DI container and configuration. builder.Build() locks it in and returns the WebApplication. Everything you call on app after that is middleware or endpoint registration. Order matters here — we'll hit that in the Gotchas section.
Equals() overrides. They're the idiomatic choice for Minimal API DTOs.Results.Ok() vs TypedResults.Ok<T>() is a common trap.Results.Ok() returns an anonymous object — OpenAPI can't generate a schema, so Swagger shows 'any'.Parameter Binding Sources in Minimal APIs — Where Does Each Parameter Come From?
One of the most powerful yet surprising aspects of Minimal APIs is how ASP.NET Core automatically determines the source of each parameter in your handler lambda. The framework uses a set of conventions to decide whether a parameter comes from the route, query string, request body, HTTP headers, DI container, or form data. Understanding these binding rules is essential to avoid unexpected 400 errors or incorrect parameter values in production.
The binding source is determined by the parameter's type and name (and optional attributes). The table below summarises the default sources for common parameter types:
| Parameter Type / Attribute | Binding Source | Example |
|---|---|---|
| Simple types (int, string, Guid) matching a route template token | Route | int id from /products/{id} |
| Simple types NOT matching any route token | Query string | string name from /products?name=foo |
| Complex types (record, class) without explicit attribute | Request body (JSON) | CreateProductRequest request |
| IFormFile / IFormFileCollection | Form data | IFormFile file |
| HttpContext, HttpRequest, HttpResponse, CancellationToken, ClaimsPrincipal | Special services from the framework | HttpContext ctx |
| Services registered in DI (IProductService, LinkGenerator, etc.) | DI container (automatically resolved) | IProductService svc |
| Parameters with [FromRoute], [FromQuery], [FromBody], [FromHeader] | Explicit attribute overrides convention | [FromHeader] string apiKey |
For simple types, the framework first checks if the parameter name matches a route parameter name. If it does, it's bound from the route; otherwise, from the query string. Complex types (records, classes) are bound from the JSON body by default. You can override these conventions using attributes from Microsoft.AspNetCore.Mvc, such as [FromQuery] on a complex type to read it from query string fields.
The source-generated parameter binder is one of the reasons Minimal APIs outperform controller-based APIs. At compile time, for each endpoint, the source generator emits code that efficiently reads and converts parameters from their sources. This eliminates runtime reflection and reduces JIT compilation overhead. The improvement is most noticeable during cold starts in serverless environments.
When a parameter binding fails (e.g., a non-numeric route parameter where an integer is expected, or a malformed JSON body), ASP.NET Core returns a 400 Bad Request with a Problem Details response if AddProblemDetails is registered. This is a significant improvement over controller-based APIs, where invalid model binding often results in a generic 400 without structured error information.
Structuring a Real-World Minimal API — Dependency Injection and Route Groups
The single-file demo looks tidy at 30 lines. At 300 lines it becomes a maintenance nightmare. The good news is Minimal APIs have a clean answer: Route Groups and extension methods. You split your endpoints across feature files and group them under a shared route prefix — without giving up any of the performance benefits.
Route Groups (added in .NET 7) let you call app.MapGroup('/products') and then register all product-related endpoints on that group object. The prefix is applied automatically, middleware like authentication can be attached to the group, and your Program.cs stays clean.
Dependency injection works exactly as you'd expect — you declare your service as a parameter in the handler lambda and ASP.NET Core resolves it from the DI container. There's no constructor injection because there's no class, but the result is the same. Typed parameters from the route, query string, body, and DI container are all resolved by position and type — the framework figures out where each one comes from.
This pattern scales well. You get one feature file per domain entity, a clean Program.cs, and full access to the DI container. It's the pattern used in production Minimal API services today.
Results.Ok(), the schema shows up as 'any' in Swagger. TypedResults also makes your handlers unit-testable — you can assert the exact HTTP status in a unit test without spinning up a test server.Integrating Authentication and Authorization into Minimal API Routes
Authentication and authorization are first-class concerns in Minimal APIs, and the implementation is almost identical to controller-based APIs. You register the authentication handler (e.g., JWT Bearer), define authorization policies, and apply them to entire route groups or individual endpoints using the .RequireAuthorization() method.
The key difference from controllers is that authorization is applied at the route group or endpoint level rather than at the controller or action level. This aligns perfectly with the Minimal API philosophy of reducing ceremony while retaining power. You can also use endpoint filters for fine-grained access control, though the built-in authorization middleware is usually sufficient.
A common pitfall, as illustrated in the Production Incident section, is placing authentication middleware after endpoint mapping. The pipeline order must be: 1. UseAuthentication() 2. UseAuthorization() 3. Endpoint registration (MapGroup, MapGet, etc.)
Failure to follow this order means the endpoint executes before the auth middleware, so .RequireAuthorization() effectively does nothing.
You can also use authorization policies defined with builder.Services.AddAuthorizationCore(), and apply them by name. For example, a policy that requires the "Admin" role can be applied with .RequireAuthorization("AdminOnly"). Custom policies use the same infrastructure as controllers — there's no second-class handling.
AllowAnonymous() on a route group that also has .RequireAuthorization(), the permit-all wins for the entire group. To mix auth/no-auth within a group, apply .AllowAnonymous() only on specific endpoints after the group-level .RequireAuthorization().Validation, Error Handling, and Filters — Keeping Handlers Thin
One criticism of Minimal APIs is that validation logic can creep into handlers, making them fat and hard to test. The mature answer is Endpoint Filters — the Minimal API equivalent of Action Filters in MVC. A filter wraps your handler, runs before and after it, and can short-circuit the pipeline by returning early.
For validation, the community-standard library FluentValidation pairs beautifully with a small generic filter. You register your validators in DI, write one filter, and every endpoint that opts in gets automatic 400 responses with structured error details — no validation code inside any handler.
For global error handling, UseExceptionHandler middleware catches unhandled exceptions and returns a structured Problem Details response (RFC 7807) instead of a naked 500. This is critical for APIs because clients need machine-readable error shapes.
The pattern below shows both working together. Notice how the handler itself stays clean — it trusts that by the time it runs, the input is valid. That separation of concerns is what makes Minimal APIs maintainable at scale.
AddEndpointFilter() calls. If you add an authentication filter AND a validation filter, put authentication first. Validating a request body before confirming the caller is authenticated wastes CPU and can leak information about your schema to unauthenticated users.AddProblemDetails() means unhandled exceptions return plain 500 with no body.UseExceptionHandler() and app.UseStatusCodePages() early in the pipeline.UseProblemDetails() from .NET 8+ gives RFC 7807 errors automatically.Minimal APIs vs Controller-Based APIs — When to Choose Which
This is the question that matters most in a real project, and the honest answer is: it depends on what you're building, not on personal preference. Minimal APIs genuinely win for microservices, internal tooling APIs, BFF (Backend for Frontend) layers, and any scenario where startup time, memory footprint, and simplicity are priorities. Controller-based APIs still win for large team codebases where conventions enforce consistency, for APIs that need complex action filters, for apps already built on MVC, and anywhere OData or API versioning via the established libraries is required.
The most important thing to understand is that this isn't an either/or per project. In .NET 6+ you can mix both in the same application. You might use Minimal APIs for your health check endpoint and lightweight query endpoints, while your complex write operations live in a controller with full model validation pipelines.
From .NET 7 onward, the feature gap has closed significantly. Minimal APIs now support filters, output caching, rate limiting, authentication, authorisation, and OpenAPI — the things that kept teams on controllers. The remaining gap is tooling: Swagger attribute decoration and complex versioning scenarios are still smoother on controllers today.
Advantages and Disadvantages of Minimal APIs
Choosing between Minimal APIs and controller-based APIs is a trade-off, not a universal recommendation. Below is a balanced comparison of the pros and cons to help you decide based on your project's requirements, team size, and long-term maintainability.
| Advantage | Why It Matters |
|---|---|
| Faster startup and lower memory footprint | Critical for serverless and containerized environments where cold-start costs are real. A 30-50% faster startup can reduce infrastructure bills. |
| Less boilerplate | You can build a functional endpoint in 3-5 lines of code. This accelerates prototyping and microservice development. |
| Compile-time source generation | Parameter binding is generated at compile time, not via reflection. This improves runtime performance and reduces memory allocations. |
| Excellent unit testability | Handlers are static methods returning TypedResults — easy to test without spinning up a test server. |
| Route groups with shared middleware | MapGroup allows applying auth, rate limiting, and validation to a group of endpoints without code duplication. |
| Supports all modern ASP.NET Core features | Authentication, authorization, output caching, rate limiting, OpenAPI, health checks — all available. |
| Disadvantage | Why It Matters |
|---|---|
| Less structure for large teams | Without controllers, there's no enforced folder or naming convention. Teams need discipline and code reviews to keep code organized. |
| API versioning is less mature | The Asp.Versioning.Http library works with Minimal APIs but requires more setup than the controller attribute versioning. |
| Complex action filters are harder | Endpoint filters are simpler but don't support the full action filter pipeline (e.g., OnActionExecuting/Acting). For complex cross-cutting concerns, filters may need more work. |
| Tooling for OpenAPI attributes | Controller-based APIs can use [ProducesResponseType] and [SwaggerOperation] attributes extensively; Minimal APIs rely more on .WithOpenApi() and TypedResults, which may not cover all edge cases. |
| Learning curve for new .NET developers | Developers familiar with MVC may find Minimal APIs' lambda style disorienting at first, though the learning curve is short. |
| Not suitable for very complex APIs | If your API has dozens of endpoints with intricate routing, multiple versioning strategies, and heavy use of action filters, controllers may still be the better choice. |
When to choose Minimal APIs: Microservices, BFF layers, internal CRUD tools, serverless functions, health checks, and any scenario where developer velocity is more important than strict conventions.
When to choose Controller APIs: Large enterprise applications with multiple teams, APIs requiring strict API versioning (e.g., OData), migration from legacy MVC projects, or when you need the full action filter pipeline.
Reality check: Most projects can start with Minimal APIs and graduate to controllers only when the feature gap becomes painful. Many successful production APIs use a hybrid approach.
Testing Minimal APIs — Unit Tests Without a Test Server
One of the hidden advantages of Minimal APIs is that your handlers become testable static methods when you extract them into extension classes. Because we used TypedResults, each handler returns a strongly-typed result object like Ok<Product> or Created<Product>. In a unit test, you can mock the injected services, call the handler directly, and assert the exact HTTP status code without spinning up a test server.
For integration tests, ASP.NET Core's WebApplicationFactory works the same as with controllers. You point it at your Minimal API project and call endpoints via an HttpClient. The middleware pipeline runs fully — including auth, rate limiting, and filters — so you can test the entire request/response cycle.
The key difference from controller integration tests: you need to ensure the Program.cs class is accessible. The common pattern is to use a public partial class for Program (e.g., public partial class Program { } at the end of Program.cs) or reference the project via InternalsVisibleTo.
public partial class Program { }. This gives the test project visibility without exposing internal implementation details. Alternatively, use InternalsVisibleTo in the .csproj.Practice Exercises — Build Your Own Minimal API
The best way to internalise Minimal APIs is to build one from scratch. The following three exercises progress from basic CRUD to auth-protected endpoints and API versioning. Each exercise builds on the previous one, so you'll end with a production-like API.
### Exercise 1: CRUD Task Manager API Build a Minimal API for managing tasks. Each task has an Id, Title, IsComplete flag, and CreatedAt timestamp. - Implement GET /tasks, GET /tasks/{id}, POST /tasks, PUT /tasks/{id}, DELETE /tasks/{id}. - Validate that Title is required and between 1 and 200 characters. - Use a static in-memory list as the data store (no database). - Return appropriate HTTP status codes: 201 for create, 204 for update/delete, 404 for not found. - Use TypedResults. - Add OpenAPI support with .WithOpenApi() on the route group.
Hints: Use app.MapGroup("/tasks") and create a TaskEndpoints extension class. Use a List<TaskItem> with a simple TaskService class registered as singleton. For validation, use a ValidationFilter<TaskRequest> endpoint filter with FluentValidation.
Expected outcome: A fully functional CRUD API that you can test with curl or Swagger UI.
### Exercise 2: Add Authentication and Authorization Extend the Task Manager API from Exercise 1 to require authentication. - Add JWT Bearer authentication (you can use a test issuer like dotnet user-jwts tool or configure a simple test identity). - Create two authorization policies: UserPolicy (any authenticated user) and AdminPolicy (requires claim role: admin). - Apply AdminPolicy to DELETE /tasks endpoints — only admins can delete tasks. - Apply UserPolicy to all other endpoints. - Add a public health check endpoint GET /health that requires no authentication. - Write at least one integration test using WebApplicationFactory that verifies a 401 response when the Authorization header is missing.
Hints: Use dotnet user-jwts create to generate a test token. For integration tests, create a custom WebApplicationFactory that sets environment to "Test" and configures a test authentication scheme (e.g., with AddAuthentication().AddScheme<AuthenticationSchemeOptions, TestAuthHandler>("Test", null)).
Expected outcome: Your API now enforces authentication and role-based authorization on specific endpoints.
### Exercise 3: API Versioning with Route Groups Extend the Task Manager API to support two versions: v1 and v2. - v1: Original endpoints as built in Exercise 2. - v2: Change the response shape: include a Priority field and rename IsComplete to Done. v1 must still return the original shape. - Use route groups with version prefixes: /api/v1/tasks and /api/v2/tasks. - Share the service (TaskService) between both versions, but create separate endpoint extension methods for each version. - Add a version header X-API-Version: 2 to indicate v2 responses.
Hints: Use two MapGroup calls: app.MapGroup("/api/v1/tasks") and app.MapGroup("/api/v2/tasks"). Implement separate handler methods for v2 that return a different response record. The service layer can remain unified; mapping logic happens in the endpoint layer.
Expected outcome: Your API supports two concurrent versions with different response schemas, demonstrating how Minimal APIs can handle versioning without libraries.
For all exercises, use the patterns established in this article: TypedResults, endpoint filters for validation, middleware ordering, and clean extension method organisation.
Common Mistakes with Minimal APIs in Production
Here are the mistakes that cause real production incidents — and how to fix them before they become pager alerts.
Mistake 1: Authentication middleware after endpoint mapping Symptom: Endpoints marked with .RequireAuthorization() work without any token. The endpoint runs before auth middleware is reached. Fix: Move app.UseAuthentication() and app.UseAuthorization() to appear before any MapGroup or MapGet calls. The correct order in Program.cs is: builder.Build() → UseAuthentication() → UseAuthorization() → endpoint registration.
Mistake 2: Missing AddProblemDetails leads to silent 500 errors Symptom: Unhandled exceptions return a plain 500 with no body. Clients can't parse the error, and debugging requires log access. Fix: Call builder.Services.AddProblemDetails() and app.UseExceptionHandler() in the middleware pipeline. This gives you RFC 7807 Problem Details responses automatically.
Mistake 3: Using Results.Ok() instead of TypedResults<T> Symptom: Swagger UI shows "any" as the response type. OpenAPI clients can't generate proper types. Fix: Always use TypedResults.Ok<T>() for endpoints returning data. The type parameter gives compile-time safety and accurate OpenAPI schemas.
Minimal API Interview Questions — Junior to Senior
Here are the questions that separate engineers who've only read about Minimal APIs from those who've shipped them.
Junior Level Question What is the simplest way to create a GET endpoint in a Minimal API? Answer: Call app.MapGet("/route", handler) in Program.cs. The handler is a lambda or method that returns the response.
Mid-Level Question How do you apply authorization to a group of endpoints without repeating code? Answer: Use a RouteGroupBuilder returned by app.MapGroup("/prefix") and call .RequireAuthorization(policyName) on it. All endpoints added to that group automatically require the specified policy.
Senior Level Question Explain how the source-generated parameter binder works and why it improves cold start performance. Answer: At compile time, the source generator emits code for each endpoint that reads parameters directly from the request (route, query, body) based on their types and names. This replaces runtime reflection used in controller-based APIs, reducing startup time by 20-40% and eliminating JIT compilation overhead for parameter binding. In serverless environments, this directly reduces cold start latency.
Migrating from Controller-Based APIs to Minimal APIs
Migrating an existing controller-based API to Minimal APIs is not a rewrite - it's a refactor of the endpoint layer. The service layer, repository, and domain models stay untouched. You replace controllers and action methods with extension methods and route groups. The migration can be done incrementally: start with a single endpoint to prove the pattern, then gradually move others.
Key steps: 1. Create a new extension class (e.g., ProductEndpoints) with static methods. 2. In Program.cs, call app.MapProductEndpoints() and remove the controller route registration. 3. Keep the same DI registrations - they work for both. 4. Replace IActionResult with TypedResults for compile-time safety. 5. Update integration tests to target Minimal API endpoints.
The biggest risk during migration is middleware order. Controller-based APIs often rely on global filters for validation and exception handling, while Minimal APIs use endpoint filters and explicit middleware. Ensure that UseAuthentication, UseAuthorization, and UseExceptionHandler are correctly ordered in Program.cs.
Minimal APIs and the Pipeline: Where Your Code Actually Runs
Most devs treat app. like black magic. It's not. Every endpoint is middleware. The MapGet()RequestDelegate you pass gets wrapped in a RouteEndpoint and inserted into the pipeline at the exact position you called it — after UseRouting(), before UseEndpoints() by default.
Why this matters for production? Order of operations isn't academic. If you register exception middleware after your endpoints, you're toast. Errors bubble up but don't get caught. I've debugged silent 500s at 2 AM because someone moved UseExceptionHandler below the MapGet calls.
Here's the concrete pattern: Register all cross-cutting middleware before ANY Map* call. Then chain your groups. If you need middleware per-group, use RequireAuthorization() or custom IApplicationBuilder extensions inside the group pipeline. It's not MVC. You don't get filters for free. You get the raw pipeline. Own it.
UseExceptionHandler after your Map calls, you're swallowing exceptions into middleware never-land. Always register error handling, auth, and CORS before any route definition.Why `UseStaticFiles` and `MapGet` Fight Each Other — And How to Win
You'd think serving an index.html alongside your /api/users endpoint is trivial. It is, until you profile and realize every static file request is hitting your middleware stack for nothing. Static files should short-circuit. They don't if you order them wrong.
Here's the problem: app. is middleware. If you put it after your auth middleware, every UseStaticFiles().js, .css, and favicon request runs through JWT validation first. That's CPU waste at scale. I saw a client's API latency double because their CDN was hitting authenticated static endpoints.
Fix: Call UseStaticFiles() BEFORE any auth middleware. Unless you want to protect static assets (rarely the case), let them bypass the pipeline. If you DO need auth on specific static files, use MapFallbackToFile() with authorization — not UseStaticFiles at the end.
Extra sharp edge: Minimal APIs don't have [AllowAnonymous] attributes. Static files are unauthenticated by default. If you have a global auth filter, it won't touch static files. That's deliberate. Don't fight it.
app.Urls.Add("http://+:5000") and Bombardier. If static files take >50ms, you're running them through auth. Move UseStaticFiles() to the top of the pipeline.OpenAPI and Minimal APIs — Why Your Swagger UI is Wrong (And How to Fix It)
You added AddOpenApi() and MapOpenApi(). Swagger UI renders. Great. Now open any POST endpoint — the schema's a mess. Required fields missing. Types wrong. Nullable mismatch. That's because Minimal APIs don't implicitly generate metadata from your handler's parameters the way controllers do with [FromBody] and [ApiController].
Every request delegate is a black box to the OpenAPI generator. If you pass a Product object without defining a [EndpointSummary] or using WithOpenApi() to annotate it, Swagger guesses. And it guesses wrong — especially with record types and nullable reference types.
Production fix: Use WithOpenApi() on every non-trivial endpoint. Or better, use AddOpenApi() with an OpenApiOptions transformer. You can inject descriptions, fix request bodies, and ensure status codes are documented. Otherwise your frontend team will waste hours debugging 400 vs 422 responses because the schema says string but you return int.
Worst-case: disable the default schema generation entirely and provide explicit request/response types via [ProducesResponseType] equivalents. Minimal doesn't mean undocumented. It means you do the documentation yourself.
.WithOpenApi() on each endpoint, or use a document transformer to override bogus schemas. Your API consumers will thank you.WithOpenApi() per endpoint or a document transformer to fix schemas before your consumers see them.Prevent Over-Posting: Stop Clients From Silently Destroying Your Data
Over-posting is the security hole nobody talks about until it bites them. A client sends a POST or PUT with extra fields — like IsAdmin: true on a user registration endpoint — and your model binder just obediently sets them. Minimal APIs make this worse because there's no natural place to enforce a boundary like [Bind] on a controller parameter.
The fix is to never bind your domain entities directly. Create a separate DTO (data transfer object) that only exposes the properties you want the client to control. Map that DTO to your domain model inside the handler — explicitly. That IsAdmin field simply doesn't exist on the DTO, so even if the client sends it, it gets ignored.
Why this works: your endpoint contract becomes the DTO shape, not your database schema. The compiler enforces what the client can touch. Senior devs don't trust client input — they design it out of the equation.
Examine the PATCH Endpoint: Partial Updates Without the Full Reset
PUT replaces the entire resource. PATCH is for partial updates — but most teams implement PATCH as a glorified PUT with a null check. That destroys the entire point. A true PATCH endpoint uses JsonPatchDocument<T> from the Microsoft.AspNetCore.JsonPatch package, which applies a list of operations: add, replace, remove, test. The client sends exactly what changed, not the whole object.
Why bother? Because in production, a PUT that replaces the entire 50-field customer object litters your logs with unnecessary writes and creates merge conflicts when two services hit the same record. A PATCH with a single replace /email operation is atomic, auditable, and intentional.
Minimal APIs support JsonPatchDocument natively through the [FromBody] source. You apply it to your entity, then save. No DTO mapping required — the patch document is the contract. But watch out: you still need validation, because a test operation that fails will rollback the entire chain.
JsonPatchDocument to a DTO, not the entity, if your entity has computed properties or navigation collections. Otherwise a malicious add operation can corrupt related data.JsonPatchDocument for true partial updates — atomic, auditable, and safe.Why Your Minimal API Needs Those Exact Packages — And What Happens If You Omit One
A Minimal API project template already includes Microsoft.AspNetCore.App implicitly, but production apps require explicit packages for features like OpenAPI, authentication, or EF Core. Omitting Microsoft.AspNetCore.OpenApi means your Swagger endpoint returns 404 with no error — the framework silently skips OpenAPI generation if the package is missing. Without Microsoft.AspNetCore.Authentication.JwtBearer, your AddAuthentication(). call compiles but throws at runtime when the first token arrives. The key insight: ASP.NET Core uses a "convention-plus-package" model — the API surface compiles but the middleware pipeline fails if the underlying package isn't resolved. Always add AddJwtBearer()Microsoft.AspNetCore.OpenApi for Swagger, Swashbuckle.AspNetCore for the UI, and Microsoft.AspNetCore.Authentication.* for auth. Use dotnet list package after adding to verify transitive dependencies resolved correctly.
--no-restore, missing packages cause silent 500 errors. Always run dotnet restore explicitly in your pipeline.Wrap-Up — The Single Rule That Prevents 90% of Minimal API Failures in Production
After building, deploying, and troubleshooting Minimal APIs, one pattern separates maintainable apps from disaster: every route handler must be a single-purpose function that receives exactly the data it needs and returns exactly one shape of response. If you find yourself adding HttpContext accessor, multiple using blocks inside a handler, or conditional Results.Ok vs Results.BadRequest paths, you've outgrown Minimal API patterns and should extract that logic into a service. The production failure pattern is always the same: a route handler grows beyond 15 lines, gets a null reference, and you lose the stack trace because ASP.NET Core wraps Minimal API exceptions in a generic BadHttpRequestException. Keep handlers thin — they should only bind parameters, call a method, and map the result. Anything else belongs in a separate class.
HttpContext to a handler, the framework can't optimize your endpoint with source generators — your cold start latency increases by 60-120ms on every request.try/catch, extract it.Overview
Minimal APIs in ASP.NET Core represent a paradigm shift away from the controller-heavy patterns of traditional web APIs. Instead of requiring separate classes, attributes, and complex routing logic, Minimal APIs let you define endpoints as simple lambda expressions directly within Program.cs. This approach drastically reduces boilerplate code, making it ideal for microservices, small-to-medium APIs, and rapid prototyping. However, the simplicity of Minimal APIs can be deceptive—what looks like a quick app.MapGet() can become a maintenance nightmare if production concerns like validation, error handling, and middleware ordering are ignored. This guide covers everything from interview prep to silent data corruption to OpenAPI misconfigurations. By the end, you'll understand not just how to write endpoints, but why each decision—like choosing AddSingleton over AddScoped—affects your deployment. Minimal APIs aren't a toy; they're a powerful tool when their boundaries are respected.
Prerequisites
Before you start writing Minimal APIs, ensure your environment is set up correctly. You need .NET 6 or later (preferably .NET 8 or 9 for the latest features), an IDE like Visual Studio or JetBrains Rider, and basic familiarity with HTTP verbs (GET, POST, PUT, PATCH, DELETE). Unlike controller-based APIs, Minimal APIs don't require Startup.cs, so you'll work directly with WebApplication and WebApplicationBuilder. You must understand the difference between app.MapGet() and app.UseEndpoints()—the former registers an endpoint directly, while the latter belongs to the middleware pipeline. Also, install the Microsoft.AspNetCore.OpenApi package if you need Swagger, and a serialization package like System.Text.Json (already included). If you plan to use Entity Framework Core, add the NuGet package for your provider (e.g., Npgsql.EntityFrameworkCore.PostgreSQL). Not having these packages leads to runtime exceptions, not compilation errors. Finally, know the order of middleware: UseStaticFiles must come before MapGet to serve files correctly. Miss this, and your static files will 404.
Missing Authentication Middleware Exposes Endpoints
RequireAuthorization().RequireAuthorization() on the route group is sufficient to enforce authentication.UseAuthentication() and app.UseAuthorization() calls were placed after the call to app.MapProductEndpoints(). Auth middleware never ran on the endpoint pipeline because endpoint middleware runs before auth when registered later.UseAuthentication() and UseAuthorization() before MapProductEndpoints(). The correct order in Program.cs is: builder.Build(); then app.UseAuthentication(); app.UseAuthorization(); then app.MapProductEndpoints(); then app.Run();- Middleware order in Program.cs is absolute — auth middleware must be registered before endpoint routing.
- Always place UseAuthentication before UseAuthorization, and both before any endpoint mapping.
- Use WebApplicationFactory integration tests to verify auth enforcement across all endpoints - don't rely on manual curl checks.
AddEndpointsApiExplorer() in service registration. Ensure UseSwaggerUI() is called after UseSwagger() in the middleware pipeline. Add .WithOpenApi() to the group or endpoint.Services.AddProblemDetails() and app.UseStatusCodePages(). Ensure the ValidationFilter returns ValidationProblem(errors) rather than BadRequest.dotnet run --environment Developmentcurl -v http://localhost:5000/productsMapGet() or MapPost() calls are present before app.Run() and after the middleware pipeline. Use app.UseRouting() is not needed - Minimal APIs wire routing internally.Common mistakes to avoid
5 patternsAuthentication middleware after endpoint mapping
RequireAuthorization() work without any token. The endpoint runs before auth middleware is reached.UseAuthentication() and app.UseAuthorization() to appear before any endpoint registration (MapGroup, MapGet, etc.).Missing AddProblemDetails leads to silent 500 errors
Services.AddProblemDetails() and app.UseExceptionHandler() in the middleware pipeline.Using Results.Ok() instead of TypedResults<T>
Forgetting to register services before builder.Build()
Build().Not exposing Program class for integration tests
public partial class Program { } at the end of Program.cs or use InternalsVisibleTo in .csproj.Interview Questions on This Topic
What is the difference between app.MapGet() with a lambda vs a static method?
Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Lessons pulled from things that broke in production.
That's ASP.NET. Mark it forged?
23 min read · try the examples if you haven't