Apidog

Plataforma de desarrollo de API colaborativa todo en uno

Diseño de API

Documentación de API

Depuración de API

Simulación de API

Prueba automatizada de API

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

Mark Ponomarev

Mark Ponomarev

Updated on May 19, 2025

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:

  • offset: El número de registros a omitir desde el inicio del conjunto de datos.
  • limit: El número máximo de registros a devolver en una sola página.

Alternativamente, los clientes pueden especificar:

  • page: El número de página que desean recuperar.
  • pageSize (o per_page, limit): El número de registros por página.

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:

  • Simplicidad: Fácil de entender e implementar.
  • Navegación con Estado: Permite la navegación directa a cualquier página específica (por ejemplo, "ir a la página 50").
  • Ampliamente Compatible: El soporte de bases de datos para OFFSET y LIMIT es común.

Desventajas de la Paginación Offset/Limit:

  • Degradación del Rendimiento con Offsets Grandes: A medida que el valor de offset aumenta, las bases de datos pueden volverse más lentas. La base de datos a menudo todavía tiene que escanear todas las filas offset + limit antes de descartar las filas offset. Esto puede ser ineficiente para páginas profundas.
  • Sesgo de Datos/Elementos Omitidos: Si se añaden nuevos elementos o se eliminan elementos existentes del conjunto de datos mientras un usuario está paginando, la "ventana" de datos puede desplazarse. Esto podría hacer que un usuario vea el mismo elemento en dos páginas diferentes u omita un elemento por completo. Esto es particularmente problemático con conjuntos de datos que se actualizan con frecuencia. Por ejemplo, si estás en la página 2 (elementos 11-20) y se añade un nuevo elemento al principio de la lista, cuando solicites la página 3, lo que antes era el elemento 21 ahora es el elemento 22. Podrías omitir el nuevo elemento 21 o ver duplicados dependiendo del momento exacto y los patrones de eliminación.

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:

  • Opaco para el cliente: El cliente no debería necesitar entender su estructura interna. Simplemente lo recibe de una respuesta y lo envía de vuelta en la siguiente solicitud.
  • Basado en columna(s) única(s) y ordenadas secuencialmente: Típicamente, este es el ID primario (si es secuencial como un UUIDv1 o una secuencia de base de datos) o una columna de marca de tiempo. Si una sola columna no es lo suficientemente única (por ejemplo, varios elementos pueden tener la misma marca de tiempo), se utiliza una combinación de columnas (por ejemplo, timestamp + id).
  • Codificable y Decodificable: A menudo codificado en Base64 para asegurar que sea seguro para URLs. Podría ser tan simple como el ID mismo, o un objeto JSON { "last_id": 123, "last_timestamp": "2023-10-27T10:00:00Z" } que luego se codifica en Base64.

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:

  • Rendimiento en Grandes Conjuntos de Datos: Generalmente funciona mejor que offset/limit para paginación profunda, ya que la base de datos puede buscar eficientemente la posición del cursor utilizando índices.
  • Estable en Conjuntos de Datos Dinámicos: Menos susceptible a elementos omitidos o duplicados cuando se añaden o eliminan datos con frecuencia, ya que el cursor se ancla a un elemento específico. Si se elimina un elemento antes del cursor, no afecta a los elementos subsiguientes.
  • Adecuado para Desplazamiento Infinito: El modelo de "página siguiente" encaja naturalmente con interfaces de usuario de desplazamiento infinito.

Desventajas de la Paginación Basada en Cursor:

  • No hay "Saltar a Página": Los usuarios no pueden navegar directamente a un número de página arbitrario (por ejemplo, "página 5"). La navegación es estrictamente secuencial (siguiente/anterior).
  • Implementación Más Compleja: Definir y gestionar cursores, especialmente con múltiples columnas de ordenación u órdenes de ordenación complejas, puede ser más intrincado.
  • Limitaciones de Ordenación: El orden de ordenación debe ser fijo y basarse en las columnas utilizadas para el cursor. Cambiar el orden de ordenación sobre la marcha con cursores es complejo.

