Paginación y límites de resultados en colecciones REST

Capítulo 5

Tiempo estimado de lectura: 7 minutos

+ Ejercicio

¿Qué problema resuelve la paginación en colecciones?

Cuando un endpoint devuelve una colección (por ejemplo, /orders), responder con todos los elementos puede ser lento, costoso y propenso a fallos (tiempos de espera, consumo de memoria, respuestas enormes). La paginación limita cuántos elementos se devuelven por respuesta y permite al cliente recorrer el conjunto de resultados de forma controlada y predecible.

Además de paginar, es buena práctica definir límites máximos por página y mecanismos antiabuso para evitar que un cliente solicite páginas gigantes o haga scraping agresivo.

Dos enfoques comunes: offset/limit vs cursor-based

1) Paginación offset/limit

El cliente indica cuántos elementos quiere (limit) y desde qué posición lógica (offset).

  • Parámetros típicos: ?limit=50&offset=100
  • Ventajas: simple de entender, fácil de implementar, permite “saltar” a páginas arbitrarias (p. ej. ir al offset 1000).
  • Desventajas: puede degradar rendimiento en bases de datos grandes (offset alto), y es sensible a inserciones/borrados concurrentes: el contenido de “la página 3” puede cambiar entre peticiones.
  • Cuándo elegirlo: listados pequeños/medianos, backoffice con navegación aleatoria, o cuando el conjunto es relativamente estable y el rendimiento con offsets altos no es un problema.

2) Paginación basada en cursor (cursor-based / keyset pagination)

En lugar de un offset numérico, el servidor devuelve un cursor que representa una posición en el ordenamiento. El cliente pide la siguiente página usando ese cursor.

  • Parámetros típicos: ?limit=50&cursor=eyJzb3J0IjoiMjAyNi0wMi0wM1QxMjozNDowMFoiLCJpZCI6IjEwMDAifQ==
  • Ventajas: más estable ante cambios concurrentes (si se diseña bien), mejor rendimiento en grandes volúmenes (evita offsets altos), ideal para scroll infinito.
  • Desventajas: no permite saltar fácilmente a una página arbitraria; requiere un ordenamiento bien definido y un cursor opaco; complica algunos casos de UI tipo “ir a la página N”.
  • Cuándo elegirlo: feeds, timelines, listados muy grandes, escenarios con inserciones frecuentes y necesidad de rendimiento consistente.

Guía práctica paso a paso para diseñar paginación predecible

Paso 1: Define parámetros estándar y su contrato

Recomendación: usa nombres consistentes en toda la API.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

  • limit: tamaño de página solicitado. Debe tener mínimo y máximo.
  • offset: desplazamiento (solo si soportas offset/limit).
  • cursor: token opaco (solo si soportas cursor-based).

Evita mezclar offset y cursor en la misma petición. Si soportas ambos estilos, define reglas claras: por ejemplo, cursor tiene prioridad y offset se rechaza con error de validación.

Paso 2: Define un ordenamiento estable (imprescindible)

La paginación solo es predecible si el orden de los elementos es determinista. Un error común es paginar sin un sort estable, lo que produce duplicados o elementos “saltados” entre páginas.

Buenas prácticas:

  • Define un orden por defecto explícito (por ejemplo, createdAt desc).
  • Para estabilidad total, añade un desempate único: createdAt desc, id desc. Así, si dos elementos comparten createdAt, el id evita ambigüedad.
  • Si permites sort configurable, limita campos permitidos y exige desempate (o aplícalo internamente).

Ejemplo de orden estable recomendado:

ORDER BY created_at DESC, id DESC

Paso 3: Diseña la respuesta con metadatos y enlaces de navegación

En colecciones paginadas, el cliente necesita: los elementos, información de paginación y cómo pedir la siguiente/anterior página.

Patrón recomendado: incluir data y un objeto page o meta, más enlaces links (o al menos next/prev).

  • total opcional: calcular el total puede ser costoso. Si lo ofreces, hazlo opcional (por ejemplo, ?includeTotal=true) o solo en endpoints donde sea viable.
  • Links: devuelve URLs completas o relativas para next y prev para evitar que el cliente reconstruya parámetros.
  • Cursors: en cursor-based, devuelve nextCursor (y opcionalmente prevCursor si soportas navegación hacia atrás).

Ejemplos completos

Ejemplo A: offset/limit

Solicitud

GET /orders?limit=25&offset=50

Respuesta

{  "data": [    { "id": "o_103", "createdAt": "2026-02-03T12:34:00Z", "status": "paid" },    { "id": "o_102", "createdAt": "2026-02-03T12:20:00Z", "status": "paid" }  ],  "page": {    "limit": 25,    "offset": 50,    "returned": 2,    "total": 1000  },  "links": {    "self": "/orders?limit=25&offset=50",    "next": "/orders?limit=25&offset=75",    "prev": "/orders?limit=25&offset=25"  }}

Notas prácticas:

  • returned ayuda a depurar y a clientes que no quieren contar elementos.
  • total es útil para UI con “páginas” (1..N), pero puede ser caro. Considera hacerlo opcional.

Ejemplo B: cursor-based (keyset)

Supón orden por defecto: createdAt desc, id desc. El cursor codifica el último par (createdAt, id) de la página actual.

