Email copiado — support@tuurt.com
Cargando experiencia
dotnet · 19 de agosto de 2026 · 6 min

Minimal APIs en .NET 8: cuándo reemplazan a los controllers y cuándo no

Minimal APIs quita ceremonia, pero no organiza el proyecto por ti. Repasamos como agrupar endpoints, donde van los filtros de validacion y el punto en el que conviene volver a controllers.

Por Equipo Tuurt

Minimal APIs en .NET 8: cuándo reemplazan a los controllers y cuándo no

Cada vez que empezamos un servicio nuevo en .NET, alguien en el equipo pregunta si esta vez toca usar Minimal APIs en lugar de controllers con MVC. La pregunta suele venir cargada de una expectativa: menos archivos, menos ceremonia, arranque más simple. Todo eso es cierto en la superficie. Lo que no es cierto es que Minimal APIs sea siempre la opción correcta solo porque es la más nueva. Es la opción correcta para un tipo de proyecto, y la equivocada para otro, y confundir ambos casos es lo que produce esas migraciones a medio camino que terminan con un Program.cs de mil líneas.

Qué resuelven bien

Minimal APIs elimina la distancia entre "quiero un endpoint" y "tengo un endpoint". No hace falta una clase controller, no hace falta heredar de ControllerBase, no hace falta decorar con atributos de ruta si el mapeo ya está expresado en la llamada:

app.MapGet("/orders/{id}", async (int id, IOrderRepository repo) =>
{
    var order = await repo.FindAsync(id);
    return order is not null ? Results.Ok(order) : Results.NotFound();
});

La inyección de dependencias llega por parámetro, sin constructor, sin campo privado, sin readonly. Para un servicio pequeño con veinte o treinta endpoints, esto no es solo menos código: es menos indirección para leer. Alguien nuevo en el equipo entiende qué hace este endpoint sin saltar a otro archivo a ver el constructor del controller.

También resuelve bien el caso de los servicios que existen para exponer una sola cosa: un BFF (backend-for-frontend) para una pantalla concreta, un webhook receiver, un microservicio de traducción de eventos. Ahí la ceremonia de MVC —convenciones de routing, filtros globales, model binding complejo— es peso que el proyecto no necesita cargar.

Organización en proyectos medianos

El problema empieza cuando el proyecto crece más allá de ese primer archivo. La tentación es seguir añadiendo app.MapGet y app.MapPost en Program.cs, y a los tres meses ese archivo tiene doscientas líneas de rutas mezcladas con configuración de middleware, políticas de CORS y registro de servicios. No es un problema de Minimal APIs en sí, es un problema de disciplina que MVC impone por convención —cada controller es su propio archivo— y que Minimal APIs no impone en absoluto.

La forma que nos funciona es tratar cada grupo de endpoints como su propio módulo, usando MapGroup para el prefijo compartido y métodos de extensión para aislar el registro:

public static class OrderEndpoints
{
    public static RouteGroupBuilder MapOrderEndpoints(this RouteGroupBuilder group)
    {
        group.MapGet("/{id}", GetOrder);
        group.MapPost("/", CreateOrder);
        group.MapPut("/{id}/cancel", CancelOrder);
        return group;
    }

    private static async Task<IResult> GetOrder(int id, IOrderRepository repo)
    {
        var order = await repo.FindAsync(id);
        return order is not null ? TypedResults.Ok(order) : TypedResults.NotFound();
    }
}

Y en Program.cs queda solo la línea de registro:

app.MapGroup("/orders")
   .MapOrderEndpoints()
   .RequireAuthorization()
   .WithTags("Orders");

Esto recupera buena parte de la organización que MVC da gratis, pero exige que el equipo la mantenga a propósito. Si nadie establece esa convención desde el primer sprint, el proyecto migra sin darse cuenta hacia el Program.cs monolítico, y revertir eso más adelante cuesta más que haberlo evitado desde el principio.

