API REST back-end : filtrage, tri et pagination des collections

Capítulo 4

Temps de lecture estimé : 8 minutes

+ Exercice

Objectif : des collections flexibles sans complexifier l’API

Quand un endpoint retourne une collection (ex. GET /users, GET /orders), les clients ont besoin de contrôler le volume et l’ordre des résultats : filtrer (par statut, date, catégorie), rechercher (texte libre), trier, choisir les champs utiles, et paginer. L’enjeu est de proposer une grammaire de paramètres de requête cohérente, facile à valider côté serveur, et sûre (éviter les requêtes trop coûteuses).

Conception des paramètres de requête

1) Filtrage : des paramètres explicites et composables

Le filtrage consiste à restreindre la collection à des éléments répondant à des critères. La forme la plus simple et la plus lisible est d’utiliser des paramètres nommés : ?status=active, ?role=admin, ?country=FR.

  • Égalité : ?status=active
  • Multi-valeurs (choisir une convention) : ?status=active,inactive ou ?status=active&status=inactive
  • Plages (dates, nombres) : ?createdAt[gte]=2026-01-01&createdAt[lt]=2026-02-01 ou ?minCreatedAt=...&maxCreatedAt=...
  • Booléens : ?isVerified=true

Recommandation pratique : adoptez une convention unique pour les opérateurs (par exemple [gte], [lte], [in]) et documentez la liste des champs filtrables. Évitez d’accepter des filtres arbitraires sur n’importe quel champ : cela complique la validation et peut provoquer des scans coûteux.

2) Recherche : q= pour le texte libre

La recherche texte libre se fait généralement via un paramètre dédié : ?q=alice. Contrairement au filtrage, q implique souvent une logique spécifique (full-text, préfixe, tolérance aux accents, etc.).

  • Exemple : GET /users?q=alice (cherche dans name, email… selon votre définition)
  • Bonnes pratiques : limiter la longueur de q, normaliser (trim), refuser les caractères de contrôle, et définir clairement les champs couverts.

3) Tri : sort=createdAt,-name

Le tri contrôle l’ordre des résultats. Une convention courante est une liste de champs séparés par des virgules, avec un préfixe - pour le décroissant.

Continuez dans notre application.
  • Écoutez le fichier audio avec l'écran éteint.
  • Obtenez un certificat à la fin du programme.
  • Plus de 5000 cours à découvrir !
Ou poursuivez votre lecture ci-dessous...
Download App

Téléchargez l'application

  • Exemple : GET /users?sort=createdAt,-name (croissant sur createdAt, puis décroissant sur name)
  • Règle : n’autoriser que des champs triables explicitement listés (whitelist).
  • Stabilité : ajoutez un champ de tie-breaker (souvent id) si le tri principal n’est pas unique, surtout pour la pagination.

4) Sélection de champs : fields= pour réduire la charge

La sélection de champs permet au client de ne récupérer que ce dont il a besoin, réduisant la taille de réponse et parfois le coût de sérialisation.

  • Exemple : GET /users?fields=id,name,createdAt
  • Variante : autoriser une exclusion fields=-passwordHash (souvent plus risqué à maintenir).
  • Bonnes pratiques : définir un set de champs par défaut, refuser les champs sensibles, et gérer les champs calculés (ex. stats) via une liste autorisée.

Pagination : offset/limit vs curseur

Pagination offset/limit

Principe : le client demande une page via un décalage et une taille.

  • Paramètres : page et pageSize (ou offset et limit)
  • Exemple : GET /users?page=3&pageSize=20 (ou ?offset=40&limit=20)
CritèreOffset/limit
SimplicitéTrès simple à comprendre
Accès direct à une pageOui (page 10, etc.)
Stabilité si données changentPeut provoquer des doublons/éléments manqués si insertions/suppressions
Performance à grande profondeurPeut devenir coûteux pour des offsets élevés

Quand l’utiliser : collections modestes, besoins d’accès direct à une page, et résultats relativement stables (ou acceptation d’une légère incohérence).

Pagination par curseur (cursor-based)