Elección de la Estrategia Correcta

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

  • Offset/Limit a menudo es suficiente si:
  • El conjunto de datos es relativamente pequeño o no cambia con frecuencia.
  • La capacidad de saltar a páginas arbitrarias es una característica crítica.
  • La simplicidad de implementación es una alta prioridad.
  • El rendimiento para páginas muy profundas no es una preocupación importante.
  • Basada en Cursor generalmente se prefiere si:
  • Estás tratando con conjuntos de datos muy grandes y que cambian con frecuencia.
  • El rendimiento a escala y la consistencia de los datos durante la paginación son primordiales.
  • La navegación secuencial (como el desplazamiento infinito) es el caso de uso principal.
  • No necesitas mostrar el número total de páginas ni permitir saltar a páginas específicas.

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:
  • Siempre establece valores predeterminados sensibles para limit (por ejemplo, 10 o 25).
  • Impón un limit máximo para evitar que los clientes soliciten demasiados datos y sobrecarguen el servidor (por ejemplo, máximo 100 registros por página). Devuelve un error o limita el valor si se solicita un valor no válido.
  1. Documentación Clara de la API: Documenta tu estrategia de paginación de forma exhaustiva:
  • Explica los parámetros utilizados.
  • Proporciona ejemplos de solicitudes y respuestas.
  • Aclara los límites predeterminados y máximos.
  • Explica cómo se utilizan los cursores (si aplica), sin revelar su estructura interna si están destinados a ser opacos.
  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:
  • Resultados Vacíos: Devuelve un array de datos vacío y los metadatos de paginación apropiados (por ejemplo, totalItems: 0 o hasNextPage: false).
  • Parámetros Inválidos: Devuelve un error 400 Bad Request si los clientes proporcionan parámetros de paginación inválidos (por ejemplo, límite negativo, número de página no entero).
  • Cursor No Encontrado (para paginación basada en cursor): Si un cursor proporcionado es inválido o apunta a un elemento eliminado, decide un comportamiento: devuelve un 404 Not Found o 400 Bad Request, o vuelve elegantemente a la primera página.
  1. Consideraciones sobre el Recuento Total:
  • Para offset/limit, proporcionar totalItems y totalPages es común. Ten en cuenta que COUNT(*) puede ser lento en tablas muy grandes. Explora optimizaciones o estimaciones específicas de la base de datos si esto se convierte en un cuello de botella.
  • Para paginación basada en cursor, totalItems a menudo se omite por rendimiento. Si es necesario, considera proporcionar un recuento estimado o un endpoint separado que lo calcule (potencialmente de forma asíncrona).
  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)

  • Desplazamiento Infinito: La paginación basada en cursor es una opción natural para interfaces de usuario de desplazamiento infinito. El cliente obtiene la primera página, y a medida que el usuario se desplaza cerca de la parte inferior, utiliza el nextCursor para obtener el siguiente conjunto de elementos.
  • Paginación con Filtrado y Ordenación Complejos: Al combinar la paginación con parámetros dinámicos de filtrado y ordenación, asegúrate de que:
  • Para offset/limit: El recuento de totalItems refleje con precisión el conjunto de datos filtrado.
  • Para paginación basada en cursor: El cursor codifique el estado tanto de la ordenación como del filtrado si estos afectan lo que significa "siguiente". Esto puede complicar significativamente el diseño del cursor. A menudo, si cambian los filtros o el orden de ordenación, la paginación se restablece a la "primera página" de la nueva vista.
  • Paginación GraphQL: GraphQL tiene su propia forma estandarizada de manejar la paginación, a menudo referida como "Connections". Típicamente utiliza paginación basada en cursor y tiene una estructura definida para devolver bordes (elementos con cursores) e información de página. Si estás utilizando GraphQL, adhiérete a sus convenciones (por ejemplo, la especificación Relay Cursor Connections).

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.