Quelle est la meilleure méthode RESTful pour renvoyer le nombre total d'éléments dans un objet?

139

Je développe un service d'API REST pour un grand site Web de réseautage social auquel je participe. Jusqu'à présent, cela fonctionne très bien. Je peux émettre GET, POST, PUTet DELETEdemande aux URL de l' objet et affecter mes données. Cependant, ces données sont paginées (limitées à 30 résultats à la fois).

Cependant, quel serait le meilleur moyen RESTful d'obtenir le nombre total de membres, disons, via mon API?

Actuellement, j'émets des demandes à une structure d'URL comme la suivante:

  • / api / members: renvoie une liste de membres (30 à la fois comme mentionné ci-dessus)
  • / api / members / 1: affecte un seul membre, selon la méthode de requête utilisée

Ma question est la suivante: comment puis-je utiliser une structure d'URL similaire pour obtenir le nombre total de membres dans mon application? De toute évidence, ne demander que le idchamp (similaire à l'API Graph de Facebook) et compter les résultats serait inefficace étant donné que seule une tranche de 30 résultats ne serait renvoyée.

Martin Bean
la source

Réponses:

84

Alors que la réponse à / API / users est paginée et ne renvoie que 30 enregistrements, rien ne vous empêche d'inclure dans la réponse également le nombre total d'enregistrements et d'autres informations pertinentes, telles que la taille de la page, le numéro de page / le décalage, etc. .

L'API StackOverflow est un bon exemple de cette même conception. Voici la documentation de la méthode Users - https://api.stackexchange.com/docs/users

Franci Penov
la source
3
+1: Certainement la chose la plus RESTful à faire si des limites de récupération vont être imposées.
Donal Fellows
2
@bzim Vous sauriez qu'il y a une page suivante à récupérer car il y a un lien avec rel = "next".
Darrel Miller du
4
@Donal la "prochaine" rel est enregistrée auprès de l'IANA iana.org/assignments/link-relations/link-relations.txt
Darrel Miller
1
@Darrel - oui, cela pourrait être fait avec n'importe quel type d'indicateur "suivant" dans la charge utile. Je pense juste qu'avoir le nombre total d'éléments de collection dans la réponse est précieux en soi et fonctionne comme un indicateur "suivant" tout de même.
Franci Penov
5
Renvoyer un objet qui n'est pas une liste d'éléments n'est pas une implémentation correcte d'une API REST mais REST ne fournit aucun moyen d'obtenir une liste partielle des résultats. Donc, pour respecter cela, je pense que nous devrions utiliser des en-têtes pour transmettre d'autres informations comme le total, le jeton de la page suivante et le jeton de la page précédente. Je ne l'ai jamais essayé et j'ai besoin des conseils d'autres développeurs.
Loenix
74

Je préfère utiliser les en-têtes HTTP pour ce type d'informations contextuelles.

Pour le nombre total d'éléments, j'utilise l'en- X-total-counttête.
Pour les liens vers la page suivante, précédente, etc. J'utilise l'en- Linktête http :
http://www.w3.org/wiki/LinkHeader

Github le fait de la même manière: https://developer.github.com/v3/#pagination

À mon avis, c'est plus propre car il peut également être utilisé lorsque vous renvoyez du contenu qui ne prend pas en charge les hyperliens (c'est-à-dire les binaires, les images).

Ondrej Bozek
la source
5
La RFC6648 déconseille la convention de préfixer les noms des paramètres non standardisés par la chaîne X-.
JDawg
70

J'ai récemment fait des recherches approfondies sur cette question et sur d'autres questions liées à la pagination REST et j'ai pensé qu'il était constructif d'ajouter certaines de mes conclusions ici. J'élargis un peu la question pour inclure des réflexions sur la pagination ainsi que sur le décompte car elles sont intimement liées.

En-têtes