Principe : au lieu de dire “donne-moi la page N”, on dit “donne-moi les éléments après celui-ci”, via un curseur (souvent basé sur la valeur de tri + un identifiant).

  • Paramètres : pageSize + cursor (ou after)
  • Exemple : GET /users?sort=createdAt,id&pageSize=20&cursor=eyJjcmVhdGVkQXQiOiIyMDI2LTAxLTE1VDEwOjAwOjAwWiIsImlkIjoiMTIzIn0=
CritèreCurseur
SimplicitéPlus complexe (curseur opaque)
Accès direct à une pageNon (navigation séquentielle)
Stabilité si données changentMeilleure stabilité (si tri déterministe)
Performance à grande profondeurSouvent meilleure (évite les grands offsets)

Quand l’utiliser : gros volumes, scroll infini, flux d’activité, et besoin de résultats stables malgré les insertions.

Choisir selon volumétrie et stabilité

  • Volumétrie faible à moyenne + besoin de “page 12” : offset/limit.
  • Volumétrie élevée + navigation séquentielle + données qui changent : curseur.
  • Tri non unique : dans les deux cas, rendez le tri déterministe (ex. sort=createdAt,id).

Définir un format de réponse paginée

Option A : métadonnées page/pageSize/total (offset/limit)

Format typique : inclure les éléments et un objet meta décrivant la pagination. Ajouter des liens de navigation facilite l’usage côté client.

{
  "data": [
    { "id": "u1", "name": "Alice" },
    { "id": "u2", "name": "Bob" }
  ],
  "meta": {
    "page": 3,
    "pageSize": 20,
    "total": 241,
    "totalPages": 13
  },
  "links": {
    "self": "/users?page=3&pageSize=20",
    "next": "/users?page=4&pageSize=20",
    "prev": "/users?page=2&pageSize=20"
  }
}

Notes pratiques : total peut être coûteux à calculer sur de très grandes tables. Si c’est un problème, rendez total optionnel (ex. seulement si includeTotal=true) ou remplacez-le par un indicateur approximatif/absent.

Option B : curseurs (cursor-based)

Le serveur renvoie un nextCursor (et éventuellement prevCursor) que le client réutilise.

{
  "data": [
    { "id": "u120", "name": "Chloé", "createdAt": "2026-01-15T10:00:00Z" },
    { "id": "u121", "name": "David", "createdAt": "2026-01-15T10:02:00Z" }
  ],
  "meta": {
    "pageSize": 20,
    "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTAxLTE1VDEwOjAyOjAwWiIsImlkIjoidTEyMSJ9"
  },
  "links": {
    "self": "/users?sort=createdAt,id&pageSize=20&cursor=...",
    "next": "/users?sort=createdAt,id&pageSize=20&cursor=eyJjcmVhdGVkQXQiOiIyMDI2LTAxLTE1VDEwOjAyOjAwWiIsImlkIjoidTEyMSJ9"
  }
}

Recommandation : utilisez un curseur opaque (ex. base64 d’un JSON signé) plutôt qu’un curseur lisible, pour éviter la dépendance des clients à votre implémentation et limiter la manipulation.

Valeurs par défaut, limites maximales et validation

Définir des valeurs par défaut cohérentes

  • pageSize par défaut : ex. 20 ou 50 selon l’usage.
  • Tri par défaut : ex. sort=-createdAt,id pour renvoyer les plus récents avec un tie-breaker.
  • fields par défaut : un sous-ensemble “safe” et utile (éviter les champs lourds).

Imposer des limites maximales (anti-abus)

Sans garde-fous, un client peut demander pageSize=100000 ou trier/filtrer sur des champs non indexés, ce qui dégrade le service.

  • pageSize max : ex. 100 (ou 200) ; au-delà, soit vous clamp (ramenez à max), soit vous rejetez.
  • Profondeur max (offset) : ex. refuser offset > 10000 si la base ne suit pas.
  • Champs autorisés : whitelist pour sort, fields, filtres.
  • Complexité max : limiter le nombre de critères (ex. max 5 filtres, max 3 champs de tri).

Validation des paramètres : approche étape par étape

