Cómo Implementar Paginación en APIs REST (Guía Paso a Paso)

Mark Ponomarev

Mark Ponomarev

19 May 2025

Cómo Implementar Paginación en APIs REST (Guía Paso a Paso)

Al construir APIs REST que devuelven una lista de recursos, es crucial considerar cómo manejar grandes conjuntos de datos. Devolver miles o incluso millones de registros en una única respuesta de API es poco práctico y puede generar problemas significativos de rendimiento, alto consumo de memoria tanto para el servidor como para el cliente, y una mala experiencia de usuario. La paginación es la solución estándar a este problema. Implica dividir un gran conjunto de datos en partes más pequeñas y manejables llamadas "páginas", que luego se sirven secuencialmente. Este tutorial te guiará a través de los pasos técnicos para implementar varias estrategias de paginación en tus APIs REST.

💡
¿Quieres una excelente herramienta de prueba de API que genere hermosa Documentación de API?

¿Quieres una plataforma integrada, todo en uno para que tu Equipo de Desarrolladores trabaje junto con máxima productividad?

¡Apidog cumple todas tus demandas y reemplaza a Postman a un precio mucho más asequible!
button

¿Por qué es Esencial la Paginación?

Antes de sumergirnos en los detalles de implementación, hablemos brevemente de por qué la paginación es una característica no negociable para las APIs que manejan colecciones de recursos:

  1. Rendimiento: Solicitar y transferir grandes cantidades de datos puede ser lento. La paginación reduce el tamaño de la carga útil de cada solicitud, lo que lleva a tiempos de respuesta más rápidos y una carga reducida en el servidor.
  2. Consumo de Recursos: Las respuestas más pequeñas consumen menos memoria en el servidor que las genera y en el cliente que las analiza. Esto es especialmente crítico para clientes móviles o entornos con recursos limitados.
  3. Limitación de Tasa y Cuotas: Muchas APIs imponen límites de tasa. La paginación ayuda a los clientes a mantenerse dentro de estos límites al obtener datos en partes más pequeñas a lo largo del tiempo, en lugar de intentar obtener todo a la vez.
  4. Experiencia de Usuario: Para las interfaces de usuario que consumen la API, presentar los datos en páginas es mucho más fácil de usar que abrumar a los usuarios con una lista enorme o un desplazamiento muy largo.
  5. Eficiencia de la Base de Datos: Obtener un subconjunto de datos generalmente es menos exigente para la base de datos en comparación con recuperar una tabla completa, especialmente si se aplican índices adecuados.

Estrategias Comunes de Paginación

Existen varias estrategias comunes para implementar la paginación, cada una con sus propias ventajas y desventajas. Exploraremos las más populares: offset/limit (a menudo referida como paginación basada en página) y basada en cursor (también conocida como keyset o seek pagination).

1. Paginación Offset/Limit (o Basada en Página)

Este es posiblemente el método de paginación más sencillo y ampliamente adoptado. Funciona permitiendo al cliente especificar dos parámetros principales:

Alternativamente, los clientes pueden especificar:

El offset se puede calcular a partir de page y pageSize utilizando la fórmula: offset = (page - 1) * pageSize.

Pasos Técnicos de Implementación:

Supongamos que tenemos un endpoint de API /items que devuelve una lista de elementos.

a. Parámetros de Solicitud de API:
El cliente haría una solicitud como:
GET /items?offset=20&limit=10 (obtener 10 elementos, omitiendo los primeros 20)
o
GET /items?page=3&pageSize=10 (obtener la 3ª página, con 10 elementos por página, lo cual es equivalente a offset=20, limit=10).

Es una buena práctica establecer valores predeterminados para estos parámetros (por ejemplo, limit=20, offset=0 o page=1, pageSize=20) si el cliente no los proporciona. Además, impón un limit o pageSize máximo para evitar que los clientes soliciten un número excesivamente grande de registros, lo que podría sobrecargar el servidor.

b. Lógica de Backend (Conceptual):
Cuando el servidor recibe esta solicitud, necesita traducir estos parámetros en una consulta a la base de datos.