Solicitud

GET /orders?limit=25

Respuesta

{  "data": [    { "id": "o_200", "createdAt": "2026-02-03T12:34:00Z", "status": "paid" },    { "id": "o_199", "createdAt": "2026-02-03T12:33:10Z", "status": "pending" }  ],  "page": {    "limit": 25,    "returned": 2,    "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTAyLTAzVDEyOjMzOjEwWiIsImlkIjoib18xOTkifQ=="  },  "links": {    "self": "/orders?limit=25",    "next": "/orders?limit=25&cursor=eyJjcmVhdGVkQXQiOiIyMDI2LTAyLTAzVDEyOjMzOjEwWiIsImlkIjoib18xOTkifQ=="  }}

Siguiente página

GET /orders?limit=25&cursor=eyJjcmVhdGVkQXQiOiIyMDI2LTAyLTAzVDEyOjMzOjEwWiIsImlkIjoib18xOTkifQ==

Recomendaciones:

  • Trata el cursor como opaco: el cliente no debe interpretarlo.
  • Firma o cifra el cursor si contiene datos sensibles o si quieres evitar manipulación (por ejemplo, HMAC).
  • Define expiración si el cursor depende de estado que pueda invalidarse, pero intenta que sea estable el mayor tiempo posible.

Estabilidad del orden y concurrencia: qué puede salir mal

Problema típico con offset: inserciones entre páginas

Si el cliente pide offset=0 y luego offset=25, pero entre ambas peticiones se insertan nuevos elementos al inicio (por ejemplo, nuevos pedidos), los elementos se desplazan: el cliente puede ver duplicados o perder elementos.

Mitigaciones:

  • Usar cursor-based para feeds dinámicos.
  • Ofrecer un filtro de “ventana” temporal: por ejemplo, fijar createdAt<=t0 donde t0 es el instante de la primera respuesta (el servidor puede devolverlo como snapshotAt y el cliente lo reenvía).
  • Si necesitas consistencia fuerte, considera mecanismos de snapshot/transaction a nivel de almacenamiento (no siempre viable en APIs públicas).

Cursor-based también requiere cuidado

Cursor-based reduce problemas, pero no los elimina si el orden no es estable o si el cursor no incluye el desempate. Por ejemplo, si solo usas createdAt y hay múltiples elementos con la misma marca de tiempo, puedes repetir o saltarte elementos.

Regla práctica: el cursor debe incluir todos los campos del ORDER BY (incluyendo el desempate único).

Parámetros y validaciones recomendadas

ParámetroReglaEjemplo
limitEntero, mínimo 1, máximo fijo (p. ej. 100). Si falta, usa un default (p. ej. 20).?limit=50
offsetEntero >= 0. Rechaza valores enormes si afectan rendimiento o aplica topes.?offset=0
cursorCadena opaca. Valida formato y firma si aplica. Si es inválido, responde error de validación.?cursor=...
includeTotal (opcional)Booleano. Si true, incluye total (si es viable).?includeTotal=true

Recomendaciones de límites máximos por página y protección ante abuso

Máximos por página

  • Define un máximo global (por ejemplo, maxLimit=100 o 200) y documenta que el servidor puede “capar” el limit solicitado.
  • Si el cliente pide limit mayor al máximo, elige una política consistente: capar (devolver el máximo) o rechazar (error de validación). Cualquiera es válida si es consistente; capar suele ser más amigable.
  • Considera máximos distintos por recurso si el tamaño de cada item varía mucho.

Protección ante abuso

  • Rate limiting: limita solicitudes por token/IP para evitar scraping masivo.
  • Cost-based limiting: pondera el costo de la consulta (por ejemplo, limit alto + filtros complejos) y aplica cuotas.
  • Topes de offset: en offset/limit, offsets muy altos pueden ser caros. Puedes imponer un máximo de offset o recomendar cursor-based para recorridos profundos.
  • Campos y expansión: si tu API permite seleccionar campos o incluir relaciones, limita combinaciones costosas en endpoints paginados.

Checklist de implementación

  • Define un orden por defecto estable (con desempate único).
  • Elige estrategia: offset/limit (simplicidad) o cursor-based (rendimiento/estabilidad).
  • Estandariza parámetros: limit + (offset o cursor).
  • Devuelve links.next/links.prev y metadatos de página; total solo si es necesario y viable.
  • Valida límites y aplica máximos; añade rate limiting y/o cuotas.
  • Prueba concurrencia: inserciones entre páginas, empates en ordenamiento, cursores inválidos o manipulados.

Ahora responde el ejercicio sobre el contenido:

En una API REST que pagina una colección, ¿qué medida ayuda a que la paginación sea predecible y evite duplicados o elementos omitidos entre páginas?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

La paginación solo es estable si el orden es determinista. Añadir un desempate único evita ambigüedades (p. ej., registros con la misma fecha) y reduce duplicados o saltos entre páginas, especialmente con concurrencia.

Siguiente capítulo

Filtrado y ordenamiento: consultas expresivas sin romper la simplicidad REST

Arrow Right Icon
Portada de libro electrónico gratuitaDiseño de APIs REST: buenas prácticas, errores comunes y estándares
50%

Diseño de APIs REST: buenas prácticas, errores comunes y estándares

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.