Qué es?
Starndard API Responses es una buena práctica en el desarrollo de APIs RESTful que consiste en estructurar las respuestas de la API de manera consistente. Esto facilita la comprensión y el manejo de las respuestas por parte de los clientes que consumen la API (ya sean desarrolladores, usuarios o aplicaciones).
Por qué son una buena práctica?
Cuando las respuestas de una API siguen un formato estándar pasa lo siguiente:
- Los consumidores de la API pueden procesarlas de forma más predecible, mejorando la experiencia de desarrollo y el manejo de errores.
- Se proporciona una manera clara y estructurada de transmitir la información, incluidos los mensajes de éxito, advertencias, errores, y cualquier dato relevante.
- Mejora la experiencia del consumidor de la API.
- Facilita el manejo de errores y mensajes, evitando que el cliente tenga que analizar diferentes tipos de respuestas.
- Permite que tanto las respuestas de éxito como de error sigan el mismo esquema.
Comparación
Sin aplicar esta buena práctica tus responses lucen algo así:
Cuando ya aplicamos esta buena práctica los responses tienen una estructura definida, y lucen algo así:
Como te darás cuenta hasta los errores en el modelo o json son manejados de una mejor forma y no retorna cualquier cosa, sino siempre sigue una misma estructura.
Lo que vamos a hacer en este taller es que todos los endpoints retornen una de estos dos esquemas:
- Los que retornan algo de información:
{
"data" : "...",
"success" : true,
"errorMessage" : "..."
}
- Los demás:
{
"success" : true,
"errorMessage" : "..."
}
Qué implica aplicar esta buena práctica?
Para poder aplicarlo en tu API, deberás entender al menos superficialmente conceptos como:
- Patrón DTO: aquí te lo explico.
- Conceptos de arquitectura de software: para que sepas cómo se comunican distintas capas de un proyecto.
- Inyección de dependencias: este artículo te ayudará.
- Middleware y manejo de errores de modelo: aquí te ayudo.
No es necesario que seas un capo en cada uno de estos puntos mencionados pero sí deberás tener una noción de ellos como mínimo, y si no es así, entonces no te desanimes, investiga, por eso te dejé links de artículos donde explico algunos puntos.
Aplicando la homogenización
Lo aplicaremos a un proyecto ficticio muy sencillo pero que contiene todo lo que necesitas para aprender esta buena práctica, se trata de un API RESTful para gestionar libros.
Son 4 pasos:
1. DTO
En tu capa de contratos o DTO o como la hayas llamado añade estas dos clases DTO:
public class BaseResponseGeneric<T> : BaseResponse
{
public T Data { get; set; } = default!;
}
public class BaseResponse
{
public bool Success { get; set; }
public string ErrorMessage { get; set; } = default!;
}
2. Capa de servicios
Actualizar la capa de servicios, por supuesto esto va a depender cómo esté tu proyecto, quizá ni tengas capa de servicios, sea como sea, necesitas tener esta capa si estás utilizando una arquitectura layered, así que debería quedar algo así:
La interfaz:
public interface IBookService
{
Task<BaseResponseGeneric<ICollection<BookResponseDto>>> GetAsync();
Task<BaseResponseGeneric<BookResponseDto>> GetAsync(int id);
Task<BaseResponseGeneric<int>> AddAsync(BookRequestDto request);
Task<BaseResponse> UpdateAsync(int id, BookRequestDto request);
Task<BaseResponse> DeleteAsync(int id);
}
La implementación:
public class BookService : IBookService
{
private readonly IBookRepository repository;
private readonly ILogger<BookService> logger;
private readonly IMapper mapper;
public BookService(IBookRepository repository, ILogger<BookService> logger, IMapper mapper)
{
this.repository = repository;
this.logger = logger;
this.mapper = mapper;
}
public async Task<BaseResponseGeneric<ICollection<BookResponseDto>>> GetAsync()
{
var response = new BaseResponseGeneric<ICollection<BookResponseDto>>();
try
{
logger.LogInformation("Obteniendo todos los generos...");
response.Data = mapper.Map<List<BookResponseDto>>(await repository.GetAsync());
response.Success = true;
}
catch (Exception ex)
{
response.ErrorMessage = "Ocurrió un error al obtener la información.";
logger.LogError(ex, ex.Message);
}
return response;
}
public async Task<BaseResponseGeneric<BookResponseDto>> GetAsync(int id)
{
var response = new BaseResponseGeneric<BookResponseDto>();
try
{
response.Data = mapper.Map<BookResponseDto>(await repository.GetAsync(id));
response.Success = response.Data != null;
}
catch (Exception ex)
{
response.ErrorMessage = "Ocurrió un error al obtener la información.";
logger.LogError(ex, ex.Message);
}
return response;
}
public async Task<BaseResponseGeneric<int>> AddAsync(BookRequestDto request)
{
var response = new BaseResponseGeneric<int>();
try
{
response.Data = await repository.AddAsync(mapper.Map<Book>(request));
response.Success = true;
}
catch (Exception ex)
{
response.ErrorMessage = "Ocurrió un error al añadir la información.";
logger.LogError(ex, ex.Message);
}
return response;
}
public async Task<BaseResponse> UpdateAsync(int id, BookRequestDto request)
{
var response = new BaseResponse();
try
{
var entity = await repository.GetAsync(id);
if (entity is null)
{
response.ErrorMessage = "No se encontró el registro.";
return response;
}
mapper.Map(request, entity);
await repository.UpdateAsync();
response.Success = true;
}
catch (Exception ex)
{
response.ErrorMessage = "Ocurrió un error al actualizar la información.";
logger.LogError(ex, ex.Message);
}
return response;
}
public async Task<BaseResponse> DeleteAsync(int id)
{
var response = new BaseResponse();
try
{
await repository.DeleteAsync(id);
response.Success = true;
}
catch (Exception ex)
{
response.ErrorMessage = "Ocurrió un error al eliminar la información.";
logger.LogError(ex, ex.Message);
}
return response;
}
}
3. Actualizar controllers
Tu controlador quedaría algo así:
public class BooksController : ControllerBase
{
private readonly IBookService service;
public BooksController(IBookService service)
{
this.service = service;
}
[HttpGet]
public async Task<IActionResult> Get()
{
var response = await service.GetAsync();
return Ok(response);
}
[HttpGet("{id:int}")]
public async Task<IActionResult> Get(int id)
{
var response = await service.GetAsync(id);
return response.Success ? Ok(response) : NotFound(response);
}
[HttpPost]
public async Task<IActionResult> Post(BookRequestDto bookRequestDto)
{
var response = await service.AddAsync(bookRequestDto);
return response.Success ? Ok(response) : BadRequest(response);
}
[HttpPut("{id:int}")]
public async Task<IActionResult> Put(int id, BookRequestDto bookRequestDto)
{
var response = await service.UpdateAsync(id, bookRequestDto);
return response.Success ? Ok(response) : BadRequest(response);
}
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id)
{
var response = await service.DeleteAsync(id);
return response.Success ? Ok(response) : BadRequest(response);
}
}
4. Manejar errores de modelo
Finalmente deberás añadir este código a tu middleware, es decir a tu clase Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddScoped<IBookRepository, BookRepository>();
builder.Services.AddScoped<IBookService, BookService>();
//Configuring context
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("defaultConnection"));
});
builder.Services.AddControllers();
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var errors = context.ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
var response = new BaseResponse
{
Success = false,
ErrorMessage = string.Join("; ", errors) // Une los mensajes de error en un solo string.
};
return new BadRequestObjectResult(response);
};
});
Deberás añadir el código resaltado a tu clase.
El código completo esta en mi github
Aquí está el repo con este proyecto para que puedas analizarlo mejor y aplicarlo en tu caso, espero te sirva, ya sabes sígueme en mi github para más contenido, comparte con tu equipo de ingeniería y ...
Si esta entrada te ha gustado, compártela crack!
Créditos de la imagen de la portada: Foto de Chris Leipelt en Unsplash