// Ejemplo en Java con Spring Boot
@GetMapping("/items")
public ResponseEntity<PaginatedResponse<Item>> getItems(
    @RequestParam(defaultValue = "0") int offset,
    @RequestParam(defaultValue = "20") int limit
) {
    // Validar el límite para prevenir abuso
    if (limit > 100) {
        limit = 100; // Imponer un límite máximo
    }

    List<Item> items = itemRepository.findItemsWithOffsetLimit(offset, limit);
    long totalItems = itemRepository.countTotalItems(); // Para metadatos

    // Construir y devolver la respuesta paginada
    // ...
}

c. Consulta a la Base de Datos (Ejemplo SQL):
La mayoría de las bases de datos relacionales admiten cláusulas offset y limit directamente.

Para PostgreSQL o MySQL:

SELECT *
FROM items
ORDER BY created_at DESC -- El orden consistente es crucial para una paginación estable
LIMIT 10 -- Este es el parámetro 'limit'
OFFSET 20; -- Este es el parámetro 'offset'

Para SQL Server (las versiones antiguas pueden usar ROW_NUMBER()):

SELECT *
FROM items
ORDER BY created_at DESC
OFFSET 20 ROWS
FETCH NEXT 10 ROWS ONLY;

Para Oracle:

SELECT *
FROM (
    SELECT i.*, ROWNUM rnum
    FROM (
        SELECT *
        FROM items
        ORDER BY created_at DESC
    ) i
    WHERE ROWNUM <= 20 + 10 -- offset + limit
)
WHERE rnum > 20; -- offset

Nota Importante sobre el Orden: Para que la paginación offset/limit sea fiable, el conjunto de datos subyacente debe estar ordenado por una clave consistente y única (o casi única), o una combinación de claves. Si el orden de los elementos puede cambiar entre solicitudes (por ejemplo, se insertan nuevos elementos o se actualizan elementos de manera que afecte su orden de clasificación), los usuarios podrían ver elementos duplicados u omitir elementos al navegar por las páginas. Una opción común es ordenar por marca de tiempo de creación o por un ID primario.

d. Estructura de la Respuesta de API:
Una buena respuesta paginada no solo debe incluir los datos de la página actual, sino también metadatos para ayudar al cliente a navegar.

{
  "data": [
    // array de elementos para la página actual
    { "id": "item_21", "name": "Item 21", ... },
    { "id": "item_22", "name": "Item 22", ... },
    // ... hasta 'limit' elementos
    { "id": "item_30", "name": "Item 30", ... }
  ],
  "pagination": {
    "offset": 20,
    "limit": 10,
    "totalItems": 5000, // Número total de elementos disponibles
    "totalPages": 500, // Calculado como ceil(totalItems / limit)
    "currentPage": 3 // Calculado como (offset / limit) + 1
  },
  "links": { // Enlaces HATEOAS para navegación
    "self": "/items?offset=20&limit=10",
    "first": "/items?offset=0&limit=10",
    "prev": "/items?offset=10&limit=10", // Nulo si está en la primera página
    "next": "/items?offset=30&limit=10", // Nulo si está en la última página
    "last": "/items?offset=4990&limit=10"
  }
}

Proporcionar enlaces HATEOAS (Hypermedia as the Engine of Application State) (self, first, prev, next, last) es una buena práctica REST. Permite a los clientes navegar por las páginas sin tener que construir las URLs ellos mismos.

Ventajas de la Paginación Offset/Limit:

Desventajas de la Paginación Offset/Limit:

2. Paginación Basada en Cursor (Keyset/Seek)

La paginación basada en cursor aborda algunas de las deficiencias de offset/limit, particularmente el rendimiento con grandes conjuntos de datos y los problemas de consistencia de datos. En lugar de depender de un offset absoluto, utiliza un "cursor" que apunta a un elemento específico en el conjunto de datos. El cliente luego solicita elementos "después" o "antes" de este cursor.

El cursor es típicamente una cadena opaca que codifica el(los) valor(es) de la(s) clave(s) de ordenación del último elemento recuperado en la página anterior.

Pasos Técnicos de Implementación:

