La evolución de las Minimal APIs en 2026

🏠 Web API 📅 15 Feb 2026 ⏱️ 5 min 💬 0

Hace ya más de cuatro años que microsoft lanzó las minimal API con la idea principal de reducir el código boilerplate por el que tanto destaca C#. 

En este post, vamos a ver el estado actual y lo que podemos hacer en la actualidad con esta funcionalidad, que cada día es más popular.

 

 

1 - Las primeras implementaciones

 

Cuando todo empezó, las implementaciones no eran bonitas, todas las minimal API tenian que estar ubicadas en el program CS o hacer malabares para poder ponerlas en otros ficheros. Esto ya lo vimos en un vídeo en 2021, pero estamos ya en 2026 y las cosas han cambiando.

 

Microsoft ha ido “inspirándose” en otras librerías open source que tenían mucha popularidad como son FastAPI o Nancy para ir mejroando la implementación para hacerla mas amigable a los desarrolladores. 

 

 

1.1 - La estructura clásica de Controladores

 

Tomamos como ejemplo el código de mi libro La guía completa de full stack con .NET donde tengo una estructura core-driven utilizando controladores clásicos.

Esto quiere decir que tengo un controlador que actúa de proxy entre la llamada HTP y el caso de uso:

[Authorize]
[Route("api/[controller]")]
[ApiController]
[ProducesResponseType(typeof(ProblemDetails),StatusCodes.Status500InternalServerError)]
public class FlagsController(FlagsUseCases flagsUseCases) : ControllerBase
{
	[ProducesResponseType(typeof(ResultDto<bool>), StatusCodes.Status200OK)]
	[ProducesResponseType(typeof(ResultDto<bool>), StatusCodes.Status404NotFound)]
	[HttpGet("{flagname}")]
	public async Task<IActionResult> GetSingleFlag(string flagname)
		=> await flagsUseCases.Get
			.Execute(flagname)
			.Map(a => a.IsEnabled)
			.ToActionResult();
		
	[ProducesResponseType(typeof(ResultDto<bool>), StatusCodes.Status200OK)]
	[ProducesResponseType(typeof(ResultDto<bool>), StatusCodes.Status404NotFound)]
	[HttpDelete("{flagname}")]
	public async Task<IActionResult> Delete(string flagname)
		=> await flagsUseCases.Delete
			.Execute(flagname)
			.ToActionResult();
		
