¿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.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
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 compartencreatedAt, elidevita ambigüedad. - Si permites
sortconfigurable, limita campos permitidos y exige desempate (o aplícalo internamente).
Ejemplo de orden estable recomendado:
ORDER BY created_at DESC, id DESCPaso 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).
totalopcional: 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
nextyprevpara evitar que el cliente reconstruya parámetros. - Cursors: en cursor-based, devuelve
nextCursor(y opcionalmenteprevCursorsi soportas navegación hacia atrás).
Ejemplos completos
Ejemplo A: offset/limit
Solicitud
GET /orders?limit=25&offset=50Respuesta
{ "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:
returnedayuda a depurar y a clientes que no quieren contar elementos.totales ú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=25Respuesta
{ "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<=t0dondet0es el instante de la primera respuesta (el servidor puede devolverlo comosnapshotAty 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ámetro | Regla | Ejemplo |
|---|---|---|
limit | Entero, mínimo 1, máximo fijo (p. ej. 100). Si falta, usa un default (p. ej. 20). | ?limit=50 |
offset | Entero >= 0. Rechaza valores enormes si afectan rendimiento o aplica topes. | ?offset=0 |
cursor | Cadena 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=100o200) y documenta que el servidor puede “capar” ellimitsolicitado. - Si el cliente pide
limitmayor 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,
limitalto + 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+ (offsetocursor). - Devuelve
links.next/links.prevy metadatos de página;totalsolo 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.