Esta es la segunda parte de la serie Arquitectura Esencial de Web APIs en .NET 10. En la Parte 1 cubrimos los fundamentos: Health Checks, Exception Handling, Validación y OpenAPI nativo. Ahora le toca el turno a los componentes que marcan la diferencia cuando tu API empieza a recibir carga real.
Estos tres los ignoré durante demasiado tiempo, hasta que los problemas aparecieron solos en producción.
5. Rate Limiting: protegé tu API del abuso
Una API pública sin rate limiting es un blanco fácil. Me tocó ver picos de miles de requests por minuto desde una sola IP dejando el servidor de rodillas. Desde .NET 7 el middleware de rate limiting viene integrado en el framework, y en .NET 10 está completamente maduro.
.NET ofrece cuatro estrategias distintas según el caso de uso:
- Fixed Window: X peticiones por ventana de tiempo fija
- Sliding Window: igual pero la ventana se mueve (más preciso)
- Token Bucket: permite bursts controlados
- Concurrency: limita las peticiones simultáneas, no el rate
builder.Services.AddRateLimiter(options =>
{
// Política general: 100 requests por minuto por IP
options.AddFixedWindowLimiter("General", opt =>
{
opt.Window = TimeSpan.FromMinutes(1);
opt.PermitLimit = 100;
opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
opt.QueueLimit = 10; // Cuántas requests pueden esperar en cola
});
// Política estricta para endpoints sensibles
options.AddSlidingWindowLimiter("Strict", opt =>
{
opt.Window = TimeSpan.FromMinutes(1);
opt.SegmentsPerWindow = 6; // Segmentos de 10 segundos
opt.PermitLimit = 20;
});
// Respuesta personalizada cuando se supera el límite
options.OnRejected = async (context, cancellationToken) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
await context.HttpContext.Response.WriteAsJsonAsync(
new ProblemDetails { Title = "Too Many Requests", Status = 429 },
cancellationToken);
};
});
var app = builder.Build();
app.UseRateLimiter();
// Aplicar a endpoints específicos
app.MapGet("/api/data", () => "Datos")
.RequireRateLimiting("General");
app.MapPost("/api/auth/login", (LoginDto dto) => Results.Ok())
.RequireRateLimiting("Strict");
Para APIs que manejan usuarios autenticados, podés segmentar el rate limiting por user ID en lugar de IP, así los usuarios legítimos no se ven afectados por el comportamiento de otros.
6. Output Cache: no calcules lo mismo dos veces
En un proyecto de reportes que tenía consultas de 2 segundos contra SQL Server, implementar Output Caching llevó el tiempo de respuesta promedio a menos de 10ms. Sin tocar una sola línea de lógica de negocio.
Para endpoints que devuelven datos que no cambian constantemente, el Output Cache guarda la respuesta completa en memoria (o Redis) y la sirve directamente sin ejecutar nada de la lógica de negocio.
builder.Services.AddOutputCache(options =>
{
// Política base: cache de 60 segundos
options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromSeconds(60)));
// Política nombrada para datos de referencia
options.AddPolicy("ReferenceData", builder =>
builder.Expire(TimeSpan.FromMinutes(10))
.Tag("reference")); // Tag para invalidación selectiva
});
var app = builder.Build();
app.UseOutputCache();
// Cache por 10 segundos (override de la política base)
app.MapGet("/api/stats", () => GetExpensiveStats())
.CacheOutput(c => c.Expire(TimeSpan.FromSeconds(10)));
// Usando política nombrada
app.MapGet("/api/products", () => GetProducts())
.CacheOutput("ReferenceData");
// Invalidar el cache cuando cambian los datos
app.MapPost("/api/products", async (ProductDto product, IOutputCacheStore cache) =>
{
await SaveProduct(product);
await cache.EvictByTagAsync("reference", CancellationToken.None); // Invalida el cache
return Results.Created($"/api/products/{product.Id}", product);
});
La invalidación por tags es la clave para usar Output Cache sin miedo a servir datos stale. Cuando mutás datos, invalidás el tag correspondiente y la próxima request regenera el cache.
Para ambientes distribuidos (múltiples instancias), podés configurar Redis como backing store con AddStackExchangeRedisOutputCache().
7. API Versioning: cambiar sin romper a tus clientes
Esta es una de las lecciones más caras que aprendí: si tu API tiene clientes externos y no versiona desde el principio, el día que necesités hacer un cambio incompatible vas a tener un problema enorme. Agregarle versionado a una API ya desplegada es doloroso. Hacerlo desde el inicio es trivial.
En Minimal APIs el versionado se maneja mediante Version Sets. Primero instalá el paquete Asp.Versioning.Http:
dotnet add package Asp.Versioning.Http
// Program.cs
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true; // Devuelve las versiones disponibles en headers
});
var app = builder.Build();
var apiVersionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(1, 0))
.HasApiVersion(new ApiVersion(2, 0))
.ReportApiVersions()
.Build();
// V1: respuesta simple
app.MapGet("/api/v{version:apiVersion}/users", () =>
new[] { new { Id = 1, Name = "Juan" } })
.WithApiVersionSet(apiVersionSet)
.MapToApiVersion(1, 0);
// V2: respuesta enriquecida con datos extra
app.MapGet("/api/v{version:apiVersion}/users", () =>
new[] { new { Id = 1, Name = "Juan", Email = "juan@ejemplo.com", Role = "Admin" } })
.WithApiVersionSet(apiVersionSet)
.MapToApiVersion(2, 0);
Los clientes existentes siguen usando /api/v1/users sin ningún cambio. Los nuevos pueden adoptar /api/v2/users con el formato enriquecido. Convivencia perfecta.
Además del versionado por URL, podés usar query string (?api-version=2.0) o header (X-API-Version: 2.0) según las necesidades de tus clientes.
Resumen de la Parte 2
Tres componentes que hacen la diferencia cuando la API escala:
- ✅ Rate Limiting → protección contra abuso, con estrategias flexibles por caso de uso
- ✅ Output Cache → performance brutal en endpoints de lectura, con invalidación inteligente
- ✅ API Versioning → libertad para evolucionar sin romper contratos existentes
En la Parte 3 cerramos la serie con los componentes más avanzados: OpenTelemetry, Polly, Feature Flags y Server-Sent Events.
Deja una respuesta