22 agosto 2022

REST API: Respuestas Paginadas

Cuando nos encontramos ante el diseño de una API REST entre las dudas con que nos podemos cruzar está el diseño de listas paginadas.

Trabajando con Spring, y accediendo a los datos con Spring Data, estaremos acostumbrados a usar las características de paginación que ofrece el framework. Esto está bien para el acceso a la base de datos. Pero, ¿cómo devolvemos estos datos a través de una API? ¿Los devolvemos tal cual? ¿O los convertimos a DTOs específicos?

En este artículo propondremos una (de tantas) soluciones posibles para este problema.

Comenzamos

Para comenzar veremos una sencilla API REST que devuelve un conjunto de datos ya paginados desde la consulta a la base de datos. A partir de aquí iremos aplicando modificaciones hasta conseguir el resultado buscado.

La estructura es muy simple: un controlador, un repositorio, y una clase de entidad. Si observamos el código del controlador veremos que sólo contiene una llamada (con paginación) al repositorio para a continuación devolver directamente el resultado obtenido:

@RestController
@RequestMapping("/users")
public class UserController {

    private final UserRepository userRepository;

    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping("")
    public Page<User> listUsers(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) {
        PageRequest pageRequest = PageRequest.of(page - 1, size);
        return this.userRepository.findAll(pageRequest);
    }
}

Si ejecutamos la aplicación y realizamos una petición a http://localhost:8080/users obtendremos algo similar a esto:

{
  "content": [
    {
      "id": 1,
      "firstName": "Ameline",
      "lastName": "Farrant",
      "email": "afarrant0@nationalgeographic.com",
      "gender": "Female",
      "ipAddress": "241.39.5.78"
    },
    {
      "id": 2,
      "firstName": "Lauren",
      "lastName": "Willmont",
      "email": "lwillmont1@behance.net",
      "gender": "Female",
      "ipAddress": "197.243.106.164"
    },
    ...
    {
      "id": 10,
      "firstName": "Inna",
      "lastName": "Peterken",
      "email": "ipeterken9@dmoz.org",
      "gender": "Female",
      "ipAddress": "41.86.33.168"
    }
  ],
  "pageable": {
    "sort": {
      "empty": true,
      "sorted": false,
      "unsorted": true
    },
    "offset": 0,
    "pageSize": 10,
    "pageNumber": 0,
    "paged": true,
    "unpaged": false
  },
  "last": false,
  "totalElements": 50,
  "totalPages": 5,
  "size": 10,
  "number": 0,
  "sort": {
    "empty": true,
    "sorted": false,
    "unsorted": true
  },
  "first": true,
  "numberOfElements": 10,
  "empty": false
}

Como se puede comprobar el JSON devuelto corresponde al interface Page de Spring Data, conteniendo objetos que se corresponden a la clase *entity* User de la aplicación.

Esta solución, para un servicio sencillo, ya nos puede ir bien. Sin embargo, puede darse el caso de que no nos interese que el servicio devuelva una respuesta como la indicada. Por ejemplo, porque en nuestro equipo de trabajo tengamos especificado un formato diferente para las respuestas de las APIs REST.

Para estos casos será necesario transformar la respuesta obtenida del repositorio en otra que será la que devolvamos en el servicio

Implementar los DTOs

Lo primero, veamos como queremos que sea la respuesta paginada de la API REST. Esta respuesta estará formada por dos objetos: meta y data. El primero, meta, contendrá los datos de paginación de la respuesta. Mientras que data consistirá en una lista de items, que a su vez serán DTOs diferentes de las clases de entidad.

/**
 * Encapsula los "metadatos" de la respuesta
 */
public class PageMeta implements Serializable {

    private int currentPage;
    private long totalElements;
    private int totalPages;
    private int size;
    private int numberOfElements;

    public PageMeta() {
    }

    // Nos ahorramos los getters/setters para mayor legibilidad del código
}
/**
 * Contiene los datos que queremos devolver de cada usuario
 */
public class UserDTO implements Serializable {
    private long id;
    private String name;
    private String email;
    private String gender;

    public UserDTO() {
    }

    // Nos ahorramos los getters/setters para mayor legibilidad del código
}

Transformar la entidad User en el DTO UserDTO

TBD

Clase genérica de respuesta paginada

TBD

Versión mejorada del controller

TBD

Resultado

{
  "meta": {
    "currentPage": 2,
    "totalElements": 50,
    "totalPages": 17,
    "size": 3,
    "numberOfElements": 3
  },
  "data": [
    {
      "id": 4,
      "name": "Sapphira Brandolini",
      "email": "sbrandolini3@time.com",
      "gender": "F"
    },
    {
      "id": 5,
      "name": "Yorgo Lightbourn",
      "email": "ylightbourn4@prlog.org",
      "gender": "M"
    },
    {
      "id": 6,
      "name": "Phylis Pencost",
      "email": "ppencost5@51.la",
      "gender": "A"
    }
  ]
}

Conclusión

Puedes consultar el código usado para realizar este tutorial en este repositorio de GitHub.

Además, puedes seguir el detalle paso a paso en este vídeo: