Minimal APIs en ASP.NET Core en 2026: APIs REST sin controladores ni magia

Las Minimal APIs aparecieron en .NET 6 con una idea muy concreta: definir endpoints REST directamente en Program.cs, sin controladores, sin clases de Startup y sin atributos de routing. En .NET 9 han madurado lo suficiente como para que puedas construir con ellas APIs completas de producción.

Tienen sentido cuando el proyecto es pequeño o mediano, cuando montas un microservicio que hace tres cosas concretas, o cuando necesitas una función serverless que responda a un webhook. Un controlador con cinco métodos y cero lógica de negocio es código que sobra; las Minimal APIs eliminan exactamente eso.

Ahora bien, los controladores MVC siguen siendo la mejor opción en proyectos grandes con varios equipos, routing complejo o mucha lógica de filtros. No es que uno reemplace al otro: se pueden mezclar en el mismo proyecto sin problema.

La estructura básica

Este es el programa más corto posible que devuelve una lista de usuarios:

var app = WebApplication.Create();

app.MapGet("/users", () => new[] { "Ana", "Pedro" });

app.Run();

Sin clases, sin atributos [Route], sin inyección de constructores. El tipo de retorno se serializa a JSON de forma automática.

Para respuestas HTTP más explícitas tienes Results.Ok(data), Results.NotFound() y Results.BadRequest(mensaje):

app.MapGet("/users/{id:int}", (int id) =>
{
    var user = FindUser(id);
    return user is null ? Results.NotFound() : Results.Ok(user);
});

Parámetros: inyección automática

ASP.NET Core deduce de dónde viene cada parámetro según su tipo y nombre. No tienes que decorar nada con [FromBody] o [FromQuery] la mayor parte del tiempo.

  • Del body: app.MapPost("/users", (CreateUserRequest req) => { ... }) — se deserializa automáticamente desde el JSON de la petición.
  • De la ruta: app.MapGet("/users/{id:int}", (int id) => { ... }) — el nombre tiene que coincidir con el segmento.
  • De la query: app.MapGet("/search", (string? q, int page = 1) => { ... }) — parámetros opcionales con valor por defecto.
  • Del contenedor de DI: app.MapGet("/users", (IUserService svc) => svc.GetAll()) — si el tipo está registrado en el contenedor, se inyecta sin más.

Esa última opción es especialmente útil: puedes mantener la lógica en servicios perfectamente testeables e inyectarlos directamente en el lambda.

Organizar endpoints con RouteGroupBuilder

Cuando tienes varios endpoints relacionados, MapGroup te permite aplicar un prefijo de ruta y middlewares comunes a todos de golpe:

var usersGroup = app.MapGroup("/api/users").RequireAuthorization();

usersGroup.MapGet("/", GetAllUsers);
usersGroup.MapGet("/{id:int}", GetUserById);
usersGroup.MapPost("/", CreateUser);
usersGroup.MapDelete("/{id:int}", DeleteUser);

El prefijo /api/users y el middleware de autorización se aplican a los cuatro endpoints. Si luego necesitas cambiar el prefijo, lo cambias en un sitio.

Para proyectos más grandes puedes mover los handlers a clases estáticas o métodos de extensión y mantener Program.cs limpio:

// UserEndpoints.cs
public static class UserEndpoints
{
    public static void Map(WebApplication app)
    {
        var group = app.MapGroup("/api/users").RequireAuthorization();
        group.MapGet("/", GetAll);
        group.MapGet("/{id:int}", GetById);
    }

    static async Task<IResult> GetAll(IUserService svc) =>
        Results.Ok(await svc.GetAllAsync());

    static async Task<IResult> GetById(int id, IUserService svc)
    {
        var user = await svc.GetByIdAsync(id);
        return user is null ? Results.NotFound() : Results.Ok(user);
    }
}

Validación con Data Annotations y FluentValidation

El atributo [AsParameters] junto con un record te permite validar los parámetros del body con Data Annotations:

record CreateUserRequest(
    [Required] string Name,
    [EmailAddress] string Email,
    [Range(18, 120)] int Age
);

app.MapPost("/users", ([AsParameters] CreateUserRequest req) => { ... })
   .AddEndpointFilter<ValidationFilter>();

Si prefieres FluentValidation, se integra como middleware: valida el body antes de que llegue al handler y devuelve 400 automáticamente si algo no cuadra:

app.MapPost("/users", async (
    [FromBody] CreateUserRequest req,
    IValidator<CreateUserRequest> validator) =>
{
    var result = await validator.ValidateAsync(req);
    if (!result.IsValid)
        return Results.ValidationProblem(result.ToDictionary());

    // lógica aquí
    return Results.Created($"/users/{newId}", req);
});

IResult y TypedResults: respuestas tipadas

La diferencia entre Results.Ok(user) y TypedResults.Ok(user) es el tipo genérico: TypedResults.Ok<User>(user) hace que el tipo aparezca en la documentación OpenAPI sin que tengas que añadir ningún atributo.

Puedes además declarar todos los tipos de respuesta posibles en la firma del método:

app.MapGet("/users/{id:int}", async Task<Results<Ok<User>, NotFound>>
    (int id, IUserService svc) =>
{
    var user = await svc.GetByIdAsync(id);
    return user is null
        ? TypedResults.NotFound()
        : TypedResults.Ok(user);
});

Con esto la documentación OpenAPI refleja exactamente los códigos 200 y 404 que puede devolver el endpoint, sin [ProducesResponseType] por ningún lado.

OpenAPI integrado en .NET 9

.NET 9 incluye soporte OpenAPI nativo, sin necesidad de Swashbuckle ni NSwag para generar la especificación:

// Program.cs
builder.Services.AddOpenApi();
// ...
app.MapOpenApi(); // expone /openapi/v1.json

Puedes añadir metadatos a cada endpoint con una API fluida:

app.MapGet("/users/{id:int}", GetUserById)
   .WithName("GetUser")
   .WithTags("Users")
   .WithDescription("Obtiene un usuario por su ID")
   .WithSummary("Buscar usuario");

Swagger UI sigue siendo un paquete separado (Scalar.AspNetCore o Swashbuckle.AspNetCore.SwaggerUI), pero el JSON de la spec lo genera .NET sin depender de nada externo.

Minimal APIs vs controladores en 2026

En los benchmarks de TechEmpower, ASP.NET Core con Minimal APIs aparece consistentemente en el top 5 de los frameworks más rápidos del mundo. La diferencia respecto a los controladores es pequeña en la mayoría de las aplicaciones, pero existe: menos reflection en cada petición, menos middleware por defecto.

La pregunta práctica no es cuál es más rápido, sino cuál encaja mejor con el proyecto:

  • Proyecto nuevo pequeño o microservicio: Minimal APIs, sin dudarlo.
  • API grande con equipo y routing complejo: los controladores MVC siguen siendo perfectamente válidos y más fáciles de organizar a gran escala.
  • Proyecto existente: puedes añadir Minimal APIs para endpoints nuevos sin tocar los controladores que ya tienes. Conviven sin problema en el mismo proyecto.

Si quieres profundizar en la base sobre la que se asientan las Minimal APIs, echa un vistazo a ASP.NET Core: de los controladores a las Minimal APIs. Y si te interesa el lenguaje en sí, en C# y el desarrollo web moderno con .NET tienes el contexto completo.

Imagen: Pexels / Bibek ghosh

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP