APIs

Pagination d'API : Cursor vs offset, bonnes pratiques

Découvrez les différences entre pagination par offset et curseur pour vos API : avantages, inconvénients, exemples de code et bonnes pratiques.

11 Mar 2026 15 min de lecture 3 vues
3

Lectures

15

Minutes

0

Partages

Introduction

En 2024, la gestion des grandes quantités de données par les API est cruciale pour toute application web moderne. Une étude récente montre que 70 % des développeurs rencontrent des problèmes de performance directement liés à la pagination. Avec l'augmentation exponentielle des volumes de données et du nombre d'utilisateurs simultanés, il est essentiel de comprendre comment optimiser la pagination pour offrir une expérience fluide et réactive.

La pagination est le mécanisme qui permet de diviser un ensemble volumineux de résultats en pages plus petites et plus faciles à consommer. Sans elle, une API qui retourne des millions d'enregistrements en une seule réponse saturerait la mémoire du serveur, congestionnerait le réseau et rendrait l'interface client inutilisable. Deux approches dominent le paysage : la pagination par offset et la pagination par curseur.

Cet article explore en profondeur les différences entre ces deux méthodes, leurs avantages et inconvénients respectifs, ainsi que les bonnes pratiques à adopter pour chaque cas d'usage. Vous apprendrez à les implémenter efficacement avec des exemples concrets en SQL, PHP, Python et JavaScript, et vous découvrirez comment éviter les erreurs courantes qui dégradent la performance de vos API.

Comprendre la pagination par offset

La pagination par offset est la technique la plus ancienne et la plus couramment utilisée. Son principe est simple : on spécifie un décalage (offset) correspondant au nombre d'éléments à ignorer, puis on retourne un nombre fixe d'éléments à partir de ce point. Cette approche est intuitive car elle correspond directement à la notion de numéro de page.

Dans une requête HTTP typique, les paramètres ressemblent à ceci : GET /api/articles?page=3&limit=10. Le serveur calcule alors l'offset comme (page - 1) * limit, soit 20 dans cet exemple, puis récupère les 10 éléments suivants.

Exemple de code SQL avec offset

-- Récupérer la page 3 avec 10 articles par page
-- Le moteur de base de données doit parcourir les 20 premiers enregistrements avant de retourner les résultats
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
LIMIT 10 OFFSET 20;

Ce code récupère 10 articles en sautant les 20 premiers, triés par date de création décroissante. Bien que cette méthode soit facile à implémenter, elle présente des inconvénients majeurs en termes de performance, notamment pour les tables volumineuses.

Implémentation en Python avec Flask

# Endpoint de pagination par offset avec Flask
from flask import Flask, request, jsonify
import sqlite3

app = Flask(__name__)

@app.route('/api/articles', methods=['GET'])
def get_articles():
    # Récupérer les paramètres de pagination avec des valeurs par défaut
    page = request.args.get('page', 1, type=int)
    limit = request.args.get('limit', 10, type=int)
    
    # Sécuriser la taille de page maximale pour éviter les abus
    limit = min(limit, 100)
    offset = (page - 1) * limit
    
    conn = sqlite3.connect('database.db')
    cursor = conn.cursor()
    
    # Requête avec OFFSET et LIMIT
    cursor.execute(
        'SELECT id, title, created_at FROM articles ORDER BY created_at DESC LIMIT ? OFFSET ?',
        (limit, offset)
    )
    articles = cursor.fetchall()
    
    # Compter le total pour calculer le nombre de pages
    cursor.execute('SELECT COUNT(*) FROM articles')
    total = cursor.fetchone()[0]
    
    return jsonify({
        'data': articles,
        'page': page,
        'limit': limit,
        'total': total,
        'total_pages': (total + limit - 1) // limit
    })

Cet exemple montre une implémentation complète incluant le calcul du nombre total de pages, ce qui est utile pour afficher une barre de navigation dans l'interface utilisateur.

Limitations de la pagination par offset

L'une des principales limites de l'offset est qu'il effectue une lecture complète jusqu'à l'index spécifié. Concrètement, pour afficher la page 1000 avec 10 éléments par page, la base de données doit parcourir 10 000 enregistrements avant de retourner les 10 résultats demandés. Ce comportement entraîne une dégradation linéaire des performances à mesure que le numéro de page augmente.

De plus, la pagination par offset peut entraîner des incohérences lorsque les données changent entre les requêtes. Si un nouvel article est inséré pendant qu'un utilisateur navigue entre les pages, certains éléments peuvent apparaître en double ou être complètement omis. Ce problème est particulièrement critique pour les applications en temps réel comme les fils d'actualité ou les tableaux de bord.

"La pagination par offset est simple à comprendre et à implémenter, mais elle devient coûteuse en performance dès que le dataset dépasse quelques dizaines de milliers d'enregistrements. Pour les tables de plusieurs millions de lignes, elle peut rendre l'API inutilisable."

Pagination par curseur : une alternative performante

La pagination par curseur, aussi appelée keyset pagination ou seek method, résout les principaux problèmes de l'offset en utilisant un pointeur opaque pour indiquer la position actuelle dans la liste des résultats. Au lieu de sauter un nombre fixe d'enregistrements, elle continue à partir du dernier enregistrement récupéré grâce à une condition WHERE.

Le principe est le suivant : chaque réponse de l'API inclut un curseur (généralement l'identifiant ou un horodatage encodé du dernier élément). Le client transmet ce curseur dans la requête suivante pour obtenir les éléments qui suivent. La requête HTTP ressemble à : GET /api/articles?cursor=eyJpZCI6NDJ9&limit=10.

Exemple de code SQL avec curseur

-- Pagination par curseur : récupérer les 10 articles suivants après l'ID 42
-- Cette requête utilise directement l'index primaire, sans parcourir les enregistrements précédents
SELECT id, title, created_at
FROM articles
WHERE id > 42
ORDER BY id ASC
LIMIT 10;

-- Variante avec tri par date (curseur composite)
-- Nécessite un index composite sur (created_at, id)
SELECT id, title, created_at
FROM articles
WHERE (created_at, id) < ('2024-01-15 10:30:00', 42)
ORDER BY created_at DESC, id DESC
LIMIT 10;

La première requête est la forme la plus simple du curseur. La seconde montre comment gérer un tri par date avec un curseur composite pour éviter les problèmes lorsque plusieurs enregistrements partagent le même horodatage.

Exemple de code en PHP avec PDO

// Implémentation complète de la pagination par curseur en PHP
function getArticlesWithCursor(PDO $pdo, ?string $encodedCursor, int $limit = 10): array
{
    // Décoder le curseur (base64 contenant l'ID du dernier élément)
    $lastId = 0;
    if ($encodedCursor !== null) {
        $decoded = json_decode(base64_decode($encodedCursor), true);
        $lastId = (int) ($decoded['id'] ?? 0);
    }

    // Requête avec condition WHERE au lieu d'OFFSET
    $query = "SELECT id, title, created_at FROM articles WHERE id > :cursor ORDER BY id ASC LIMIT :limit";
    $stmt = $pdo->prepare($query);
    $stmt->bindValue(':cursor', $lastId, PDO::PARAM_INT);
    $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
    $stmt->execute();

    $articles = $stmt->fetchAll(PDO::FETCH_ASSOC);

    // Générer le curseur suivant à partir du dernier élément
    $nextCursor = null;
    if (count($articles) === $limit) {
        $lastArticle = end($articles);
        $nextCursor = base64_encode(json_encode(['id' => $lastArticle['id']]));
    }

    return [
        'data' => $articles,
        'next_cursor' => $nextCursor,
        'has_more' => $nextCursor !== null
    ];
}

Ce code montre une implémentation production-ready avec encodage du curseur en base64, gestion du premier appel sans curseur, et détection automatique de la fin des résultats.

Exemple en JavaScript avec Node.js et Express

// Endpoint de pagination par curseur avec Express et MySQL
const express = require('express');
const mysql = require('mysql2/promise');
const app = express();

app.get('/api/articles', async (req, res) => {
    const limit = Math.min(parseInt(req.query.limit) || 10, 100);
    const cursor = req.query.cursor || null;

    let query, params;

    if (cursor) {
        // Décoder le curseur et récupérer les éléments suivants
        const decoded = JSON.parse(Buffer.from(cursor, 'base64').toString());
        query = 'SELECT id, title, created_at FROM articles WHERE id > ? ORDER BY id ASC LIMIT ?';
        params = [decoded.id, limit];
    } else {
        // Première page : pas de curseur
        query = 'SELECT id, title, created_at FROM articles ORDER BY id ASC LIMIT ?';
        params = [limit];
    }

    const [rows] = await pool.execute(query, params);

    // Construire le curseur pour la page suivante
    const nextCursor = rows.length === limit
        ? Buffer.from(JSON.stringify({ id: rows[rows.length - 1].id })).toString('base64')
        : null;

    res.json({
        data: rows,
        pagination: {
            next_cursor: nextCursor,
            has_more: nextCursor !== null,
            limit: limit
        }
    });
});

