Why We Built Our Backend on .NET Minimal API for a High-Load Platform
We migrated an agricultural certification platform from traditional MVC controllers to .NET 10 Minimal API. Here is what we measured, what surprised us, and where traditional controllers are still the right answer.
The Context
The platform processes certification requests from field auditors who work in areas with poor connectivity. This means offline-first mobile clients that batch operations and send them in bursts when connectivity is restored. The backend needs to handle uneven load spikes rather than consistent throughput.
Our initial architecture used ASP.NET Core MVC controllers. It worked. But as the feature set grew, we started feeling the overhead in three areas: cold start time on serverless functions, request pipeline latency on low-traffic endpoints, and the sheer amount of scaffolding needed for simple CRUD operations.
What .NET Minimal API Actually Changes
Minimal API (introduced in .NET 6, significantly improved in .NET 10) is not just syntactic sugar. It reduces the middleware pipeline stages that every request passes through when you do not need them.
In a traditional controller-based app, every request goes through the full pipeline including model binding, controller activation, action filters, and result execution. Most of these stages do nothing for simple endpoints, but they still execute.
Minimal API lets you define endpoints as delegates. The pipeline is shorter by default.
// Traditional controller
[ApiController]
[Route("api/certifications")]
public class CertificationController : ControllerBase
{
private readonly ICertificationService _service;
public CertificationController(ICertificationService service)
{
_service = service;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(string id, CancellationToken ct)
{
var result = await _service.GetByIdAsync(id, ct);
if (result is null) return NotFound();
return Ok(result);
}
}
// .NET 10 Minimal API equivalent
app.MapGet("/api/certifications/{id}", async (
string id,
ICertificationService service,
CancellationToken ct) =>
{
var result = await service.GetByIdAsync(id, ct);
return result is null ? Results.NotFound() : Results.Ok(result);
})
.RequireAuthorization("Auditor")
.WithName("GetCertificationById")
.Produces<CertificationDto>()
.ProducesProblem(404);The endpoint definition is more compact. Authorization, response type documentation, and naming are all inline.
What We Measured
We benchmarked both approaches on our staging environment with realistic request payloads.
Cold start (Azure Functions, Consumption plan):
- MVC controllers: 820ms average cold start
- Minimal API: 510ms average cold start
- Improvement: ~38%
Requests per second (8-core VM, simple GET endpoint):
- MVC controllers: 42,000 RPS
- Minimal API: 58,000 RPS
- Improvement: ~38%
Memory allocation per request (simple serialization endpoint):
- MVC controllers: 2.1KB
- Minimal API: 1.4KB
These numbers are consistent with Microsoft's own benchmarks. The improvement comes from fewer middleware stages, direct source generation for serialization (System.Text.Json source generators), and less reflection overhead.
For a platform processing batch sync from hundreds of offline devices, the cold start improvement mattered. Field auditors sync when they get connectivity — there is no warm baseline.
The Architecture We Landed On
We organize Minimal API endpoints using extension methods on IEndpointRouteBuilder. This replaces the controller-per-feature pattern with a feature-per-module pattern.
// CertificationModule.cs
public static class CertificationModule
{
public static IEndpointRouteBuilder MapCertificationEndpoints(
this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/api/certifications")
.RequireAuthorization();
group.MapGet("/{id}", GetById);
group.MapPost("/", Submit);
group.MapPost("/batch", SubmitBatch);
group.MapPatch("/{id}/status", UpdateStatus);
return routes;
}
private static async Task<IResult> GetById(
string id,
ICertificationService service,
CancellationToken ct)
{
var result = await service.GetByIdAsync(id, ct);
return result is null ? Results.NotFound() : Results.Ok(result);
}
private static async Task<IResult> Submit(
CertificationSubmitRequest request,
ICertificationService service,
IValidator<CertificationSubmitRequest> validator,
CancellationToken ct)
{
var validation = await validator.ValidateAsync(request, ct);
if (!validation.IsValid)
return Results.ValidationProblem(validation.ToDictionary());
var id = await service.SubmitAsync(request, ct);
return Results.Created($"/api/certifications/{id}", new { id });
}
}Each module is registered in Program.cs:
app.MapCertificationEndpoints();
app.MapAuditorEndpoints();
app.MapAdminEndpoints();This keeps Program.cs clean and makes the routing structure discoverable.
The project file targets net10.0:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>Validation Without Filters
One thing you lose with Minimal API is automatic model validation via [ApiController] and validation attributes. We use FluentValidation injected directly into endpoint handlers.
For consistency, we wrote a minimal extension:
public static class ValidationExtensions
{
public static async Task<IResult?> ValidateAsync<T>(
this IValidator<T> validator,
T request,
CancellationToken ct = default)
{
var result = await validator.ValidateAsync(request, ct);
return result.IsValid
? null
: Results.ValidationProblem(result.ToDictionary());
}
}
// Usage in endpoint
private static async Task<IResult> Submit(
CertificationSubmitRequest request,
IValidator<CertificationSubmitRequest> validator,
ICertificationService service,
CancellationToken ct)
{
var validationResult = await validator.ValidateAsync(request, ct);
if (validationResult is not null) return validationResult;
var id = await service.SubmitAsync(request, ct);
return Results.Created($"/api/certifications/{id}", new { id });
}Where We Still Use Controllers
We do not use Minimal API everywhere.
Long controller actions with complex filter logic. If an endpoint uses multiple action filters, result filters, and exception filters that are reused across many endpoints, the controller model is cleaner. Filters on Minimal API endpoints require endpoint filters, which are less ergonomic for complex chains.
Admin panels with heavy scaffolding. If you need RazorPages or server-rendered views, controllers are still the natural fit.
Third-party libraries that assume MVC. Some reporting or export libraries build on IActionResult and ControllerBase. Wrapping them for Minimal API is not worth the effort.
The rule we apply: new feature endpoints use Minimal API by default. Legacy code stays on controllers. We migrate controllers when we touch them for other reasons — not as a separate refactor task.
The Serialization Win
One underappreciated benefit of .NET Minimal API is that Results.Ok(value) with source-generated serialization is measurably faster than Ok(value) in a controller with runtime reflection.
Enable source generation in your JsonSerializerContext:
[JsonSerializable(typeof(CertificationDto))]
[JsonSerializable(typeof(List<CertificationDto>))]
[JsonSerializable(typeof(ProblemDetails))]
internal partial class AppJsonContext : JsonSerializerContext { }Register it:
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
});In our benchmarks, source-generated serialization cut JSON serialization overhead by approximately 40% on complex response objects. On a platform handling thousands of batch sync requests, this is not a micro-optimization.
Conclusion
.NET Minimal API is production-ready and delivers measurable performance improvements over MVC controllers for request-intensive endpoints. The 38% throughput improvement and lower cold start times were worth the migration cost for our platform.
The tradeoffs are real: you lose some ergonomics around filters, model binding annotations, and scaffolding. For greenfield APIs — especially SaaS platform backends where cold start latency matters — these tradeoffs are almost always worth accepting. For existing MVC codebases, migrate incrementally at the endpoint level. Do not treat it as an all-or-nothing rewrite.
If you are building a new .NET backend from scratch and want to start right, our .NET backend development team defaults to Minimal API on every new project. The best time to start is on new endpoints added to an existing system — you get the benefits immediately without disrupting working code.
If you are already using Minimal API and want to add AI capabilities on top, see our guide on integrating OpenAI API into a .NET 10 web app.