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,inactiveou?status=active&status=inactive - Plages (dates, nombres) :
?createdAt[gte]=2026-01-01&createdAt[lt]=2026-02-01ou?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 dansname,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.
- Écoutez le fichier audio avec l'écran éteint.
- Obtenez un certificat à la fin du programme.
- Plus de 5000 cours à découvrir !
Téléchargez l'application
- Exemple :
GET /users?sort=createdAt,-name(croissant surcreatedAt, puis décroissant surname) - 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 :
pageetpageSize(ouoffsetetlimit) - Exemple :
GET /users?page=3&pageSize=20(ou?offset=40&limit=20)
| Critère | Offset/limit |
|---|---|
| Simplicité | Très simple à comprendre |
| Accès direct à une page | Oui (page 10, etc.) |
| Stabilité si données changent | Peut provoquer des doublons/éléments manqués si insertions/suppressions |
| Performance à grande profondeur | Peut 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(ouafter) - Exemple :
GET /users?sort=createdAt,id&pageSize=20&cursor=eyJjcmVhdGVkQXQiOiIyMDI2LTAxLTE1VDEwOjAwOjAwWiIsImlkIjoiMTIzIn0=
| Critère | Curseur |
|---|---|
| Simplicité | Plus complexe (curseur opaque) |
| Accès direct à une page | Non (navigation séquentielle) |
| Stabilité si données changent | Meilleure stabilité (si tri déterministe) |
| Performance à grande profondeur | Souvent 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,idpour 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 > 10000si 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.
- Parser : lire
status,q,sort,fields,page/pageSizeoucursor. - Normaliser : trim, convertir types (int, bool, date), appliquer valeurs par défaut.
- Valider : vérifier formats (date ISO), bornes (
pageSize), et appartenance à une whitelist. - 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.
- 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=20Règles de traitement côté serveur (exemple) :
status: valeurs autoriséesactive,inactive,blocked.q: longueur 2–50, recherche surnameetemail.sort: champs autoriséscreatedAt,name,id; ajout implicite deidsi absent.fields: champs autorisésid,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=20Ré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=eyJjcmVhdGVkQXQiOiIyMDI2LTAxLTEwVDA5OjAxOjAwWiIsImlkIjoidTIwMSJ9Point 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.