Avantages de la pagination par curseur

  • Performance constante : le temps de réponse reste identique quelle que soit la profondeur de la pagination, car la requête utilise toujours un index.
  • Cohérence des données : les insertions ou suppressions entre deux requêtes n'entraînent ni doublons ni éléments manquants.
  • Scalabilité : idéale pour les datasets de plusieurs millions d'enregistrements sans dégradation.
  • Compatible avec le défilement infini : parfaitement adaptée aux interfaces de type scroll infini sur mobile et web.

Inconvénients de la pagination par curseur

  • Pas d'accès direct à une page : impossible de sauter directement à la page 50 sans parcourir les pages précédentes.
  • Complexité accrue : l'implémentation du curseur, surtout composite, demande plus de code et de tests.
  • Nombre total inconnu : il est difficile d'afficher le nombre total de pages sans une requête COUNT supplémentaire.

Comparaison détaillée des deux méthodes

Le choix entre offset et curseur dépend du contexte d'utilisation. Voici un tableau comparatif complet pour vous aider à prendre la bonne décision selon vos contraintes techniques et fonctionnelles.

CritèreOffsetCurseur
Performance sur petits datasetsExcellenteExcellente
Performance sur grands datasetsSe dégrade avec la profondeurConstante et optimisée
Simplicité d'implémentationTrès simpleModérément complexe
Accès direct à une pageOui, natifNon, séquentiel uniquement
Cohérence des donnéesRisque de doublons ou d'omissionsCohérence garantie
Nombre total de résultatsFacile à calculerRequête supplémentaire nécessaire
Défilement infiniPeu adaptéParfaitement adapté
Compatibilité RESTStandard et bien documentéNécessite une convention de curseur
Cas d'usage idéalBack-office, petites tablesAPI publiques, gros volumes

Quand choisir l'offset

La pagination par offset reste pertinente dans plusieurs scénarios. Si votre table contient moins de 100 000 enregistrements, la différence de performance avec le curseur est négligeable. Elle est également préférable lorsque votre interface nécessite une navigation par numéro de page, comme dans un back-office ou un panneau d'administration où les utilisateurs souhaitent accéder directement à une page spécifique.

Quand choisir le curseur

La pagination par curseur s'impose dès que vous travaillez avec des datasets volumineux (plus de 100 000 enregistrements), des API publiques à fort trafic, ou des interfaces de type défilement infini. Les géants du web comme Twitter, Facebook et Slack utilisent exclusivement la pagination par curseur pour leurs API publiques.

Bonnes pratiques pour implémenter la pagination

Pour garantir une pagination efficace quelle que soit la méthode choisie, il est essentiel de suivre des bonnes pratiques éprouvées. Ces recommandations s'appliquent aussi bien à l'offset qu'au curseur et couvrent les aspects performance, sécurité et expérience développeur.

Indexer les colonnes de pagination

Les index permettent d'accélérer considérablement l'accès aux données, réduisant le temps de traitement des requêtes paginées de plusieurs ordres de grandeur. Assurez-vous que les colonnes utilisées pour le tri et le filtrage de la pagination sont correctement indexées.

-- Créer un index simple pour la pagination par ID
CREATE INDEX idx_articles_id ON articles(id);

-- Index composite pour la pagination par date avec curseur
-- L'ordre des colonnes dans l'index doit correspondre à l'ORDER BY
CREATE INDEX idx_articles_created_id ON articles(created_at DESC, id DESC);

-- Vérifier que l'index est bien utilisé avec EXPLAIN
EXPLAIN SELECT id, title, created_at
FROM articles
WHERE id > 42
ORDER BY id ASC
LIMIT 10;

Utilisez systématiquement EXPLAIN pour vérifier que vos requêtes paginées exploitent bien les index et ne déclenchent pas de full table scan.

Limiter et sécuriser la taille des résultats

Il est conseillé de limiter le nombre d'enregistrements retournés par requête pour éviter une surcharge du serveur et améliorer la vitesse de réponse. Définissez toujours une valeur maximale côté serveur, même si le client demande davantage.

// Sécuriser les paramètres de pagination côté serveur
function sanitizePaginationParams(array $params): array
{
    $limit = (int) ($params['limit'] ?? 20);
    $page = (int) ($params['page'] ?? 1);

    // Empêcher les valeurs négatives ou excessives
    $limit = max(1, min($limit, 100)); // Entre 1 et 100
    $page = max(1, $page);             // Minimum 1

    return [
        'limit' => $limit,
        'page' => $page,
        'offset' => ($page - 1) * $limit
    ];
}

Inclure des métadonnées de pagination dans la réponse

Une API bien conçue inclut toujours des métadonnées permettant au client de naviguer facilement. Pour l'offset, retournez le numéro de page actuel, le total de pages et le nombre total d'éléments. Pour le curseur, retournez le curseur suivant et un indicateur has_more.