Les métadonnées de pagination sont incluses dans la réponse sous la forme d'en-têtes de réponse. Le grand avantage de cette approche est que la charge utile de la réponse elle-même n'est que la demande de données réelle. Faciliter le traitement de la réponse pour les clients qui ne sont pas intéressés par les informations de pagination.

Il existe de nombreux en-têtes (standard et personnalisés) utilisés à l'état sauvage pour renvoyer des informations relatives à la pagination, y compris le nombre total.

Nombre total X

X-Total-Count: 234

Ceci est utilisé dans certaines API que j'ai trouvées dans la nature. Il existe également des packages NPM pour ajouter la prise en charge de cet en-tête, par exemple Loopback. Certains articles recommandent également de définir cet en-tête.

Il est souvent utilisé en combinaison avec l'en- Linktête, qui est une très bonne solution pour la pagination, mais ne dispose pas des informations de comptage total.

Lien

Link: </TheBook/chapter2>;
      rel="previous"; title*=UTF-8'de'letztes%20Kapitel,
      </TheBook/chapter4>;
      rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel

J'ai le sentiment, après avoir lu beaucoup sur ce sujet, que le consensus général est d'utiliser l'en- Linktête pour fournir des liens de pagination aux clients utilisant rel=next, rel=previousetc. Le problème avec cela est qu'il manque d'informations sur le nombre total d'enregistrements, ce qui pourquoi de nombreuses API combinent cela avec l'en- X-Total-Counttête.

Alternativement, certaines API et par exemple la norme JsonApi , utilisent le Linkformat, mais ajoutent les informations dans une enveloppe de réponse au lieu de dans un en-tête. Cela simplifie l'accès aux métadonnées (et crée un emplacement pour ajouter les informations de comptage total) au détriment de la complexité croissante de l'accès aux données réelles elles-mêmes (en ajoutant une enveloppe).

Gamme de contenu

Content-Range: items 0-49/234

Promu par un article de blog nommé Range header, je vous choisis (pour la pagination)! . L'auteur plaide fermement en faveur de l'utilisation des en Range- Content-Rangetêtes et pour la pagination. Lorsque nous lisons attentivement la RFC sur ces en-têtes, nous constatons que l'extension de leur signification au-delà des plages d'octets était en fait anticipée par la RFC et est explicitement autorisée. Lorsqu'il est utilisé dans le contexte de itemsplutôt que de bytes, l'en-tête Range nous donne en fait un moyen de demander une certaine plage d'éléments et d'indiquer à quelle plage du résultat total les éléments de réponse se rapportent. Cet en-tête offre également un excellent moyen d'afficher le nombre total. Et c'est un vrai standard qui mappe principalement un à un à la pagination. Il est également utilisé dans la nature .

Enveloppe

De nombreuses API, y compris celle de notre site Web de questions-réponses préféré, utilisent une enveloppe , un wrapper autour des données qui est utilisé pour ajouter des méta-informations sur les données. De plus, les normes OData et JsonApi utilisent toutes deux une enveloppe de réponse.

Le gros inconvénient de cela (à mon humble avis) est que le traitement des données de réponse devient plus complexe car les données réelles doivent être trouvées quelque part dans l'enveloppe. Il existe également de nombreux formats différents pour cette enveloppe et vous devez utiliser le bon. Il est révélateur que les enveloppes de réponse d'OData et de JsonApi sont extrêmement différentes, OData se mélangeant dans les métadonnées à plusieurs points de la réponse.

Point de terminaison séparé

Je pense que cela a été suffisamment couvert dans les autres réponses. Je n'ai pas beaucoup enquêté car je suis d'accord avec les commentaires selon lesquels cela prête à confusion car vous avez maintenant plusieurs types de points de terminaison. Je pense que c'est plus agréable si chaque point de terminaison représente une (collection de) ressource (s).

Réflexions supplémentaires

