Exemples de pagination d’API : offset, page, curseur, keyset, en-tête Link et HAL

Six styles de pagination sur les mêmes données, chacun avec une réponse réelle et une boucle qui récupère toutes les pages.

Exemples exécutés pour la dernière fois contre l’API le

Les API découpent les longues listes en pages de six façons courantes. Offset et limit sautent un nombre d’éléments. Numéro et taille de page comptent les pages, comme Spring Data. Un curseur est un token opaque, comme chez Stripe, et la pagination keyset demande les éléments après le dernier id. Certaines API placent l’URL suivante dans un en-tête Link, comme GitHub, et HAL la place dans des liens hypermédia. https://api.testingapis.com/api/paginationconnectez-vous pour voir sert les mêmes 100 éléments dans les six styles, pour comparer les réponses côte à côte et tester un client contre chacun. Gratuit et sans clé. Chaque style est présenté ci-dessous avec une réponse réelle.

Les six styles en un coup d’œil

StyleRequêteLa page suivante vient deUtilisé par
Offset / limit/offset?offset=20&limit=10offset + limitBeaucoup d’API sur SQL
Numéro de page (à partir de 0)/page?page=2&size=10number + 1, jusqu’à ce que last vaille trueSpring Data
Curseur/cursor?limit=10&cursor=…next_cursor, jusqu’à ce que has_more vaille falseStripe
Keyset/keyset?after_id=20&limit=10next_after_idGrandes tables, fils d’actualité
En-tête Link (à partir de 1)/link?page=3&per_page=10L’URL rel="next" de l’en-tête LinkGitHub
HAL/hal?page=2&size=10_links.next.hrefSpring HATEOAS

Tous les chemins sont sous /api/paginationconnectez-vous pour voir. La taille de page va de 1 à 100, 10 par défaut.

Offset et limit

Le style le plus simple : sauter offset éléments et renvoyer les limit suivants. Le total figure dans le corps et dans X-Total-Count.

Terminal
$ curl -i "https://api.testingapis.com/api/pagination/offset?offset=0&limit=2"
HTTP/1.1 200 OK
x-total-count: 100

{"data":[{"id":1,"name":"Smart Headset",...},{"id":2,"name":"Compact Speaker",...}],
 "pagination":{"offset":0,"limit":2,"total":100,"hasMore":true}}
Cet exemple utilise des points d’accès réservés aux membres connectés.Se connecterCréer un compte gratuit

Numéro et taille de page

L’enveloppe de Spring Data. Les pages commencent à 0, et c’est le bug à tester : un client écrit pour des pages qui commencent à 1 saute la première page sans rien dire.

Terminal
$ curl "https://api.testingapis.com/api/pagination/page?page=0&size=2"

{"content":[{"id":1,...},{"id":2,...}],
 "pageable":{"pageNumber":0,"pageSize":2,"offset":0,"paged":true},
 "totalElements":100,"totalPages":50,"number":0,"size":2,"numberOfElements":2,
 "first":true,"last":false,"empty":false}
Cet exemple utilise des points d’accès réservés aux membres connectés.Se connecterCréer un compte gratuit

Curseur

La réponse contient un next_cursor. Renvoyez-le tel quel pour obtenir la page suivante, et arrêtez-vous quand has_more vaut false. Le curseur se trouve être du JSON encodé en base64, mais un client doit le traiter comme opaque. Un curseur modifié reçoit un 400 :

Terminal
$ curl "https://api.testingapis.com/api/pagination/cursor?limit=2"

{"object":"list","url":"/api/pagination/cursor",
 "data":[{"id":1,...},{"id":2,...}],
 "has_more":true,"next_cursor":"eyJsYXN0SWQiOjJ9"}

$ curl "https://api.testingapis.com/api/pagination/cursor?cursor=garbage"
HTTP/1.1 400 Bad Request

{"status":400,"error":"Bad Request","message":"Invalid cursor. Pass the 'next_cursor' value from a previous response unchanged."}
Cet exemple utilise des points d’accès réservés aux membres connectés.Se connecterCréer un compte gratuit
GET/api/pagination/cursor?limit=3
Cette requête utilise un point d’accès réservé aux membres connectés.Se connecterCréer un compte gratuit

Keyset

Comme un curseur, mais lisible : demandez les éléments dont l’id dépasse le dernier que vous avez vu. Les insertions et suppressions sur les pages précédentes ne peuvent pas décaler les résultats, d’où son usage pour les fils d’actualité et les très grandes tables.

Terminal
$ curl "https://api.testingapis.com/api/pagination/keyset?after_id=10&limit=2"

