Hola genios, en el desarrollo moderno de .NET es decir de la versión , especialmente con Minimal APIs (que salieron en .NET 6), uno de los cambios más importantes es cómo definimos lo que un endpoint puede retornar.
Durante años, IActionResult fue la respuesta universal, pero tenía un problema: todo era demasiado dinámico y hasta ambiguo.
Los Result Contracts (Typed Results) llegan a resolver eso.
Son una manera explícita, predecible y más segura de declarar las respuestas de un endpoint.
El problema con IActionResult
Un endpoint típico luce masomenos así:
public IActionResult Get(int id)
{
if (id <= 0) return BadRequest();
var item = _repo.Find(id);
if (item == null) return NotFound();
return Ok(item);
}
Se ve bien, pero:
- Para el compilador, retorna cualquier cosa.
- Swagger documenta lo mejor que puede, pero sin garantías.
- Los clientes generados (TypeScript, C#, Java) adivinan los tipos.
- Las pruebas unitarias ven todo como IActionResult, no como tipos reales.
- No existe un contrato claro: el endpoint ofrece un “lo que salga”.
Esto escala mal cuando trabajas con múltiples equipos, módulos o microservicios.
¿Qué son los Result Contracts?
Los Result Contracts definen de manera fuerte y explícita:
- qué códigos de estado puede devolver un endpoint
- qué tipo exacto acompaña a cada código
- qué resultado final entiende el compilador
Así:
Task<Results<Ok<ProductDto>, NotFound<ErrorDto>>>
Esto significa literalmente:
Este endpoint solo puede devolver 200 con un
ProductDtoo 404 con unErrorDto.
Nada más. No hay ambigüedad ni sorpresas.
Ejemplo moderno con Minimal API
app.MapGet("/products/{id}",
async Task<Results<
Ok<ProductDto>,
NotFound<ErrorDto>,
UnauthorizedHttpResult
>> (int id, IProductService service) =>
{
if (!UserIsAuthorized())
return TypedResults.Unauthorized();
var product = await service.GetByIdAsync(id);
if (product is null)
return TypedResults.NotFound(new ErrorDto("Producto no encontrado"));
return TypedResults.Ok(product);
});
Este endpoint solo puede devolver:
| Código de respuesta | Tipo | Caso |
|---|---|---|
| 200 | ProductDto | Producto encontrado |
| 404 | ErrorDto | Producto no existe |
| 401 | UnauthorizedHttpResult | Autenticación inválida |
Entonce como notarás dev, es un contrato explícito entre la API y sus consumidores.
Y qué pasa con los errores 500?
Puedes devolver un 500 manualmente:
return TypedResults.StatusCode(500);
Pero en APIs modernas, el 500 lo maneja el Exception Handler.
Es mejor centralizar:
app.UseExceptionHandler(handler =>
{
handler.Run(async context =>
{
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(
new ErrorDto("Error inesperado")
);
});
});
Así no tienes 500 dispersos por toda la API.
Puedo devolver 401, 403, 422?
Sí, con resultados tipados:
return TypedResults.Unauthorized(); // 401
return TypedResults.Forbid(); // 403
return TypedResults.UnprocessableEntity(); // 422
Hay typed results para casi todos los códigos usados en APIs reales, así que no te preocupes 😜.
Sigue existiendo IActionResult?
Por supuesto que sigue existiendo y tiene sus ventajas, sin embargo también sus desventajas como:
- No ofrece seguridad estática
- No comunica tan bien los contratos
- Si usas minimal APIs, puede que no se integre tan bien
- Podría limitar la calidad del OpenAPI generado en cuestión de tipos de retorno
Y qué hay del ActionResult?
ActionResult<T> sí fue un avance, pero no resuelve todo lo que resuelven los Result Contracts (Typed Results)
Los ActionResult existen para permitir que un controller retorne un tipo fuerte cuando todo sale bien,
pero al mismo tiempo permitir devolver códigos de error, sin embargo estos códigos de error pueden devolver cualquier cosa:
return BadRequest("algo");
return BadRequest(new { x = 1 });
return BadRequest();
return StatusCode(500, new { msg = "error fatal mi crack" });
Ahora veámoslo en una tabla comparativa:
| Característica | IActionResult | ActionResult<T> | Result Contracts |
|---|---|---|---|
| Tipo 200 tipado | ❌ | ✅ | ✅ |
| Tipo por cada código | ❌ | ❌ | ✅ |
| Validación por compilador | ❌ | ➖ Parcial | ✅Completo |
| Swagger preciso | ❌ Bajo | ➖ Medio | ✅Alto |
| Contrato explícito | ❌ | ❌ | ✅ |
| Compatible con Minimal APIs | ❌ | ➖ Parcial | ✅Total |
| Clientes generados confiables | ❌ Bajo | ➖ Mejor | ✅Excelente |
En conclusión dev, ya sabes...
Los Result Contracts (Typed Results) elevan la calidad de tus APIs:
- Definen contratos explícitos
- mejoran la documentación
- facilitan la generación de clientes
- permiten detectar errores en tiempo de compilación
- hacen tus endpoints más predecibles y mantenibles
- reducen ambigüedad y comportamiento dinámico innecesario
No es un cambio superficial ah, es una mejora real en arquitectura, diseño y mantenibilidad.
Cambio y fuera.
Comparte este artículo dev, ya mismo! 🐿️🙌
Créditos de imagen de portada: Foto de John Fornander en Unsplash