Filtros y validación

Aquí es donde la diferencia con MVC se nota más en el día a día. MVC tiene action filters (IActionFilter, IAsyncActionFilter) con un pipeline maduro y atributos declarativos como [ValidateAntiForgeryToken] o [Authorize(Roles = "...")] que cualquiera reconoce de inmediato. Minimal APIs tiene IEndpointFilter, que cubre el mismo caso de uso pero con menos convenciones ya construidas:

public class ValidationFilter<T> : IEndpointFilter where T : class
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var arg = context.Arguments.OfType<T>().FirstOrDefault();
        var results = new List<ValidationResult>();
        if (arg is not null && !Validator.TryValidateObject(arg, new ValidationContext(arg), results, true))
        {
            return Results.ValidationProblem(results.ToDictionary(
                r => r.MemberNames.FirstOrDefault() ?? string.Empty,
                r => new[] { r.ErrorMessage ?? string.Empty }));
        }
        return await next(context);
    }
}

Funciona, y una vez escrito se reutiliza igual que un action filter de MVC. El costo real no está en escribirlo una vez, está en que cada equipo lo escribe distinto porque no hay una convención estándar del framework para validación de modelo como la que trae [ApiController] en MVC —ese atributo activa validación automática de model state y devuelve 400 sin que nadie escriba una línea de más—. En Minimal APIs, ese comportamiento hay que construirlo y mantenerlo como código propio del proyecto.

Testing de integración

En testing, la diferencia es menor de lo que parece. WebApplicationFactory<TEntryPoint> funciona igual para ambos modelos, porque en ambos casos se está probando el pipeline HTTP completo, no la clase controller ni el delegate del endpoint por separado:

public class OrderEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public OrderEndpointsTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetOrder_ReturnsNotFound_WhenOrderDoesNotExist()
    {
        var response = await _client.GetAsync("/orders/999");
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
    }
}

La única fricción real aparece en tests unitarios que quieren aislar la lógica de un endpoint sin levantar el pipeline completo. Con un controller, se instancia la clase directamente y se llama al método. Con Minimal APIs, el delegate suele estar definido como método estático o lambda dentro del archivo de registro, así que para probarlo de forma aislada conviene extraerlo a un método público y testeable, como hicimos arriba con GetOrder. Si el delegate se deja como lambda anónima dentro de MapGet, queda atado al pipeline y solo se puede probar de forma integrada.

Dónde deja de escalar

El punto donde recomendamos volver a controllers no es un número mágico de endpoints, es la aparición de necesidades que MVC ya resolvió y que Minimal APIs obliga a resolver de nuevo: versionado de API con convenciones establecidas, documentación OpenAPI agrupada por área funcional con metadata rica, filtros de autorización compuestos con lógica condicional, o un equipo grande donde la consistencia entre endpoints importa más que la velocidad de escribir uno nuevo.

También pesa el factor humano: un equipo que ya conoce MVC a fondo, con convenciones internas de años, paga un costo de productividad real al migrar a Minimal APIs aunque el proyecto en sí sea pequeño. Ese costo no aparece en ningún benchmark de framework, aparece en cuánto tarda alguien del equipo en ubicar dónde vive la lógica de un endpoint la primera vez que toca ese código.

Lo que hemos dejado de hacer es tratarlo como una decisión de arquitectura global para toda la empresa. Es una decisión por servicio. Un servicio pequeño, de vida corta o con pocos endpoints gana con Minimal APIs. Una API con superficie amplia, múltiples versiones activas y un equipo grande que ya domina MVC gana quedándose en controllers. Mezclar los dos criterios —elegir por moda en vez de por forma del proyecto— es lo que produce el peor resultado de los dos: la ceremonia de MVC sin sus convenciones, o la simplicidad de Minimal APIs sin la disciplina que exige para no perderla.

dotnet api arquitectura backend
← Volver al blog