TL;DR
Pour les grands ensembles de données, utilisez la pagination basée sur un curseur ou sur un jeu de clés au lieu de la pagination basée sur un décalage. La pagination par décalage (?page=1&limit=20) est peu performante avec des millions d'enregistrements et peut entraîner des incohérences de données. La Modern PetstoreAPI implémente une pagination basée sur un curseur avec des jetons opaques et des liens HATEOAS pour des résultats efficaces et cohérents.
Introduction
Votre API renvoie une liste d'animaux de compagnie. Vous avez 10 millions d'animaux de compagnie dans la base de données. Un client demande GET /pets?page=500000&limit=20. Votre base de données exécute OFFSET 10000000 LIMIT 20. La requête prend 30 secondes. Votre API expire.
C'est le problème de la pagination par décalage. Elle fonctionne bien pour les petits ensembles de données mais échoue à grande échelle. La base de données doit parcourir des millions de lignes pour atteindre le décalage, même si vous ne renvoyez que 20 résultats.
L'ancien Swagger Petstore n'aborde pas du tout la pagination. La Modern PetstoreAPI implémente une pagination basée sur un curseur qui s'adapte à des millions d'enregistrements avec des performances constantes.
Dans ce guide, vous apprendrez pourquoi la pagination par décalage échoue, comment fonctionne la pagination basée sur un curseur et comment la Modern PetstoreAPI implémente une pagination efficace.
Pourquoi la pagination par décalage échoue à grande échelle
La pagination par décalage est l'approche la plus courante, mais elle présente de sérieux problèmes.
Comment fonctionne la pagination par décalage
GET /pets?page=1&limit=20 → OFFSET 0 LIMIT 20
GET /pets?page=2&limit=20 → OFFSET 20 LIMIT 20
GET /pets?page=3&limit=20 → OFFSET 40 LIMIT 20
La base de données ignore les lignes offset et renvoie les lignes limit.
Problème 1 : Les performances se dégradent avec le numéro de page
Page 1 :
SELECT * FROM pets OFFSET 0 LIMIT 20;
-- Rapide : scanne 20 lignes
Page 1000 :
SELECT * FROM pets OFFSET 20000 LIMIT 20;
-- Lent : scanne 20 020 lignes, renvoie 20
Page 500 000 :
SELECT * FROM pets OFFSET 10000000 LIMIT 20;
-- Très lent : scanne 10 000 020 lignes, renvoie 20
La base de données doit scanner toutes les lignes jusqu'au décalage, même si elle les ignore. Les performances se dégradent linéairement avec le numéro de page.
Problème 2 : Résultats incohérents
Pendant qu'un client parcourt les résultats, les données changent :
Requête 1 :
GET /pets?page=1&limit=2
Renvoie : [Animal A, Animal B]
Quelqu'un ajoute l'Animal Z (se classe en premier par ordre alphabétique)
Requête 2 :
GET /pets?page=2&limit=2
Renvoie : [Animal B, Animal C] ← L'Animal B apparaît deux fois !
L'Animal B est apparu sur les deux pages parce qu'un nouvel animal a été inséré. Inversement, des animaux peuvent être ignorés si des suppressions ont lieu.
Problème 3 : La pagination profonde est coûteuse
Les utilisateurs dépassent rarement la page 10. Mais si votre API autorise ?page=1000000, vous devez la gérer. Les requêtes de pagination profonde sont coûteuses et peuvent être utilisées pour des attaques par déni de service.
Quand la pagination par décalage est acceptable
La pagination par décalage convient pour :
- Petits ensembles de données (< 10 000 enregistrements)
- API internes avec une utilisation contrôlée
- Interfaces d'administration où les utilisateurs n'effectueront pas de pagination profonde
- Données qui changent peu fréquemment
Pour les API publiques ou les grands ensembles de données, utilisez la pagination basée sur un curseur.
Explication de la pagination basée sur un curseur
La pagination basée sur un curseur utilise un jeton opaque pour marquer la position dans l'ensemble de résultats.
Comment ça marche
Requête 1 :
GET /pets?limit=20
Réponse 1 :
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9",
"hasMore": true
}
}
Requête 2 :
GET /pets?cursor=eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9&limit=20
Le curseur est un jeton opaque (généralement encodé en base64) qui encode la position. Le client ne le parse pas, il le renvoie simplement.
Avantages
1. Performances constantes
La base de données utilise un index pour trouver directement la position du curseur :
SELECT * FROM pets
WHERE id > '019b4132-70aa-764f-b315-e2803d882a24'
ORDER BY id
LIMIT 20;
Cette requête est rapide quelle que soit la position dans l'ensemble de données. Elle utilise une recherche d'index, pas un balayage.
2. Résultats cohérents
Les curseurs sont stables. Si les données changent entre les requêtes, vous obtenez toujours des résultats cohérents. Les nouveaux enregistrements ne provoquent pas de doublons ni d'omissions.
3. Pas d'attaques de pagination profonde
Les clients ne peuvent pas sauter à des positions arbitraires. Ils doivent paginer séquentiellement, ce qui limite les abus.
Format du curseur
Les curseurs sont généralement des JSON encodés en base64 :
// Curseur décodé
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"createdAt": "2026-03-13T10:30:00Z"
}
Le curseur contient suffisamment d'informations pour reprendre la pagination. Pour la Modern PetstoreAPI, cela inclut l'ID de la ressource et le champ de tri.
Pagination par jeu de clés pour les données triées
La pagination par jeu de clés est une variante de la pagination basée sur un curseur pour les données triées.
Comment ça marche
Au lieu d'un curseur opaque, vous utilisez la dernière valeur de la page précédente :
Requête 1 :
GET /pets?limit=20&sortBy=createdAt
Réponse 1 :
{
"data": [
{"id": "...", "createdAt": "2026-03-13T10:00:00Z"},
...
{"id": "...", "createdAt": "2026-03-13T10:30:00Z"}
]
}
Requête 2 :
GET /pets?limit=20&sortBy=createdAt&after=2026-03-13T10:30:00Z
Le paramètre after utilise la dernière valeur de createdAt de la page précédente.
Requête SQL
SELECT * FROM pets
WHERE created_at > '2026-03-13T10:30:00Z'
ORDER BY created_at
LIMIT 20;
Ceci est efficace car cela utilise un index sur created_at.
Quand utiliser la pagination par jeu de clés
- Les données sont naturellement triées (par horodatage, ID, etc.)
- Les clients doivent comprendre la clé de pagination
- Vous voulez une pagination transparente (pas de curseurs opaques)
La Modern PetstoreAPI utilise la pagination basée sur un curseur par défaut mais prend en charge la pagination par jeu de clés pour les données de séries temporelles.
Comment la Modern PetstoreAPI implémente la pagination
La Modern PetstoreAPI utilise la pagination basée sur un curseur avec des liens HATEOAS.
Format de la requête
GET /pets?limit=20
GET /pets?cursor={token}&limit=20
Paramètres :
limit- Nombre de résultats par page (par défaut : 20, max : 100)cursor- Jeton de pagination opaque de la réponse précédente
Format de la réponse
{
"data": [
{
"id": "019b4132-70aa-764f-b315-e2803d882a24",
"name": "Fluffy",
"species": "CAT"
}
],
"pagination": {
"limit": 20,
"hasMore": true,
"nextCursor": "eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9"
},
"links": {
"self": "https://petstoreapi.com/pets?limit=20",
"next": "https://petstoreapi.com/pets?cursor=eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9&limit=20"
}
}
Fonctionnalités clés
1. Curseurs opaques
Les curseurs sont encodés en base64. Les clients ne les analysent pas.
2. Liens HATEOAS
L'objet links fournit des URL prêtes à l'emploi. Les clients n'ont pas besoin de construire les URL de pagination.
3. Indicateur hasMore
Indique si d'autres résultats existent. Les clients savent quand arrêter de paginer.
4. Validation de la limite
La limite maximale est de 100. Empêche les clients de demander des pages trop volumineuses.
Consultez la documentation de la pagination de la Modern PetstoreAPI pour des détails complets.
Format de la réponse de pagination
La Modern PetstoreAPI encapsule les réponses paginées dans une structure cohérente.
Enveloppe de collection
{
"data": [...],
"pagination": {...},
"links": {...}
}
Pourquoi envelopper les collections ?
- Extensibilité - Peut ajouter des métadonnées sans casser les clients
- Cohérence - Tous les points de terminaison paginés utilisent le même format
- HATEOAS - Les liens guident les clients à travers la pagination
Métadonnées de pagination
"pagination": {
"limit": 20,
"hasMore": true,
"nextCursor": "...",
"totalCount": 1000 // Optionnel, coûteux à calculer
}
totalCount est optionnel car son calcul est coûteux pour les grands ensembles de données. La plupart des clients n'en ont pas besoin.
Tester la pagination avec Apidog
Apidog vous aide à tester le comportement de la pagination de manière exhaustive.
Scénarios de test
1. Première page
GET /pets?limit=20
Attendre : 20 résultats, hasMore=true, nextCursor présent
2. Pages suivantes
GET /pets?cursor={token}&limit=20
Attendre : 20 résultats, hasMore=true/false, nextCursor présent/absent
3. Dernière page
GET /pets?cursor={lastToken}&limit=20
Attendre : < 20 résultats, hasMore=false, pas de nextCursor
4. Résultats vides
GET /pets?status=NONEXISTENT&limit=20
Attendre : 0 résultats, hasMore=false, pas de nextCursor
5. Validation de la limite
GET /pets?limit=1000
Attendre : 400 Bad Request (dépasse la limite maximale)
Configuration de test Apidog
// Test : Structure de pagination
pm.test("Response has pagination", () => {
pm.expect(pm.response.json()).to.have.property('pagination');
pm.expect(pm.response.json().pagination).to.have.property('hasMore');
});
// Test : Liens HATEOAS
pm.test("Response has links", () => {
const links = pm.response.json().links;
pm.expect(links).to.have.property('self');
if (pm.response.json().pagination.hasMore) {
pm.expect(links).to.have.property('next');
}
});
Choisir la bonne stratégie de pagination
Différentes stratégies conviennent à différents cas d'utilisation.
Pagination par décalage
Utiliser quand :
- L'ensemble de données est petit (< 10 000 enregistrements)
- Les utilisateurs ont besoin d'un accès aléatoire (sauter à la page 50)
- Les données changent peu fréquemment
- API interne avec une utilisation contrôlée
Ne pas utiliser quand :
- L'ensemble de données est grand (> 100 000 enregistrements)
- La performance est importante
- Les données changent fréquemment
Pagination basée sur un curseur
Utiliser quand :
- L'ensemble de données est grand
- La performance est importante
- Les données changent fréquemment
- L'accès séquentiel est suffisant
Ne pas utiliser quand :
- Les utilisateurs ont besoin d'un accès aléatoire
- La complexité du curseur est une préoccupation
Pagination par jeu de clés
Utiliser quand :
- Les données sont naturellement triées
- La pagination transparente est préférée
- La performance est importante
Ne pas utiliser quand :
- L'ordre de tri est complexe
- Plusieurs champs de tri sont nécessaires
Recommandation de la Modern PetstoreAPI : Utilisez la pagination basée sur un curseur pour les API publiques et les grands ensembles de données.
Conclusion
La pagination est essentielle pour les API qui renvoient de grands ensembles de données. La pagination par décalage est simple mais ne s'adapte pas. La pagination basée sur un curseur offre des performances constantes et des résultats fiables pour des millions d'enregistrements.
La Modern PetstoreAPI implémente une pagination basée sur un curseur avec des jetons opaques, des liens HATEOAS et des métadonnées appropriées. Cette conception s'adapte efficacement et offre une excellente expérience aux développeurs.
Testez votre implémentation de pagination avec Apidog pour vous assurer qu'elle gère les cas limites, valide les limites et renvoie des résultats cohérents.
Points clés à retenir :
- Évitez la pagination par décalage pour les grands ensembles de données
- Utilisez la pagination basée sur un curseur pour l'évolutivité
- Enveloppez les collections avec des métadonnées et des liens
- Testez la pagination en profondeur avec Apidog
- Suivez les modèles de pagination de la Modern PetstoreAPI
FAQ
Pourquoi ne pas simplement renvoyer tous les résultats sans pagination ?
Renvoyer des millions d'enregistrements en une seule réponse provoque des problèmes de mémoire, un transfert réseau lent et une mauvaise expérience utilisateur. La pagination est essentielle pour les grands ensembles de données.
Les clients peuvent-ils sauter à une page spécifique avec la pagination par curseur ?
Non, la pagination par curseur nécessite un accès séquentiel. Si un accès aléatoire est nécessaire, envisagez la pagination par décalage pour les petits ensembles de données ou implémentez la recherche/le filtrage à la place.
Comment gérer la pagination avec le filtrage ?
Incluez les paramètres de filtre dans les requêtes de pagination : GET /pets?status=AVAILABLE&cursor={token}&limit=20. Le curseur encode à la fois la position et l'état du filtre.
Dois-je inclure le nombre total dans les réponses de pagination ?
Seulement si les clients en ont besoin et si votre ensemble de données est petit. Le calcul du nombre total est coûteux pour les grands ensembles de données (nécessite une requête COUNT séparée).
Comment implémenter la pagination par curseur en SQL ?
Utilisez une clause WHERE avec la valeur du curseur : SELECT * FROM pets WHERE id > ? ORDER BY id LIMIT 20. Assurez-vous d'avoir un index sur la colonne de tri.
Que se passe-t-il si mes jetons de curseur deviennent invalides ?
Renvoie 400 Bad Request avec un message d'erreur. Les curseurs peuvent devenir invalides si des données sont supprimées ou si l'état de la pagination expire.
Combien de temps les curseurs doivent-ils rester valides ?
Les curseurs de la Modern PetstoreAPI sont valides indéfiniment tant que la ressource référencée existe. Certaines API expirent les curseurs après 24 heures.
Puis-je utiliser la pagination par curseur avec plusieurs champs de tri ?
Oui, mais le curseur doit encoder tous les champs de tri. Cela rend les curseurs plus complexes. Envisagez d'utiliser une seule clé de tri composite à la place.
