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

How to Embed an eSignature Solution into Your Python Application

Developer-Friendly eSignature Solutions with BoldSign

Get eSign via Text Message Quickly with BoldSign Today