// Structure de réponse recommandée pour la pagination par offset
{
    "data": [...],
    "pagination": {
        "current_page": 3,
        "per_page": 10,
        "total": 2456,
        "total_pages": 246,
        "links": {
            "first": "/api/articles?page=1&limit=10",
            "prev": "/api/articles?page=2&limit=10",
            "next": "/api/articles?page=4&limit=10",
            "last": "/api/articles?page=246&limit=10"
        }
    }
}

// Structure de réponse recommandée pour la pagination par curseur
{
    "data": [...],
    "pagination": {
        "next_cursor": "eyJpZCI6NTJ9",
        "has_more": true,
        "limit": 10
    }
}

Utiliser les en-têtes HTTP Link pour le standard REST

Pour les API REST conformes aux standards, incluez les liens de pagination dans les en-têtes HTTP Link en suivant la RFC 8288. Cette pratique est utilisée par l'API GitHub et facilite la navigation automatique par les clients.

Pièges et erreurs courantes à éviter

Même les développeurs expérimentés commettent des erreurs lors de l'implémentation de la pagination. Voici les pièges les plus fréquents et comment les éviter pour garantir la fiabilité et la performance de votre API.

Erreur 1 : ne pas indexer les colonnes de tri

C'est l'erreur la plus coûteuse. Sans index, chaque requête paginée déclenche un parcours complet de la table. Sur une table de un million de lignes, la différence entre une requête indexée et non indexée peut passer de 2 millisecondes à plus de 5 secondes.

Erreur 2 : utiliser une taille de page trop grande

Retourner 1 000 éléments par page semble efficace pour réduire le nombre de requêtes, mais cela augmente le temps de traitement, la consommation mémoire et la taille de la réponse HTTP. Une taille de 10 à 50 éléments par page est généralement optimale.

Erreur 3 : ignorer la cohérence des données avec l'offset

Si votre application insère ou supprime fréquemment des enregistrements, la pagination par offset produira des résultats incohérents. Des éléments apparaîtront en double ou seront omis. Si vous devez absolument utiliser l'offset dans ce contexte, ajoutez un filtre temporel pour figer l'ensemble de données.

Erreur 4 : exposer des curseurs non sécurisés

Un curseur qui expose directement un identifiant numérique brut (?cursor=42) permet à un utilisateur malveillant de deviner et manipuler les identifiants. Encodez toujours vos curseurs en base64 ou utilisez un token opaque pour masquer la structure interne.

Erreur 5 : oublier de gérer la dernière page

Assurez-vous que votre API gère correctement le cas où il n'y a plus de résultats. Retournez un tableau vide avec has_more: false ou next_cursor: null plutôt qu'une erreur 404.

  • Toujours valider et limiter les paramètres de pagination côté serveur.
  • Tester les performances avec des données réalistes en volume.
  • Documenter clairement le format du curseur dans la documentation de l'API.
  • Prévoir un mécanisme de cache pour les requêtes paginées les plus fréquentes.
  • Monitorer les temps de réponse des endpoints paginés en production.

Conclusion

La pagination est une composante essentielle de la gestion des API modernes, particulièrement face à l'augmentation constante des volumes de données. Choisir entre offset et curseur dépend des besoins spécifiques de votre application et de ses contraintes de performance. L'offset reste adapté aux back-offices et aux petits datasets, tandis que le curseur s'impose pour les API publiques et les grands volumes.

L'essentiel est de ne jamais négliger l'impact de la pagination sur la performance globale de votre système. Une pagination mal implémentée peut transformer une API rapide en un goulot d'étranglement critique. Inversement, une pagination bien pensée et correctement indexée garantit une expérience utilisateur fluide même avec des millions d'enregistrements.

  • La pagination par offset est simple mais peut être inefficace sur les grands datasets.
  • Les curseurs offrent une performance constante pour les grands ensembles de données.
  • Indexer les colonnes de pagination est absolument crucial.
  • Limiter la taille des pages améliore la réactivité de l'application.
  • Testez toujours les performances avec des volumes de données réalistes.
  • Sécurisez les paramètres de pagination pour éviter les abus.
  • Incluez des métadonnées de pagination claires dans chaque réponse.
  • Adaptez votre approche en fonction de l'évolution des données et des utilisateurs.

En appliquant ces principes et en choisissant la méthode adaptée à votre contexte, vous optimiserez vos API pour une performance maximale et une expérience développeur exemplaire.

Tags

API Performance Pagination

Partagez cet article

Twitter Facebook LinkedIn
JY
Jordane YENO

Developpeur Full Stack passionne par le web et les nouvelles technologies

En savoir plus

Articles similaires