Nous devons non seulement communiquer les méta-informations de pagination liées à la réponse, mais aussi permettre au client de demander des pages / plages spécifiques. Il est intéressant de se pencher également sur cet aspect pour aboutir à une solution cohérente. Ici aussi, nous pouvons utiliser des en-têtes (l'en- Rangetête semble très approprié), ou d'autres mécanismes tels que les paramètres de requête. Certaines personnes préconisent le traitement des pages de résultats comme des ressources distinctes, ce qui peut donner un sens dans certains cas d'utilisation (par exemple /books/231/pages/52. J'ai fini par sélectionner une plage sauvage de paramètres de demande fréquemment utilisés tels que pagesize, page[size]et limitetc , en plus de soutenir l' en- Rangetête (et comme paramètre de requête ainsi que).

Stijn de Witt
la source
J'étais particulièrement intéressé par l'en- Rangetête, mais je n'ai pas pu trouver suffisamment de preuves que l'utilisation de tout autre élément que bytescomme type de plage est valide.
VisioN
2
Je pense que la preuve la plus claire peut être trouvée dans la section 14.5 de la RFC : acceptable-ranges = 1#range-unit | "none"je pense que cette formulation laisse explicitement de la place pour d'autres unités de plage que bytes, bien que la spécification elle-même ne fasse que définir bytes.
Stijn de Witt
24

Alternative lorsque vous n'avez pas besoin d'articles réels

La réponse de Franci Penov est certainement la meilleure façon de procéder afin que vous retourniez toujours des éléments avec toutes les métadonnées supplémentaires sur vos entités demandées. C'est ainsi que cela devrait être fait.

mais parfois, renvoyer toutes les données n'a pas de sens, car vous n'en avez peut-être pas du tout besoin. Peut-être que tout ce dont vous avez besoin, ce sont des métadonnées sur la ressource demandée. Comme le nombre total ou le nombre de pages ou autre chose. Dans ce cas, vous pouvez toujours demander à votre service de demander à votre service de renvoyer des éléments, mais plutôt des métadonnées comme:

/api/members?metaonly=true
/api/members?includeitems=0

ou quelque chose de similaire ...

Robert Koritnik
la source
10
L'intégration de ces informations dans les en-têtes présente l'avantage que vous pouvez effectuer une requête HEAD pour obtenir simplement le nombre.
felixfbecker
1
@felixfbecker exactement, merci d'avoir réinventé la roue et encombré les API avec toutes sortes de mécanismes différents :)
EralpB
1
@EralpB Merci d'avoir réinventé la roue et encombré les API !? HEAD est spécifié dans HTTP. metaonlyou includeitemsnon.
felixfbecker
2
@felixfbecker seulement "exactement" était fait pour vous, le reste est pour l'OP. Désolé pour la confusion.
EralpB
REST consiste à tirer parti de HTTP et à l'utiliser autant que possible pour ce à quoi il était destiné. Content-Range (RFC7233) doit être utilisé dans ce cas. Les solutions dans le corps ne sont pas bonnes, surtout parce qu'elles ne fonctionneront pas avec HEAD. créer de nouveaux en-têtes comme suggéré ici est inutile et faux.
Vance Shipley
23

Vous pouvez renvoyer le décompte sous forme d'en-tête HTTP personnalisé en réponse à une requête HEAD. De cette façon, si un client ne veut que le décompte, vous n'avez pas besoin de renvoyer la liste réelle et il n'est pas nécessaire d'avoir une URL supplémentaire.

(Ou, si vous êtes dans un environnement contrôlé d'un point de terminaison à un autre, vous pouvez utiliser un verbe HTTP personnalisé tel que COUNT.)

bzlm
la source
4
«En-tête HTTP personnalisé»? Cela reviendrait à être quelque peu surprenant, ce qui à son tour est contraire à ce que je pense qu'une API RESTful devrait être. En fin de compte, cela ne devrait pas être surprenant.
Donal Fellows
21
@Donal je sais. Mais toutes les bonnes réponses étaient déjà prises. :(
bzlm
1
Je sais aussi, mais parfois vous devez simplement laisser d'autres personnes répondre. Ou améliorez votre contribution par d'autres moyens, comme une explication détaillée des raisons pour lesquelles cela devrait être fait de la meilleure façon plutôt que d'autres.
Donal Fellows
4
Dans un environnement contrôlé, cela pourrait bien ne pas être surprenant, car il serait probablement utilisé en interne et basé sur la politique d'API de vos développeurs. Je dirais que c'était une bonne solution dans certains cas et qu'il valait la peine d'avoir ici comme note d'une possible solution inhabituelle.
James Billingham
1
J'aime beaucoup utiliser les en-têtes HTTP pour ce genre de chose (c'est vraiment là où ça doit être). L'en- tête Link standard peut être approprié dans ce cas (l'API Github l'utilise).
Mike Marcacci
11

Je recommanderais d'ajouter des en-têtes pour le même, comme:

HTTP/1.1 200

Pagination-Count: 100
Pagination-Page: 5
Pagination-Limit: 20
Content-Type: application/json

[
  {
    "id": 10,
    "name": "shirt",
    "color": "red",
    "price": "$23"
  },
  {
    "id": 11,
    "name": "shirt",
    "color": "blue",
    "price": "$25"
  }
]

Pour plus de détails, reportez-vous à:

https://github.com/adnan-kamili/rest-api-response-format

Pour le fichier swagger:

https://github.com/adnan-kamili/swagger-response-template

adnan kamili
la source
7

Depuis "X -" - Le préfixe est obsolète. (voir: https://tools.ietf.org/html/rfc6648 )

Nous avons trouvé les "Accept-Ranges" comme étant le meilleur pari pour mapper la pagination allant: https://tools.ietf.org/html/rfc7233#section-2.3 Car les "Range Units" peuvent être soit "bytes", soit " jeton". Les deux ne représentent pas un type de données personnalisé. (voir: https://tools.ietf.org/html/rfc7233#section-4.2 ) Pourtant, il est indiqué que

Les implémentations HTTP / 1.1 PEUVENT ignorer les plages spécifiées en utilisant d'autres unités.

Ce qui indique: l'utilisation d'unités de plage personnalisées n'est pas contraire au protocole, mais elle PEUT être ignorée.

De cette façon, nous devrions définir les plages d'acceptation sur "membres" ou quel que soit le type d'unité à distance, nous nous attendons à ce que cela soit possible. Et en outre, définissez également la plage de contenu sur la plage actuelle. (voir: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.12 )

Quoi qu'il en soit, je m'en tiendrai à la recommandation de la RFC7233 ( https://tools.ietf.org/html/rfc7233#page-8 ) d'envoyer un 206 au lieu de 200:

Si toutes les conditions préalables sont vraies, le serveur prend en charge le
champ d'en-tête Range pour la ressource cible, et la ou les plages spécifiées sont
valides et satisfaisables (comme défini au paragraphe 2.1), le serveur DEVRAIT
envoyer une réponse 206 (Contenu partiel) avec une charge utile contenant une
ou plusieurs représentations partielles qui correspondent aux
plages satisfaisables demandées, telles que définies au paragraphe 4.

Donc, en conséquence, nous aurions les champs d'en-tête HTTP suivants:

Pour un contenu partiel:

206 Partial Content
Accept-Ranges: members
Content-Range: members 0-20/100

Pour le contenu complet:

200 OK
Accept-Ranges: members
Content-Range: members 0-20/20
Lépidoptère
la source
3

Il semble plus facile d'ajouter simplement un

GET
/api/members/count

et renvoyer le nombre total de membres

willcodejavaforfood
la source
11
Pas une bonne idée. Vous obligez les clients à faire 2 demandes pour construire la pagination sur leurs pages. Première demande pour obtenir la liste des ressources et deuxième pour compter le total.
Jekis
Je pense que c'est une bonne approche ... vous pouvez également renvoyer simplement la liste des résultats sous forme de json et du côté client, vérifier la taille de la collection, ce cas est donc un exemple stupide ... de plus, vous pouvez avoir / api / members / count et ensuite / api / members? offset = 10 & limit = 20
Michał Ziobro
1
Gardez également à l'esprit que de nombreux types de pagination ne nécessitent pas de décompte (comme le défilement infini) - Pourquoi calculer cela lorsque le client n'en a peut-être pas besoin
tofarr
2

Qu'en est-il d'un nouveau point de terminaison> / api / members / count qui appelle simplement Members.Count () et renvoie le résultat

Steve Woods
la source
27
Le fait de donner au décompte un point de terminaison explicite en fait une ressource adressable autonome. Cela fonctionnera, mais soulèvera des questions intéressantes pour tous les nouveaux utilisateurs de votre API - Le nombre de membres de la collection est-il une ressource distincte de la collection? Puis-je le mettre à jour avec une demande PUT? Existe-t-il pour une collection vide ou seulement s'il contient des éléments? Si la memberscollection peut être créée par une requête POST à /api, sera-t-elle /api/members/countégalement créée comme effet secondaire, ou dois-je faire une requête POST explicite pour la créer avant de la demander? :-)
Franci Penov
2

Parfois, les frameworks (comme $ resource / AngularJS) nécessitent un tableau comme résultat de requête, et vous ne pouvez pas vraiment avoir de réponse comme {count:10,items:[...]}dans ce cas, je stocke "count" dans responseHeaders.

PS En fait, vous pouvez le faire avec $ resource / AngularJS, mais cela nécessite quelques ajustements.

Vahe Hovhannisyan
la source
Quels sont ces ajustements? Ils seraient utiles sur des questions comme celle-ci: stackoverflow.com/questions/19140017/…
JBCP
Angular ne nécessite pas de tableau comme résultat de la requête, il vous suffit de configurer votre ressource avec la propriété d'objet option:isArray: false|true
Rémi Becheras
0

Vous pourriez considérer countscomme une ressource. L'URL serait alors:

/api/counts/member
Frank Rem
la source
-1

Lorsque vous demandez des données paginées, vous connaissez (par valeur de paramètre de taille de page explicite ou valeur de taille de page par défaut) la taille de la page, vous savez donc si vous avez toutes les données en réponse ou non. Lorsqu'il y a moins de données en réponse qu'une taille de page, vous obtenez des données complètes. Lorsqu'une page complète est renvoyée, vous devez redemander une autre page.

Je préfère avoir un point de terminaison séparé pour le nombre (ou le même point de terminaison avec le paramètre countOnly). Parce que vous pouvez préparer l'utilisateur final à un processus long / fastidieux en affichant la barre de progression correctement lancée.

Si vous souhaitez renvoyer la taille des données dans chaque réponse, il doit y avoir pageSize, offset également mentionné. Pour être honnête, le meilleur moyen est de répéter également une demande de filtres. Mais la réponse est devenue très complexe. Donc, je préfère un point de terminaison dédié pour renvoyer le nombre.

<data>
  <originalRequest>
    <filter/>
    <filter/>
  </originalReqeust>
  <totalRecordCount/>
  <pageSize/>
  <offset/>
  <list>
     <item/>
     <item/>
  </list>
</data>

Ma couleur, préférez un paramètre countOnly au point de terminaison existant. Ainsi, lorsqu'elle est spécifiée, la réponse contient uniquement des métadonnées.

endpoint? filter = valeur

<data>
  <count/>
  <list>
    <item/>
    ...
  </list>
</data>

endpoint? filter = value & countOnly = true

<data>
  <count/>
  <!-- empty list -->
  <list/>
</data>
Wooff
la source