ASP.NET Core Minimal APIs: When to Use Them and When Not To Use Them

 

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?

.NET
dotnet new web -n MinimalApiStarter 
cd MinimalApiStarter     

Checkpoint: dotnet run starts the server without errors.

2) How do you enable OpenAPI document generation?

.NET
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”?

.NET
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?

.NET
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?

.NET
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?

.NET
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?

.NET
dotnet run     

How do you verify GET returns a product?

cURL
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
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
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?

.NET
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?

.NET
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:

.NET
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.

Comments

Popular posts from this blog

Get eSign via Text Message Quickly with BoldSign Today

How to Embed an eSignature Solution into Your Python Application

Send eSignature Requests via WhatsApp with BoldSign