Toma en cuenta lo siguiente antes de seguir con el post.
Antes de empezar con el post, debes tomar en cuenta que la implementación aquí mostrada puede servir para versiones >= de .NET Core 1.0, además no entrare en detalle cosas como que es un Genérico, tipos de datos, que es un protocolo HTTP, que es una API, inyección de dependencias, etc.
Ahora si empezamos.
Si llegaste hasta aquí, probablemente te estés preguntando cómo implementar el estándar RESTful en un proyecto API de ASP.NET.
A lo largo de mi carrera, he notado que en la mayoría de los proyectos en los que he trabajado, los códigos de respuesta HTTP utilizados suelen ser limitados, frecuentemente solo 2 o 3 códigos del estándar RESTful. Esto puede llevar a que el código sea repetitivo y difícil de mantener, especialmente cuando se utilizan múltiples condicionales (if-else) o un switch
para manejar diferentes respuestas.
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
// Verifica si el producto existe en la "base de datos"
if (id <= 0)
{
// Código de estado 400 Bad Request para IDs inválidos
return BadRequest(new { Message = "Invalid product ID"});
}
else if (Products.ContainsKey(id))
{
// Código de estado 200 OK para productos existentes
return Ok(new { Id = id, Name = Products[id] });
}
else
{
// Código de estado 404 Not Found
return NotFound(new { Message = "Product not found" });
}
}
Aunque este ejemplo es un escenario sencillo, nos permite destacar varios puntos importantes sobre la implementación de respuestas en una API:
Manejo de Respuestas con Condicionales
if-else
en Diversos Escenarios: El controlador utiliza múltiples condicionalesif-else
para manejar diferentes escenarios. El cual debería estar en la misma capa de negocio del proyecto y este solo reciba la respuesta de lo consultado.Control de Escenarios Diversos: El ejemplo no maneja todos los posibles escenarios que podrían ocurrir en una consulta más realista, como errores en base de datos, servicios externos no disponibles, o errores del servidor.
Escalabilidad del Código: La lógica en el controlador puede crecer rápidamente con el aumento de la complejidad, haciendo que el controlador sea difícil de mantener y de entender.
Pruebas Unitarias: La lógica de negocio en el controlador puede ser difícil de probar unitariamente debido a su acoplamiento con la capa de presentación.
PROPUESTA DE IMPLEMENTACION
Ahora que estamos en la misma sintonía en cuanto a el fin de lo que quiero expresar con este post, vayamos a la implementación.
Nota: Quizás esta implementación no sea la mejor solución para todos los escenarios, es simplemente una porción de código simple que me ayudo a tener mayor ampliación de respuestas en el proyecto en el que trabaje. Toma en cuenta esto antes de proceder a implementarlo.
Componentes
Crearemos una clase llamada ApiUtilsConst
en la capa de utilidad de nuestro proyecto, el cual contendrá las propiedades necesarias para el próximo punto, pero básicamente se encargara de soportar la centralización de respuesta que tendrán nuestros controladores.
public class ApiUtilsConst
{
public int Code { get; set; }
public string Description { get; set; }
public HttpStatusCode StatusCode { get; set; }
}
Propiedades
- Code: int — Representa un código de error o éxito específico.
- Description: string — Contiene un mensaje descriptivo asociado al código.
- StatusCode: HttpStatusCode — Almacena el código de estado HTTP correspondiente.
Luego, crearemos en nuestra capa de dominio donde estará centralizada las respuesta de los distintos escenarios que pudiera tener nuestro servicio.
public class WeatherForecastApiResponse
{
public static ApiUtilsConst SUCCESS = new() { Code = 1000, Description = "La solicitud se realizó correctamente.", StatusCode = HttpStatusCode.OK };
public static ApiUtilsConst ERROR = new() { Code = 2000, Description = "Se ha producido un error inesperado.", StatusCode = HttpStatusCode.InternalServerError };
public static ApiUtilsConst NOT_FOUND = new() { Code = 2001, Description = "El recurso solicitado no existe.", StatusCode = HttpStatusCode.NotFound };
public static ApiUtilsConst BAD_REQUEST = new() { Code = 2002, Description = "Hubo un error en la solcitud del cliente.", StatusCode = HttpStatusCode.BadRequest };
public static ApiUtilsConst UNAUTHORIZED = new() { Code = 2003, Description = "No tiene autorizacion para acceder al recurso", StatusCode = HttpStatusCode.Unauthorized };
}
Ten en cuenta que el nombre de la clase hace una conexión con el contexto de lo que se esta consumiendo, en este caso esas respuestas serán del contexto WeatherForecast
, en otros contextos agrega otra clase para que así manejes los distintos escenarios dependiendo del contexto del controlador que se consulte.
El próximo paso seria crear la clase que será la encargada de armar el cuerpo de respuesta de nuestro servicio, que en este caso le llame ApiResponseModel<T>
pero tu le puedes llamar como quieras.
public class ApiResponseModel<T>
{
public string? Message { get; set;}
public int? Operation {get; set;}
public int StatusCode { get; set;}
public T? Response { get; set;}
}
La clase ApiResponseModel está diseñada para estructurar las respuestas de una API de manera flexible utilizando un tipo genérico.
Propiedades
string? Message
: Ofrece un mensaje descriptivo sobre el resultado de la operación, como éxito o error.
int? Operation
: Almacena un código entero que identifica la operación realizada o el tipo de respuesta.
int StatusCode
: Indica el código de estado HTTP de la respuesta, como 200 para éxito o 404 para no encontrado.
T? Response
: Contiene la carga útil de la respuesta, que puede ser de cualquier tipo definido por el parámetro genérico T.
El siguiente paso será el ultimo, para luego ir a un ejemplo de uso.
Crearemos una clase llamada ApiResponseHandler
o la puedes llamar como se te venga en gana :), donde va a converger toda la funcionalidad de esta implementacion.
public class ApiResponseHandler
{
private static readonly Dictionary<int, ApiUtilsConst> ResponseMappings = new();
public static ApiResponseModel<T?> GetApiResponse<T>(ApiUtilsConst apiUtilsConst, T? response)
{
ResponseMappings[apiUtilsConst.Code] = new ApiUtilsConst()
{
Code = apiUtilsConst.Code,
Description = apiUtilsConst.Description,
StatusCode = apiUtilsConst.StatusCode
};
var apiResponse = new ApiResponseModel<T?>
{
Message = ResponseMappings.TryGetValue(apiUtilsConst.Code, out var mappedResponse) ? mappedResponse.Description : "Error desconocido",
Operation = apiUtilsConst.Code,
StatusCode = (int)(mappedResponse?.StatusCode ?? HttpStatusCode.InternalServerError),
Response = response
};
return apiResponse;
}
}
La clase ApiResponseHandler proporciona una forma centralizada de gestionar y mapear respuestas API, facilitando la consistencia en la forma en que se manejan los códigos de respuesta y los mensajes asociados.
Sección:private static readonly Dictionary<int, ApiUtilsConst> ResponseMappings
Descripción: Este diccionario privado y estático almacena un mapeo entre códigos de respuesta (int) y sus respectivas configuraciones de respuesta (ApiUtilsConst).
Propósito: Permite almacenar y actualizar los mapeos de respuestas para que puedan ser reutilizados a lo largo de la aplicación. Facilita la asociación entre códigos de respuesta, descripciones y códigos de estado HTTP.
Método: public static ApiResponseModel<T?> GetApiResponse<T>(ApiUtilsConst apiUtilsConst, T? response)
Descripción: Este método estático genera un modelo de respuesta API (ApiResponseModel) basado en la configuración proporcionada por ApiUtilsConst y la respuesta de la API.
Propósito: Proporciona una estructura uniforme para las respuestas API, incluyendo un mensaje descriptivo, el código de operación, el código de estado HTTP, y la respuesta real de la API.
Cuerpo del Método:
Actualiza el diccionario de mapeo de respuestas:
ResponseMappings[apiUtilsConst.Code] = new ApiUtilsConst()
{
Code = apiUtilsConst.Code,
Description = apiUtilsConst.Description,
StatusCode = apiUtilsConst.StatusCode
};
Descripción: Actualiza el diccionario ResponseMappings con la nueva configuración proporcionada por apiUtilsConst. Esto asegura que el diccionario siempre tenga la configuración más reciente para cada código de respuesta.
Propósito: Mantener una referencia actualizada a las configuraciones de respuesta, lo que permite que la API responda de manera consistente según las configuraciones definidas.
Genera el ApiResponseModel:
var apiResponse = new ApiResponseModel<T?>
{
Message = ResponseMappings.TryGetValue(apiUtilsConst.Code, out var mappedResponse) ? mappedResponse.Description : "Error desconocido",
Operation = apiUtilsConst.Code,
StatusCode = (int)(mappedResponse?.StatusCode ?? HttpStatusCode.InternalServerError),
Response = response
};
Descripción: Crea una instancia de ApiResponseModel utilizando el mapeo de respuesta obtenido del diccionario. Si el código de respuesta no se encuentra en el diccionario, se establece un mensaje de error genérico ("Error desconocido") y se asigna un código de estado HTTP interno por defecto.
Propósito: Proporcionar una respuesta estructurada que incluya un mensaje descriptivo, el código de operación, el código de estado HTTP, y la respuesta real de la API, facilitando la interpretación y el manejo de respuestas por parte del cliente.
Ejemplo de uso
Asumiendo que hemos separado nuestro proyecto en capas para lograr un código desacoplado y reutilizable.(Como debe de ser -.-)
Tendríamos los siguientes componentes
Controlador
[HttpGet("notfound", Name = "GetWeatherForecastNotFound")]
public async Task<IActionResult> GetNotFound()
{
var result = await
_weatherForecastService.GetWeatherForecastNotFound();
return StatusCode(result.StatusCode, result);
}
Este método se llama cuando un cliente realiza una solicitud GET a la ruta notfound(Se llama notfound porque a fines de este ejemplo, devolverá un código 404). Devolverá la respuesta con el código de estado apropiado y el resultado obtenido del servicio. Por ejemplo, si no se encuentra el pronóstico, podría devolver un código 404 con un mensaje o datos que expliquen que no se encontró la información.
Como podrás haberte dado cuenta que a diferencia del ejemplo que usamos en la introducción a este post, el EP de este controlador solo usara dos líneas de códigos, 1 donde se consulta el servicio (El cual podrá responder positiva o negativamente, eso en esta capa no nos interesa) y 2 lo que queremos retornar utilizando la funcionalidad del marco de ASP StatusCode
el cual te permite especificar que código quieres responder.
Ahora veamos como esta construido nuestro servicio...
public async Task<ApiResponseModel<WeatherForecast?>> GetWeatherForecastNotFound()
{
var result = await _weatherForecastRepository.GetWeatherForecast();
var findresult = result.Find(a => a.Summary == "XD");
if(findresult == null)
{
return ApiResponseHandler.GetApiResponse<WeatherForecast?>(WeatherForecastApiResponse.NOT_FOUND, null);
}
return
ApiResponseHandler.GetApiResponse(WeatherForecastApiResponse.SUCCESS,
findresult);
}
Este código busca un pronóstico del tiempo específico y devuelve una respuesta API adecuada basada en el resultado de la búsqueda.
Como puedes notar, el servicio responde con ApiResponseModel. Al definir esta clase con un tipo genérico, puedes especificar el tipo de dato que el servicio debe devolver, lo que añade flexibilidad a la implementación.
El código realiza una consulta en la base de datos (o cualquier otra fuente de datos) y evalúa los resultados. Dependiendo del escenario, la respuesta será WeatherForecastApiResponse.NOT_FOUND
o WeatherForecastApiResponse.SUCCESS
. Como habrás visto, la clase WeatherForecastApiResponse
centraliza las respuestas, asociando los códigos HTTP, los mensajes descriptivos, los códigos internos del sistema, y los códigos de respuesta HTTP que corresponden a cada situación.
Este enfoque te ofrece un mayor control en la capa de servicio, permitiéndote realizar validaciones, consultas y controles de forma más efectiva. Los if-else
que antes manejabas en el controlador ahora se trasladan a la capa de servicio, proporcionando un manejo más ordenado y centralizado de los códigos de respuesta según el estándar RESTful. Esto facilita la asignación de códigos de estado específicos según el escenario, mejorando la claridad y mantenibilidad de tu aplicación.
Conclusión
En resumen, hemos transformado nuestros controladores de ser un campo de batalla lleno de if-else
, en un tranquilo jardín zen de respuestas bien organizadas. Ahora, en lugar de lidiar con caos y confusión, tenemos un elegante sistema donde cada código HTTP sabe exactamente cuándo hacer su entrada triunfal. Así que la próxima vez que te enfrentes a un alboroto de condiciones, recuerda: ¡mantén la calma, organiza tus respuestas y deja que tu API fluya con la serenidad de un monje RESTful!