{"data":[{"id":11,"name":"Smart Lamp",...},{"id":12,"name":"Compact Headset",...}],
 "has_more":true,"next_after_id":12}
Cet exemple utilise des points d’accès réservés aux membres connectés.Se connecterCréer un compte gratuit

Le corps est un simple tableau, et la navigation se trouve dans l’en-tête Link (RFC 8288), avec des URL absolues pour next, prev, first et last. Une page au-delà de la fin renvoie un tableau vide, pas un 404.

Terminal
$ curl -i "https://api.testingapis.com/api/pagination/link?page=2&per_page=2"
HTTP/1.1 200 OK
link: <https://api.testingapis.com/api/pagination/link?page=3&per_page=2>; rel="next",
      <https://api.testingapis.com/api/pagination/link?page=50&per_page=2>; rel="last",
      <https://api.testingapis.com/api/pagination/link?page=1&per_page=2>; rel="first",
      <https://api.testingapis.com/api/pagination/link?page=1&per_page=2>; rel="prev"
x-total-count: 100

[{"id":3,"name":"Deluxe Blender",...},{"id":4,"name":"Eco Bottle",...}]
Cet exemple utilise des points d’accès réservés aux membres connectés.Se connecterCréer un compte gratuit

HAL

Les éléments sont sous _embedded.items, chacun avec son propre lien self, et les liens de page sous _links. Le type de contenu est application/hal+json, que certains clients ne lisent comme du JSON que si on le leur dit.

Terminal
$ curl "https://api.testingapis.com/api/pagination/hal?page=0&size=2"

{"_embedded":{"items":[
   {"id":1,...,"_links":{"self":{"href":"/api/pagination/items/1"}}},
   {"id":2,...,"_links":{"self":{"href":"/api/pagination/items/2"}}}]},
 "_links":{"self":{"href":"/api/pagination/hal?page=0&size=2"},
           "first":{"href":"/api/pagination/hal?page=0&size=2"},
           "next":{"href":"/api/pagination/hal?page=1&size=2"},
           "last":{"href":"/api/pagination/hal?page=49&size=2"}},
 "page":{"size":2,"totalElements":100,"totalPages":50,"number":0}}
Cet exemple utilise des points d’accès réservés aux membres connectés.Se connecterCréer un compte gratuit

Récupérer toutes les pages

La plupart des bugs de pagination apparaissent quand un client parcourt toute la liste : il s’arrête une page trop tôt, tourne en boucle sur la dernière ou compte les pages à partir du mauvais numéro. Ce script récupère les 100 éléments deux fois, une par curseur et une par en-tête Link. Les deux fois, il finit sur l’id 100 : rien n’a été sauté ni répété.

paginate.mjs
const BASE = "https://api.testingapis.com/api/pagination";

// Cursor: follow next_cursor until has_more is false.
async function allByCursor(limit = 25) {
  const items = [];
  let cursor;
  do {
    const url = new URL(`${BASE}/cursor`);
    url.searchParams.set("limit", limit);
    if (cursor) url.searchParams.set("cursor", cursor);
    const page = await (await fetch(url)).json();
    items.push(...page.data);
    cursor = page.has_more ? page.next_cursor : undefined;
  } while (cursor);
  return items;
}

// Link header: follow rel="next" until there is none.
async function allByLinkHeader(perPage = 25) {
  const items = [];
  let url = `${BASE}/link?per_page=${perPage}`;
  while (url) {
    const res = await fetch(url);
    items.push(...(await res.json()));
    const next = res.headers.get("link")?.match(/<([^>]+)>;\s*rel="next"/);
    url = next?.[1];
  }
  return items;
}

const byCursor = await allByCursor();
const byLink = await allByLinkHeader();
console.log("cursor:", byCursor.length, "items, last id", byCursor.at(-1).id);
console.log("link:  ", byLink.length, "items, last id", byLink.at(-1).id);
Cet exemple utilise des points d’accès réservés aux membres connectés.Se connecterCréer un compte gratuit
Sortie
cursor: 100 items, last id 100
link:   100 items, last id 100

Quel style choisir

  • Offset ou numéro de page quand les utilisateurs sautent à la page N et que les données changent lentement. Une ligne insérée ou supprimée entre deux requêtes décale toutes les pages suivantes, si bien qu’un élément peut être sauté ou apparaître deux fois.
  • Curseur ou keyset pour les fils d’actualité, le défilement infini et les exports, où les données changent pendant qu’on parcourt les pages. Impossible de sauter directement à la page 40.
  • En-tête Link ou HAL quand vous voulez que les clients suivent les URL fournies par le serveur au lieu de construire les leurs. Le serveur peut alors changer de schéma de pagination sans les casser.