a. Parámetros de Solicitud de API:
El cliente haría una solicitud como:
GET /items?limit=10 (para la primera página)
Y para páginas subsiguientes:
GET /items?limit=10&after_cursor=cadenasopacarepresentandoeliddeleltimoelemento
O, para paginar hacia atrás (menos común pero posible):
GET /items?limit=10&before_cursor=cadenasopacarepresentandoeliddelelpimerelemento

El parámetro limit aún define el tamaño de la página.

b. ¿Qué es un Cursor?
Un cursor debe ser:

c. Lógica de Backend (Conceptual):

// Ejemplo en Java con Spring Boot
@GetMapping("/items")
public ResponseEntity<CursorPaginatedResponse<Item>> getItems(
    @RequestParam(defaultValue = "20") int limit,
    @RequestParam(required = false) String afterCursor
) {
    // Validar el límite
    if (limit > 100) {
        limit = 100;
    }

    // Decodificar el cursor para obtener las propiedades del último elemento visto
    // e.g., LastSeenItemDetails lastSeen = decodeCursor(afterCursor);
    // Si afterCursor es nulo, es la primera página.

    List<Item> items;
    if (afterCursor != null) {
        DecodedCursor decoded = decodeCursor(afterCursor); // e.g., { lastId: "some_uuid", lastCreatedAt: "timestamp" }
        items = itemRepository.findItemsAfter(decoded.getLastCreatedAt(), decoded.getLastId(), limit);
    } else {
        items = itemRepository.findFirstPage(limit);
    }

    String nextCursor = null;
    if (!items.isEmpty() && items.size() == limit) {
        // Asumiendo que los elementos están ordenados, el último elemento de la lista se usa para generar el siguiente cursor
        Item lastItemOnPage = items.get(items.size() - 1);
        nextCursor = encodeCursor(lastItemOnPage.getCreatedAt(), lastItemOnPage.getId());
    }

    // Construir y devolver la respuesta paginada por cursor
    // ...
}

// Métodos auxiliares para codificar/decodificar cursores
// private DecodedCursor decodeCursor(String cursor) { ... }
// private String encodeCursor(Timestamp createdAt, String id) { ... }

d. Consulta a la Base de Datos (Ejemplo SQL):
La clave es usar una cláusula WHERE que filtre los registros basándose en la(s) clave(s) de ordenación del cursor. La cláusula ORDER BY debe alinearse con la composición del cursor.

Asumiendo ordenación por created_at (descendente) y luego por id (descendente) como desempate para una ordenación estable si created_at no es único:

Para la primera página:

SELECT *
FROM items
ORDER BY created_at DESC, id DESC
LIMIT 10;

Para páginas subsiguientes, si el cursor se decodificó a last_created_at_from_cursor y last_id_from_cursor:

SELECT *
FROM items
WHERE (created_at, id) < (CAST('last_created_at_from_cursor' AS TIMESTAMP), CAST('last_id_from_cursor' AS UUID)) -- O tipos apropiados
-- Para orden ascendente, sería >
-- La comparación de tuplas (created_at, id) < (val1, val2) es una forma concisa de escribir:
-- WHERE created_at < 'last_created_at_from_cursor'
--    OR (created_at = 'last_created_at_from_cursor' AND id < 'last_id_from_cursor')
ORDER BY created_at DESC, id DESC
LIMIT 10;

Este tipo de consulta es muy eficiente, especialmente si hay un índice en (created_at, id). La base de datos puede "buscar" directamente el punto de partida sin escanear filas irrelevantes.

e. Estructura de la Respuesta de API:

{
  "data": [
    // array de elementos para la página actual
    { "id": "item_N", "createdAt": "2023-10-27T10:05:00Z", ... },
    // ... hasta 'limit' elementos
    { "id": "item_M", "createdAt": "2023-10-27T10:00:00Z", ... }
  ],
  "pagination": {
    "limit": 10,
    "hasNextPage": true, // booleano que indica si hay más datos
    "nextCursor": "cadena_cursor_codificada_en_base64_para_item_M" // cadena opaca
    // Potencialmente un "prevCursor" si se admiten cursores bidireccionales
  },
  "links": {
    "self": "/items?limit=10&after_cursor=cursor_de_solicitud_actual_si_lo_hay",
    "next": "/items?limit=10&after_cursor=cadena_cursor_codificada_en_base64_para_item_M" // Nulo si no hay página siguiente
  }
}