Objectif : transformer la query string en une structure interne sûre (ex. filter, sort, projection, pagination), en rejetant ce qui est invalide.

  1. Parser : lire status, q, sort, fields, page/pageSize ou cursor.
  2. Normaliser : trim, convertir types (int, bool, date), appliquer valeurs par défaut.
  3. Valider : vérifier formats (date ISO), bornes (pageSize), et appartenance à une whitelist.
  4. Construire la requête : générer un plan de requête (ex. clauses WHERE, ORDER BY, projection) uniquement à partir des éléments validés.
  5. Exécuter avec garde-fous : timeouts, limites, index attendus, et logs.

Prévenir les requêtes coûteuses

  • Whitelists strictes : pas de tri sur un champ non prévu, pas de filtre libre sur n’importe quelle colonne.
  • Indexation : aligner les champs filtrables/triables avec des index (ou refuser certaines combinaisons).
  • Recherche q : si elle est coûteuse, imposez des règles (longueur min, pas de wildcard initial), ou routez vers un moteur adapté.
  • Champs lourds : forcer fields à exclure par défaut les blobs/JSON volumineux, ou les exposer via un endpoint dédié.
  • Cache : les requêtes de listing fréquentes (mêmes filtres/tri) peuvent être mises en cache côté serveur ou via un reverse proxy, surtout si elles sont publiques.

Exemple complet : grammaire de requête recommandée

Exemple de requête combinant filtrage, recherche, tri, sélection de champs et pagination :

GET /users?status=active&q=ali&sort=-createdAt,id&fields=id,name,createdAt&page=1&pageSize=20

Règles de traitement côté serveur (exemple) :

  • status : valeurs autorisées active, inactive, blocked.
  • q : longueur 2–50, recherche sur name et email.
  • sort : champs autorisés createdAt, name, id ; ajout implicite de id si absent.
  • fields : champs autorisés id, name, createdAt, status ; champs sensibles refusés.
  • pageSize : défaut 20, max 100.

Exemple complet : pagination par curseur stable

Requête initiale :

GET /users?status=active&sort=createdAt,id&pageSize=20

Réponse (extrait) :

{
  "data": [
    { "id": "u200", "createdAt": "2026-01-10T09:00:00Z" },
    { "id": "u201", "createdAt": "2026-01-10T09:01:00Z" }
  ],
  "meta": {
    "pageSize": 20,
    "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTAxLTEwVDA5OjAxOjAwWiIsImlkIjoidTIwMSJ9"
  },
  "links": {
    "next": "/users?status=active&sort=createdAt,id&pageSize=20&cursor=eyJjcmVhdGVkQXQiOiIyMDI2LTAxLTEwVDA5OjAxOjAwWiIsImlkIjoidTIwMSJ9"
  }
}

Requête suivante :

GET /users?status=active&sort=createdAt,id&pageSize=20&cursor=eyJjcmVhdGVkQXQiOiIyMDI2LTAxLTEwVDA5OjAxOjAwWiIsImlkIjoidTIwMSJ9

Point clé : le curseur encode la dernière clé de tri vue (createdAt + id), ce qui rend la navigation robuste même si de nouveaux utilisateurs sont créés entre-temps.

Répondez maintenant à l’exercice sur le contenu :

Dans quel cas la pagination par curseur est-elle généralement préférable à l’offset/limit pour une collection d’API REST ?

Tu as raison! Félicitations, passez maintenant à la page suivante

Vous avez raté! Essayer à nouveau.

La pagination par curseur est adaptée aux gros volumes et au défilement séquentiel, car elle évite les grands offsets et offre une meilleure stabilité lorsque la collection change (si le tri est déterministe, souvent avec un tie-breaker comme id).

Chapitre suivant

API REST back-end : validation des entrées et règles métier

Arrow Right Icon
Couverture de livre électronique gratuite Développement back-end : concevoir une API REST propre
50%

Développement back-end : concevoir une API REST propre

Nouveau cours

8 pages

Téléchargez l'application pour obtenir une certification gratuite et écouter des cours en arrière-plan, même avec l'écran éteint.