Cuando empecé a trabajar con .NET, mi idea de una «API lista para producción» era que compilara y los endpoints respondieran correctamente. Con el tiempo aprendí que hay una diferencia enorme entre una API que funciona en tu máquina y una que sobrevive el mundo real.
Esta es la primera parte de una serie de tres artículos donde te cuento los 11 componentes que hoy considero esenciales en cualquier Web API con .NET 10. Empezamos por los fundamentos: los cuatro que deberías tener desde el primer día, sin importar el tamaño del proyecto.
1. Health Checks: que el orquestador sepa si tu API está viva
La primera vez que deployé en Kubernetes sin health checks fue un desastre silencioso. El pod figuraba como «Running», pero la base de datos no conectaba y las requests simplemente fallaban sin que el orquestador se enterara.
Los Health Checks exponen endpoints que le dicen a Kubernetes (o a cualquier balanceador de carga) dos cosas fundamentales:
- Liveness: ¿el proceso sigue vivo?
- Readiness: ¿está listo para recibir tráfico? (sus dependencias —base de datos, Redis, etc.— están funcionando)
// Program.cs
builder.Services.AddHealthChecks()
.AddNpgSql(builder.Configuration.GetConnectionString("Default")); // Verifica PostgreSQL
var app = builder.Build();
app.MapHealthChecks("/health"); // Endpoint general
app.MapHealthChecks("/health/ready", // Solo readiness
new HealthCheckOptions { Predicate = check => check.Tags.Contains("ready") });
app.MapHealthChecks("/health/live", // Solo liveness
new HealthCheckOptions { Predicate = _ => false });
Con el paquete AspNetCore.HealthChecks.* podés agregar chequeos para SQL Server, Redis, servicios HTTP externos y más. Una línea por dependencia crítica.
2. Exception Handling Global con IExceptionHandler
Nada peor que una API que le devuelve un stack trace de C# al cliente, o un error 500 genérico sin información útil. Durante mucho tiempo usé try/catch en cada endpoint, hasta que descubrí IExceptionHandler.
Con esta interfaz centralizás el manejo de errores en un solo lugar y respondés siempre con el formato estándar ProblemDetails (RFC 7807), que es lo que los clientes modernos esperan.
public class GlobalExceptionHandler : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
_logger = logger;
}
public async ValueTask<bool> TryHandleAsync(
HttpContext context, Exception exception, CancellationToken cancellationToken)
{
_logger.LogError(exception, "Error no controlado: {Message}", exception.Message);
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "Ocurrió un error inesperado",
Detail = exception.Message
};
context.Response.StatusCode = problemDetails.Status.Value;
await context.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
return true; // Excepción manejada, detiene la cadena
}
}
// Program.cs
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler();
Podés crear múltiples handlers y registrarlos en orden: el primero que devuelva true corta la cadena. Útil para manejar ValidationException, NotFoundException, etc., con respuestas específicas.
3. Validación Nativa con Endpoint Filters
Validar el payload antes de que llegue a la lógica de negocio es crítico. En .NET 10, las Minimal APIs tienen Endpoint Filters que actúan como un pipeline de validación declarativo, sin necesidad de librerías externas para los casos más comunes.
app.MapPost("/api/users", (UserDto user) =>
{
return Results.Created($"/api/users/{user.Id}", user);
})
.AddEndpointFilter(async (context, next) =>
{
var user = context.GetArgument<UserDto>(0);
var errors = new Dictionary<string, string[]>();
if (string.IsNullOrEmpty(user.Name))
errors["Name"] = new[] { "El nombre es obligatorio" };
if (user.Age < 0 || user.Age > 120)
errors["Age"] = new[] { "La edad debe estar entre 0 y 120" };
if (errors.Any())
return Results.ValidationProblem(errors);
return await next(context);
});
Para proyectos más grandes donde necesitás validaciones complejas, FluentValidation sigue siendo una excelente opción que se integra perfectamente con este patrón.
4. OpenAPI Integrado: adiós Swashbuckle
Durante años, generar documentación Swagger significaba instalar Swashbuckle y rezar para que no conflictuara con otras dependencias. En .NET 10, la generación de especificación OpenAPI es nativa, sin dependencias externas, con mejor performance de arranque y mantenimiento garantizado por Microsoft.
builder.Services.AddOpenApi(); // Generación nativa
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi(); // Expone la spec en /openapi/v1.json
// Opción A: Scalar (UI moderna, recomendada)
app.MapScalarApiReference();
// Opción B: Swagger UI clásico
// app.UseSwaggerUI(c => c.SwaggerEndpoint("/openapi/v1.json", "API v1"));
}
Para la UI de documentación, te recomiendo probar Scalar: tiene mucho mejor UX que Swagger UI clásico y es trivial de integrar.
Resumen de la Parte 1
Estos cuatro componentes son los que agrego en cualquier proyecto nuevo desde el día uno:
- ✅ Health Checks → para que Kubernetes y el balanceador sepan el estado real de tu API
- ✅ Exception Handler global → respuestas de error consistentes y sin leakear internos
- ✅ Validación con Endpoint Filters → datos limpios antes de tocar la lógica de negocio
- ✅ OpenAPI nativo → documentación siempre actualizada sin overhead
En la Parte 2 vemos los tres componentes que marcan la diferencia cuando la API empieza a escalar: Rate Limiting, Output Cache y API Versioning.
Deja una respuesta