ASP.NET Core Minimal APIs: When to Use Them and When Not To Use Them
- Get link
- X
- Other Apps
Minimal APIs are an endpoint-first way to build HTTP APIs in ASP.NET Core by mapping routes directly to handlers (no controllers required). They’re best for teams building small-to-medium, bounded services (microservices, BFFs, webhooks, internal APIs) that want fast iteration with less ceremony, as long as you add structure early. They solve the “too much framework for a small API” problem, and they matter because choosing the wrong model (or letting Program.cs become a dumping ground) creates long-term maintenance pain.
What decision should you make: Minimal APIs or Controllers?
Use this checklist and pick the first match.
Choose Minimal APIs when your API is bounded and you can enforce conventions
- You’re building a focused service boundary (webhooks, microservice, BFF, internal tool API).
- You can enforce a module convention (route groups + one file per feature).
- Your team likes explicit routing and fluent metadata (OpenAPI tags, response types).
- You want cross-cutting behavior via middleware + endpoint filters rather than MVC filters.
Choose Controllers when the API surface is large or the team is large
- You expect many endpoints across multiple domains/features.
- You rely heavily on attribute-driven conventions (routing/filters/versioning patterns).
- Many developers contribute and you need high discoverability via familiar conventions.
- Your org already has controller-first templates, libraries, or standards.
Checkpoint: If you can’t commit to a module convention on day 1, controllers are usually the safer default.
What will you build in this guide?
You’ll build a production-shaped Minimal API starter with one clear outcome:
End result: A Minimal API that’s structured with route groups + feature modules, uses endpoint-filter validation, returns consistent Problem Details errors, exposes OpenAPI docs, and is verifiable with curl + an integration test.
What prerequisites do you need before starting?
- .NET SDK installed
- Basic C# knowledge
- Terminal access (PowerShell/Bash)
- Optional: Postman/Insomnia
Checkpoint: dotnet –version prints a version successfully.
How does a Minimal API request flow work in production?
A[HTTP Request] –> B[Middleware pipeline]
B –> C[Routing selects endpoint]
C –> D[Endpoint filters (optional)]
D –> E[Handler runs]
E –> F[Result writes response (JSON, status code)]
F –> G[HTTP Response]
Checkpoint: You can point to exactly where you’ll put (1) global behavior (middleware), (2) per-feature behavior (endpoint filters), and (3) business orchestration (handlers).
How do you avoid a giant Program.cs with Minimal APIs?
Use a feature-module convention:
- Program.cs only wires dependencies, middleware, and “maps modules”
- Endpoints/Endpoints.cs defines routes for a feature via a route group
- Infrastructure/ holds cross-cutting filters (validation, correlation, etc.)
- Services/ holds business logic (handlers stay thin)
Checkpoint: You can add a new feature by creating one new Endpoints/*Endpoints.cs file, without touching existing endpoint files.
How do you build the production starter step by step?
1) How do you create a Minimal API project?
dotnet new web -n MinimalApiStarter
cd MinimalApiStarter Checkpoint: dotnet run starts the server without errors.
2) How do you enable OpenAPI document generation?
Add the official OpenAPI package:
dotnet add package Microsoft.AspNetCore.OpenApi
Checkpoint: Package install succeeds.
3) How do you create a service layer so handlers don’t become “mini controllers”?
Create Services/ProductService.cs:
namespace MinimalApiStarter.Services;
public record ProductDto(int Id, string Name, decimal Price);
public record CreateProductRequest(string Name, decimal Price);
public interface IProductService
{
Task GetAsync(int id, CancellationToken ct);
Task CreateAsync(CreateProductRequest request, CancellationToken ct);
}
public sealed class ProductService : IProductService
{
public Task GetAsync(int id, CancellationToken ct)
=> Task.FromResult(id == 404 ? null : new ProductDto(id, "Demo", 10m));
public Task CreateAsync(CreateProductRequest request, CancellationToken ct)
=> Task.FromResult(new ProductDto(123, request.Name, request.Price));
} Checkpoint: No ASP.NET-specific types appear in the service.
4) How do you centralize validation using an endpoint filter?
Create Infrastructure/ValidateCreateProductFilter.cs:
using Microsoft.AspNetCore.Http.HttpResults;
using MinimalApiStarter.Services;
namespace MinimalApiStarter.Infrastructure;
public sealed class ValidateCreateProductFilter : IEndpointFilter
{
public async ValueTask InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var request = context.Arguments.OfType().FirstOrDefault();
if (request is null)
return TypedResults.BadRequest(new { error = "Request body is required." });
var errors = new Dictionary();
if (string.IsNullOrWhiteSpace(request.Name))
errors["name"] = new[] { "Name is required." };
if (request.Price < 0)
errors["price"] = new[] { "Price must be >= 0." };
if (errors.Count > 0)
return TypedResults.ValidationProblem(errors);
return await next(context);
}
} Checkpoint: Validation rules are written once and reused, handlers stay clean.
5) How do you define endpoints as a feature module with a route group?
Create Endpoints/ProductsEndpoints.cs:
using Microsoft.AspNetCore.Http.HttpResults;
using MinimalApiStarter.Infrastructure;
using MinimalApiStarter.Services;
namespace MinimalApiStarter.Endpoints;
public static class ProductsEndpoints
{
public static RouteGroupBuilder MapProductsEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/products")
.WithTags("Products")
.RequireAuthorization("api");
group.MapGet("/{id:int}", async Task<Results<Ok<ProductDto>, NotFound>> (
int id,
IProductService service,
CancellationToken ct) =>
{
var product = await service.GetAsync(id, ct);
return product is null ? TypedResults.NotFound() : TypedResults.Ok(product);
})
.Produces<ProductDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.WithOpenApi();
group.MapPost("/", async Task<Created<ProductDto>> (
CreateProductRequest request,
IProductService service,
CancellationToken ct) =>
{
var created = await service.CreateAsync(request, ct);
return TypedResults.Created($"/api/products/{created.Id}", created);
})
.AddEndpointFilter<ValidateCreateProductFilter>()
.Produces<ProductDto>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.WithOpenApi();
return group;
}
} Checkpoint: No endpoints are mapped inside Program.cs.
6) How do you wire auth, ProblemDetails, OpenAPI, and modules in Program.cs?
Replace Program.cs with:
using MinimalApiStarter.Endpoints;
using MinimalApiStarter.Services;
var builder = WebApplication.CreateBuilder(args);
// OpenAPI document generation
builder.Services.AddOpenApi();
// Problem Details support (standardized error shape)
builder.Services.AddProblemDetails();
// Demo authorization policy (replace with real auth in production)
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("api", policy => policy.RequireAssertion(_ => true));
});
builder.Services.AddScoped();
var app = builder.Build();
// Centralized exception handler (works well with ProblemDetails)
app.UseExceptionHandler();
app.UseAuthorization();
// Expose OpenAPI JSON endpoint
app.MapOpenApi();
// Map feature modules
app.MapProductsEndpoints();
app.Run();
// For integration tests (WebApplicationFactory)
public partial class Program { } Checkpoint: dotnet run compiles and starts successfully.
How do you test and verify the API?
How do you run the API locally?
dotnet run How do you verify GET returns a product?
curl -i http://localhost:5000/api/products/1 Expected:
- 200 OK
- JSON like: {“id”:1,”name”:”Demo”,”price”:10}
Checkpoint: You get a 200 with JSON for /1.
How do you verify GET returns 404 for missing
curl -i http://localhost:5000/api/products/404 Expected:
- 404 Not Found
Checkpoint: /404 returns 404.
How do you verify validation runs via the endpoint filter?
curl -i -X POST http://localhost:5000/api/products \
-H "Content-Type: application/json" \
-d "{\"name\":\"\",\"price\":-1}" Expected:
- 400 Bad Request
- A validation payload (from ValidationProblem)
Checkpoint: Validation fails before the handler runs.
How do you verify OpenAPI is being generated?
Fetch the OpenAPI JSON exposed by MapOpenApi() (path depends on your ASP.NET Core version/config). Confirm you see a JSON document containing /api/products/{id} and /api/products.
Checkpoint: OpenAPI JSON includes your endpoints and response metadata.
How do you add an integration test to prove endpoint wiring works?
1) How do you add a test project?
dotnet new xunit -n MinimalApiStarter.Tests
dotnet add MinimalApiStarter.Tests reference MinimalApiStarter
dotnet add MinimalApiStarter.Tests package Microsoft.AspNetCore.Mvc.Testing 2) How do you write a minimal integration test?
Create MinimalApiStarter.Tests/ProductsApiTests.cs:
using System.Net;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
public class ProductsApiTests : IClassFixture>
{
private readonly HttpClient _client;
public ProductsApiTests(WebApplicationFactory factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task Get_Product_ReturnsOk()
{
var resp = await _client.GetAsync("/api/products/1");
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
}
[Fact]
public async Task Get_MissingProduct_ReturnsNotFound()
{
var resp = await _client.GetAsync("/api/products/404");
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);
}
} Run:
dotnet test Checkpoint: Tests pass and confirm routing + module mapping works.
What are the most common production mistakes with Minimal APIs and how do you fix them?
1) Why did my Minimal API turn into a “giant Program.cs”?
Cause: You mapped endpoints directly in Program.cs as the project grew.
Fix: Move each feature into Endpoints/Endpoints.cs with MapEndpoints().
2) Why are my endpoints inconsistent (validation, errors, logging)?
Cause: Each handler does its own checks and error shapes.
Fix: Use endpoint filters for validation, and standardize error responses using Problem Details.
3) Why does RequireAuthorization(“api”) throw “policy not found”?
Cause: Policy wasn’t registered or name mismatch.
Fix: Ensure AddAuthorization(options => options.AddPolicy(“api”, …)) matches exactly.
4) Why does my POST look like the handler runs even with bad input?
Cause: Validation is in-handler, or the filter wasn’t applied.
Fix: Confirm .AddEndpointFilter() is on the endpoint or group.
5) Why is my OpenAPI endpoint returning 404?
Cause: OpenAPI services weren’t added or endpoint wasn’t mapped.
Fix: Confirm builder.Services.AddOpenApi() and app.MapOpenApi() are both present.
What did you achieve, and what should you do next?
You now have a Minimal API starter that’s intentionally “production-shaped”:
- Route groups + feature modules keep code discoverable
- Endpoint-filter validation keeps handlers thin
- ProblemDetails gives predictable error responses
- OpenAPI generation keeps API docs accurate
- Integration tests prove wiring won’t regress
Next step: Replace the demo authorization policy with real authentication (JWT/OAuth), then align your API security checklist with OWASP’s API Security Top 10.
Start today and unlock all features of BoldSign.
Need assistance? Request a demo or visit our Support Portal for quick help.
- Mastering Long-Running Tasks in ASP.NET Core Without
Blocking Requests
- ASP.NET Core JWT Authentication: Setup, Validation, and
Best Practices
- Modern Background Jobs in ASP.NET Core: Comparing Hosted
Services, Hangfire, and Quartz.NET
Note: This blog was originally published at boldsign.com
- Get link
- X
- Other Apps

Comments
Post a Comment