Por Qué Construimos Nuestro Backend en .NET Minimal API para una Plataforma de Alta Carga
Migramos una plataforma de certificación agrícola desde controladores MVC tradicionales a .NET 10 Minimal API. Aquí está lo que medimos, lo que nos sorprendió y dónde los controladores tradicionales siguen siendo la respuesta correcta.
El Contexto
La plataforma procesa solicitudes de certificación de auditores de campo que trabajan en zonas con mala conectividad. Esto significa clientes móviles offline-first que acumulan operaciones y las envían en ráfagas cuando se restaura la conexión. El backend necesita manejar picos de carga irregulares en lugar de un throughput constante.
Nuestra arquitectura inicial usaba controladores ASP.NET Core MVC. Funcionaba. Pero a medida que crecía el conjunto de features, empezamos a sentir la sobrecarga en tres áreas: tiempo de arranque en frío en funciones serverless, latencia del pipeline de peticiones en endpoints de poco tráfico, y la enorme cantidad de scaffolding necesario para operaciones CRUD simples.
Qué Cambia Realmente .NET Minimal API
Minimal API (introducida en .NET 6, significativamente mejorada en .NET 10) no es solo azúcar sintáctico. Reduce las etapas del pipeline de middleware que cada petición recorre cuando no las necesitas.
En una app tradicional basada en controladores, cada petición pasa por el pipeline completo incluyendo model binding, activación del controlador, action filters y ejecución del resultado. La mayoría de estas etapas no hacen nada para endpoints simples, pero igualmente se ejecutan.
Minimal API te permite definir endpoints como delegates. El pipeline es más corto por defecto.
// Controlador tradicional
[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);
}
}
// Equivalente .NET 10 Minimal API
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);La definición del endpoint es más compacta. La autorización, la documentación del tipo de respuesta y el nombre están todos inline.
Lo que Medimos
Hicimos benchmarks de ambos enfoques en nuestro entorno de staging con payloads realistas.
Arranque en frío (Azure Functions, plan Consumption):
- Controladores MVC: 820ms promedio de arranque en frío
- Minimal API: 510ms promedio de arranque en frío
- Mejora: ~38%
Peticiones por segundo (VM de 8 núcleos, endpoint GET simple):
- Controladores MVC: 42.000 RPS
- Minimal API: 58.000 RPS
- Mejora: ~38%
Asignación de memoria por petición (endpoint de serialización simple):
- Controladores MVC: 2,1 KB
- Minimal API: 1,4 KB
Estos números son consistentes con los benchmarks propios de Microsoft. La mejora proviene de menos etapas de middleware, generación directa de código para serialización (source generators de System.Text.Json) y menos overhead por reflexión.
Para una plataforma que procesa sincronización por lotes de cientos de dispositivos offline, la mejora en arranque en frío fue relevante. Los auditores de campo sincronizan cuando obtienen conectividad — no hay una baseline caliente.
La Arquitectura que Adoptamos
Organizamos los endpoints de Minimal API usando extension methods en IEndpointRouteBuilder. Esto reemplaza el patrón de un controlador por feature con un patrón de módulo por feature.
// 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);
}
}Dónde Seguimos Usando Controladores
No usamos Minimal API en todos lados.
Acciones de controlador largas con lógica de filtros compleja. Si un endpoint usa múltiples action filters, result filters y exception filters reutilizados en muchos endpoints, el modelo de controlador es más limpio.
Paneles de administración con mucho scaffolding. Si necesitas RazorPages o vistas server-rendered, los controladores siguen siendo la opción natural.
La regla que aplicamos: los nuevos endpoints de features usan Minimal API por defecto. El código legacy permanece en controladores. Migramos controladores cuando los tocamos por otras razones — no como una tarea de refactoring separada.
La Ventaja de la Serialización
Un beneficio poco apreciado de .NET Minimal API es que Results.Ok(value) con serialización generada por código es mediblemente más rápido que Ok(value) en un controlador con reflexión en runtime.
Activa la generación de código en tu JsonSerializerContext:
[JsonSerializable(typeof(CertificationDto))]
[JsonSerializable(typeof(List<CertificationDto>))]
[JsonSerializable(typeof(ProblemDetails))]
internal partial class AppJsonContext : JsonSerializerContext { }En nuestros benchmarks, la serialización generada por código redujo el overhead de serialización JSON en aproximadamente un 40% en objetos de respuesta complejos. En una plataforma que maneja miles de peticiones de sincronización por lotes, esto no es una micro-optimización.
Conclusión
.NET Minimal API está listo para producción y ofrece mejoras de rendimiento medibles sobre los controladores MVC para endpoints con mucho tráfico. La mejora del 38% en throughput y los menores tiempos de arranque en frío valieron el costo de migración para nuestra plataforma.
Si estás construyendo un nuevo backend .NET desde cero y quieres empezar bien, nuestro equipo de desarrollo de backend .NET elige Minimal API por defecto en cada proyecto nuevo. El mejor momento para empezar es en los nuevos endpoints que se añaden a un sistema existente — obtienes los beneficios de inmediato sin romper el código funcional.