Observa que la paginación basada en cursor típicamente no proporciona totalPages ni totalItems porque calcularlos requeriría un escaneo completo de la tabla, anulando algunos de los beneficios de rendimiento. Si estos son estrictamente necesarios, se podría proporcionar un endpoint separado o una estimación.

Ventajas de la Paginación Basada en Cursor:

Desventajas de la Paginación Basada en Cursor:

Elección de la Estrategia Correcta

La elección entre paginación offset/limit y basada en cursor depende de tus requisitos específicos:

En algunos sistemas, incluso se utiliza un enfoque híbrido, o se ofrecen diferentes estrategias para diferentes casos de uso o endpoints.

Mejores Prácticas para Implementar la Paginación

Independientemente de la estrategia elegida, adhiérete a estas mejores prácticas:

  1. Nomenclatura Consistente de Parámetros: Utiliza nombres claros y consistentes para tus parámetros de paginación (por ejemplo, limit, offset, page, pageSize, after_cursor, before_cursor). Mantén una convención (por ejemplo, camelCase o snake_case) en toda tu API.
  2. Proporciona Enlaces de Navegación (HATEOAS): Como se muestra en los ejemplos de respuesta, incluye enlaces para self, next, prev, first y last (donde sea aplicable). Esto hace que la API sea más descubrible y desacopla al cliente de la lógica de construcción de URLs.
  3. Valores Predeterminados y Límites Máximos:
  1. Documentación Clara de la API: Documenta tu estrategia de paginación de forma exhaustiva:
  1. Ordenación Consistente: Asegúrate de que los datos subyacentes estén ordenados de forma consistente para cada solicitud paginada. Para offset/limit, esto es vital para evitar sesgos de datos. Para paginación basada en cursor, el orden de ordenación dicta cómo se construyen e interpretan los cursores. Utiliza una columna de desempate única (como un ID primario) si la columna de ordenación primaria puede tener valores duplicados.
  2. Maneja Casos Extremos:
  1. Consideraciones sobre el Recuento Total:
  1. Manejo de Errores: Devuelve los códigos de estado HTTP apropiados para los errores (por ejemplo, 400 para entrada incorrecta, 500 para errores del servidor durante la obtención de datos).
  2. Seguridad: Aunque no es directamente un mecanismo de paginación, asegúrate de que los datos que se paginan respeten las reglas de autorización. Un usuario solo debe poder paginar a través de los datos que tiene permiso para ver.
  3. Caché: Las respuestas paginadas a menudo se pueden almacenar en caché. Para paginación basada en offset, GET /items?page=2&pageSize=10 es altamente cacheable. Para paginación basada en cursor, GET /items?limit=10&after_cursor=XYZ también es cacheable. Asegúrate de que tu estrategia de caché funcione bien con la forma en que se generan y consumen los enlaces de paginación. Las estrategias de invalidación deben considerarse si los datos subyacentes cambian con frecuencia.

Temas Avanzados (Menciones Breves)

Conclusión

Implementar la paginación correctamente es fundamental para construir APIs REST escalables y fáciles de usar. Si bien la paginación offset/limit es más sencilla para empezar, la paginación basada en cursor ofrece un rendimiento y consistencia superiores para conjuntos de datos grandes y dinámicos. Al comprender los detalles técnicos de cada estrategia, elegir la que mejor se adapte a las necesidades de tu aplicación y seguir las mejores prácticas de implementación y diseño de API, puedes asegurar que tu API entregue datos de manera eficiente a tus clientes, sin importar la escala. Recuerda siempre priorizar la documentación clara y el manejo robusto de errores para proporcionar una experiencia fluida a los consumidores de la API.


Practica el diseño de API en Apidog

Descubre una forma más fácil de construir y usar APIs