	[ProducesResponseType(typeof(Result<FlagDto>), StatusCodes.Status200OK)]
	[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
	[HttpPut("{flagName}")]
	public async Task<IActionResult> Update(UpdateFlagDto flagDto,  string flagName)
		=> await flagsUseCases.Update
			.Execute(new FlagDto()
			{
				Name = flagName,
				IsEnabled = flagDto.IsEnabled,
			})
			.ToValueOrProblemDetails();
		
	[ProducesResponseType(typeof(Result<FlagDto>), StatusCodes.Status200OK)]
	[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
	[HttpPost]
	public async Task<IActionResult> Create(FlagDto flagDto)
		=> await flagsUseCases.Add
			.Execute(flagDto.Name, flagDto.IsEnabled)
			.ToValueOrProblemDetails();
}

Esta arquitectura se apoya en dos pilares principales:

- Single Responsibility Principle (SRP): Un fichero, una acción.

- CQRS: Separación clara entre lecturas y escrituras

 

Esto hace que para hacer get por ID tengamos un endpoint que actúa de proxy a una clase con la lógica.

Para crear tenemos un endpoint que hace de proxy a un fichero con la lógica.

Lo mismo para actualizar, o para realizar las distintas acciones que el diseño de nuestra API nos permite. 

 

Además cada uno de los controladores lo tenemos decorado con una serie de atributos que pueden estar configurados tanto a nivel de controlador, o a nivel de endpoint en particular. 

Y como hemos visto en el ejemplo, los controladores al actuar únicamente como proxy, están bastante limpios.

 

 

2 - Estructurando Minimal APIs con IEndpointRouteBuilder

 

Aquí es donde entra el tipo IEndpointRouteBuilder. Mi opinión sobre las Minimal APIs cambió gracias a él. Este tipo nos permite, mediante métodos de extensión, estructurar las rutas de forma estática y organizada, eliminando ese "hack" que teníamos  las primeras versiones.

 

De esta forma el resultado es muy parecido a lo que teníamos anteriormente:

public static class FlagsApiEndpoints
{
    public static IEndpointRouteBuilder MapFlagsApiEndpoints(this IEndpointRouteBuilder endpoints)
    {
        var group = endpoints.MapGroup("/api/flags-minimal")
                .RequireAuthorization()
            ;

        group.MapGet("/{flagname}", async (string flagname, GetSingleFlagUseCase getSingleFlag)
                => await getSingleFlag
                    .Execute(flagname))
            .WithName("FlagsApiGetSingle")
            .WithSummary("Whatever")
            .Produces<ResultDto<bool>>(StatusCodes.Status200OK)
            .Produces<ResultDto<bool>>(StatusCodes.Status404NotFound);

        group.MapDelete("/{flagname}", async (string flagname, DeleteFlagUseCase deleteFlagUseCase)
                => await deleteFlagUseCase
                    .Execute(flagname))
            .WithName("deleteFlag")
            .Produces<ResultDto<bool>>(StatusCodes.Status200OK)
            .Produces<ResultDto<bool>>(StatusCodes.Status404NotFound);

        group.MapPut("/{flagName}",
                async (UpdateFlagDto flagDto, string flagname, UpdateFlagUseCase updateFlagUseCase)
                    => await updateFlagUseCase
                        .Execute(new FlagDto()
                        {
                            Name = flagname,
                            IsEnabled = flagDto.IsEnabled,
                        }))
            .WithName("updateFlag")
            .Produces<ResultDto<bool>>(StatusCodes.Status200OK)
            .Produces<ProblemDetails>(StatusCodes.Status404NotFound);

        group.MapPost("", async (FlagDto flagDto, AddFlagUseCase createFlagUseCase)
                => await createFlagUseCase
                    .Execute(flagDto.Name, flagDto.IsEnabled))
            .WithName("create flag")
            .Produces<ResultDto<bool>>(StatusCodes.Status200OK)
            .Produces<ProblemDetails>(StatusCodes.Status409Conflict);

        return endpoints;
    }
}

Si te fijas, en ambos casos tenemos los mismos requerimientos, cambia la forma de declararlos, pero todos tienen rutas, todos tienen autorización, todos tienen el tipo que responden, etc. 

 

Así que por lo que parece, el cambio no es muy grande, es prácticamente igual pero, si podemos crear esta información en un único fichero, que nos impide separarlo algo mas? 

 

 

 

2.1- El uso de Handlers en Minimal API

 

Las minimal API nos permiten utilizar un handler (un delegado o método) que separe la declaración de la ruta de la api, de la propia lógica. Esto es útil cuando la capa de API necesita realizar acciones como validar algo rápido o transformar datos antes de tocar la lógica de negocio.

public static class DeleteFlagAlmostExtreme
{
    public static RouteGroupBuilder DeleteFlagEndpoint(this RouteGroupBuilder group)
    {
        group.MapDelete("/{flagname}", HandleAsync)
            .WithName("deleteFlag")
            .Produces<ResultDto<bool>>(StatusCodes.Status200OK)
            .Produces<ResultDto<bool>>(StatusCodes.Status404NotFound);

        return group;
    }

    private static async Task<IResult> HandleAsync(string flagname, [FromServices] DeleteFlagUseCase deleteFlagUseCase)
    {
        return (IResult)await deleteFlagUseCase
            .Execute(flagname)
            .ToValueOrProblemDetails();
    }
}

Como vemos en el ejemplo, la declaración de la api está en un método mientras que la lógica de lo que sería el controlador está en otro método al que llamamos desde el propio, lo cual puede ser muy util ti tenemos minimal API y además tenemos lógica en el controlador, de esta forma queda mucho mas limpio todo.

 

 

3 - La simplificación de las API de .NET extrema con el patrón REPR

 

Como siempre las cosas se pueden llevar al extremo, y aquí es donde quiero llegar con este post, ya que podemos llevar tanto minimal API, como CQRS, como SRP, todo esto, al mismo fichero. 

Primero necesitamos una clase padre que será la encargada de invocar todo.

public static class FlagsApiEndpointsExtreme
{
    public static IEndpointRouteBuilder MapFlagsApiEndpointsExtreme(this IEndpointRouteBuilder endpoints)
    {
        return endpoints.MapGroup("/api/flags-minimal-extreme")
            .RequireAuthorization()
            .AddFlagEndpoint()
            .GetFlagEndpoint()
            .DeleteFlagEndpoint();
    }
}

 

Luego lo que vamos a hacer es crear tanto el endpoint como el caso de uso que necesitemos en un mismo fichero,  sería muy similar a la implementación del patrón REPR (Request-Endpoint-Response), que se puede hacer con otras librerías.  

public static class GetFlagExtreme
{
    public static RouteGroupBuilder GetFlagEndpoint(this RouteGroupBuilder group)
    {
        group.MapGet("/{flagname}", async (string flagname, GetSingleFlagUseCase getSingleFlag)
                => await getSingleFlag
                    .Execute(flagname))
            .WithName("FlagsApiGetSingle")
            .WithSummary("Whatever")
            .Produces<ResultDto<bool>>(StatusCodes.Status200OK)
            .Produces<ResultDto<bool>>(StatusCodes.Status404NotFound);

        return group;
    }
    
    public class GetSingleFlagUseCase(ApplicationDbContext applicationDbContext)
    {
        public async Task<Result<FlagDto>> Execute(string flagName)
            => await GetFromDb(flagName)
                .Bind(flag => flag ?? Result.NotFound<FlagEntity>("FlagDoes not exist"))
                .Map(x => x.ToDto());

        private async Task<Result<FlagEntity?>> GetFromDb(string flagname)
            => await applicationDbContext.Flags
                .Where(a => a.Name.Equals(flagname, StringComparison.InvariantCultureIgnoreCase))
                .AsNoTracking()
                .FirstOrDefaultAsync();
    }
}

 

¿Rompe esto el SRP?

Podrías pensar que sí, pero técnicamente estamos separando la definición del transporte (clase estática) de la ejecución de la lógica (instancia). Es una cohesión funcional altísima. Además, si mañana necesitas disparar esa lógica desde un Worker o un evento de bus, nada te impide mover el Handler a la capa de Aplicación manteniendo la interfaz IEndpointRouteBuilder en la capa de API.

 

Lo que ganamos es una velocidad de desarrollo y una facilidad de mantenimiento brutales: si falla el "Get User", vas al fichero GetUser.cs y ahí tienes todo (la ruta, los permisos y la lógica).


💬 Comentarios