ZEP-02 : Elaboration d'une API

a marqué ce sujet comme résolu.

Ok, tu as sûrement plus d'expertise dans le domaine que moi, tu te rends sans doute plus compte de ce qu'il en retourne.

Je veux créer connaitre la liste des topics crées par le membre "firm1", j'y accède par quelle url ?

Pour moi, - /api/v1/forums/sujets/ : Renvoie la liste de tous les sujets de tous les forums (hell !!) ; - /api/v1/forums/sujets/?membre=firm1 : Renvoie la liste de tous les sujets de tous les forums de l'utilisateur firm1.

Pour moi, - /api/v1/forums/sujets/ : Renvoie la liste de tous les sujets de tous les forums (hell !!) ; - /api/v1/forums/sujets/?membre=firm1 : Renvoie la liste de tous les sujets de tous les forums de l'utilisateur firm1.

Là déjà en posant des cas concret on se rend compte que tout le monde n'est pas d'accord sur la forme des urls, d'où la necessité de toutes les expliciter clairement.

D'accord avec Andr0 c'est, je pense, le format le plus naturel.

L'API forums sert des ressources de types forum : donc des messages, et des sujets.

Mais oui, il faut dresser la liste complète des URLs et fonctionnalités attendues avec :

  • l'URL

  • la méthode HTTP

  • les paramètres possibles (critères de filtre, de tri, de pagination), leurs types, et les valeurs autorisées

  • un exemple de résultat renvoyé ou attendu par le serveur

  • les différents codes de retour possibles

Perso j'ai appelé ça le DDD. En fait ça s'appelle des specs, mais dans le cas de l'API ce qui est cool c'est que ça sert de doc par la suite :)

+4 -0

Un truc vraiment top à prendre en compte et, je pense, même en v1 histoire de poser les bases le plus tôt possible, c'est les requêtes conditionnelles.

Un exemple dans l'API Github v3

Quand on développe une app tiers basée sur des APIs c'est vraiment le genre de truc où on se dit "ah p'tain ils ont bien fait les choses".

On se sert souvent d'APIs de façon incrémentale. On récupère un jeu de données, on poll périodiquement pour récupérer les nouvelles données. Se refader le contenu entier à chaque fois pour déterminer ce qui a changé c'est lourd, vraiment pénible, et pas efficace pour tout le monde.

A ça, certains répondent par un queryParam du type lastModified= mais ça donne des URLs pas très jolies, je trouve la solution d'utiliser les headers beaucoup plus élégante.

D'autres trucs à prendre en compte, en vrac :

  • les problèmes de CORS auxquels on va inévitablement être confrontés. C'est le cas à chaque fois et c'est chiant. Encore une fois adressés dans l'API Github. En gros se contenter de renvoyer le header Access-Control-Allow-Origin devrait suffire

  • Utiliser un format standard pour les dates renvoyées et ne jamais en déroger. Je penche pour l'ISO8601 : YYYY-MM-DDTHH:MM:SSZ c'est celui que j'ai vu quasiment partout.

  • Prendre en compte la pagination. C'est toujours pénible à implémenter mais il faut le faire. Parce que le nombre de messages dans un forum typiquement, ça va vite devenir infernal à renvoyer et autant pas se dire "on verra à ce moment là". Là encore, l'implémentation faite par Github me paraît être une excellente source d'inspiration. En résumé : le nombre d'items renvoyé par page est paramétrable (dans les limites du raisonnable) en queryParam, la page voulue est elle aussi indiquée en queryParam. Les informations de pagination (combien de pages, …) sont quant à elles envoyées dans les headers de la réponse HTTP histoire de ne pas surcharger la charge utile (payload) de la réponse. (certains le mettent directement dans le contenu de la réponse HTTP en JSON hein, ça se fait aussi).

  • au niveau des formats de sortie, sauf si vraiment c'est complètement transparent côté serveur, autant ne pas s'emmerder et renvoyer du JSON (c'est ce que fait Twitter par exemple). Si jamais plusieurs formats sont acceptés en entrée et en sortie, penser à se baser sur les headers HTTP Content-Type et Accept envoyés par le client.

  • des limitations vont devoir être implémentées assez rapidement également… Encore une fois, c'est du boulot supplémentaire jamais très sexy : "on stocke ça où, comment ? ah ouais mais faut le reset à un moment donné… Ah mais c'est une fenêtre glissante dans le temps, berk, c'est chiant !" Mais on n'est pas à l'abri d'un client (même pas mal intentionné) qui écroule la plateforme à grand coup de GET api/v1/forums/messages toutes les secondes. Surtout dans notre cas où l'API, le site etc. sont totalement publiques

PS : aux vues du travail assez important à fournir, je suis vraiment, vraiment pour se contenter en v1 d'une seule API, publique, et en lecture uniquement. Ce qui nous permet d'adresser l'authentification après, ça peut paraître un peu fou dit comme ça mais OAuth ça se plug vraiment bien dans plein de frameworks et je pense que le plus important c'est d'avoir une API robuste dans son format avant de gérer l'authentification (qui finalement n'est qu'un simple contrôle de token à chaque appel de méthode API et renvoie nue 403 si KO, pas très compliqué en regard de tout le reste à mon sens).

+4 -0

Note : en regardant sur Github j'ai vu qu'il existait une branche nommée API qui date un peu. Je voudrais foutre en l'air le travail de personne en respécifiant qqch de déjà spécifié, donc arrêtez-moi si je vais trop loin svp :)

Pour apporter de l'eau au moulin de cette ZEP, cet article est assez intéressant.

Tout n'est pas bon à garder dans notre contexte mais je pense que tout le monde devrait y jeter un oeil.

Ensuite, comme j'ai la p'tite impression que je risque d'être le premier client de l'API1 (Andr0 peut-être ?) et qu'on n'est jamais mieux servi que par soi-même ;) je propose d'avancer un peu sur une API en lecture, très simple, l'API /membres.

Version de l'API

La version est indiquée dans l'URL d'appel de l'API.

1
http://zestedesavoir.com/api/v1/...

User-Agent

L'en-tête HTTP User-Agent doit être renseigné par le client.

Cross-Origin Request Sharing

Le serveur doit renvoyer les en-tête HTTP suivants afin d'éviter les soucis de CORS :

1
2
3
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link
Access-Control-Allow-Credentials: true

Considération générales

Formatage des erreurs

Lorsqu'un appel API provoque une erreur, il faut s'appuyer sur les codes HTTP standards. La réponse HTTP doit donc systématiquement utiliser un code interprétable pour le client. En plus de cela, le contenu de la réponse doit indiquer un code (par défaut le même que celui de la réponse HTTP, mais peut être différents pour donner de plus amples informations) ainsi qu'un message d'erreur compréhensible par le développeur de l'API.

Exemple :

1
{errors:[{code:404, message:"Ce membre n'existe pas"}]}

Format d'entrée

  • Seul le JSON est accepté.

  • Lecture du header Content-Type, seule la valeur application/json est acceptée. Les autres formats donnent lieu à un code d'erreur HTTP 406 Not Acceptable. (NB : Twitter fait la différence entre le format des données envoyées et le format dans lequel les données sont envoyées. Dans le cas présent, ils renverraient une 404. A nous de décider ce qu'on fait).

1
{errors: [{code:406, message:"Le client doit envoyer des données au format application/json et positionner le header HTTP Content-Type de façon à le signaler"}]}

Format de sortie

  • Uniquement en JSON.

  • Lecture du header HTTP Accept, si la valeur application/json est absente, envoi d'un code HTTP 406 et de l'erreur suivante :

1
{errors: [{code:406, message:"Le client doit accepter des réponses au format JSON et le signaler en positionnant le header HTTP Accept avec la valeur application/json"}]}

Caching

  1. Lorsqu'une réponse est envoyée à un client, un ETag est calculé (une fonction de hashage est appliquée au contenu renvoyé) est envoyée au client dans le header de la réponse HTTP prévu à cet effet (ETag).

  2. Lorsqu'une requête parvient au serveur, ce dernier lit la valeur présente dans le header HTTP If-None-Modified. Lorsque celle-ci correspond à la valeur qui lui serait retournée, il ne faut pas lui faire parvenir la réponse mais simplement le code HTTP 304 Not Modified.

  3. Dans de futures implémentations, on peut imaginer stocker en base ce champ ETag pour des raisons évidentes de performances. Il faudra alors bien penser à l'invalider lorsque des modifications sur la ressource sont effectuées (et ce, peu importe la source de ces modifications : édition sur le site, appel API, modification directe en base !).

Pagination

  • Les résultats de requête doivent être paginés. Par défaut, une page de résultat doit contenir 50 résultats

  • L'utilisateur indique la page de résultats qu'il souhaite récupérer à l'aide du paramètre &page=. Dans le cas où ce paramètre est invalide (n'est pas un nombre entier, ou en dehors des limites de la pagination) l'API retourne une erreur 400 :

1
{errors:[{code:4001, message:"le paramètre indiqué pour la pagination n'est pas un nombre entier valide"}]}
1
{errors:[{code:4002, message:"la page indiquée n'existe pas"}]}
  • L'API fournit des indications dans les headers de la réponse HTTP afin d'aider le client à naviguer dans les ressources paginées à l'aide du header Link. de la façon suivante :
1
2
3
4
Link: <http://zestedesavoir.com/membres/?search=toto&page=4>; rel="next",
  <http://zestedesavoir.com/membres/?search=toto&page=5>; rel="last",
  <http://zestedesavoir.com/membres/?search=toto&page=1>; rel="first",
  <http://zestedesavoir.com/membres/?search=toto&page=2>; rel="prev"

NB : Ne pas oublier de remettre tous les paramètres utilisés par le client dans l'URL (ici search=)

  • Dans de futures versions, l'utilisateur pourra indiquer le nombre de résultats souhaités en positionnant le paramètre &perPage= dans sa requête

Format de dates

Le format de dates retenu pour la communication avec le serveur est le format ISO 8601 (attention Java 7 nécessaire pour les utilisateurs de Java), pas de soucis en Python a priori.

En ce qui concerne les en-têtes HTTP, voici la liste des formats à gérer côté serveur.

API Membres

Objectif

Fournir aux clients un ensemble d'outils pour consulter la liste des membres et leurs profils.

Primitives

Chemin Méthode Paramètres et signification Description fonctionnelle Codes d'erreur Exemple de résultat
/api/v1/membres/ GET aucun pour le moment, search pour de futures versions Renvoie la liste des membres du site aucun autre que les codes standard {[membres:{id:1,pseudo:'Javier', …}]}
/api/v1/membres/{idMembre} GET aucun Renvoie la fiche profil d'un membre 400 si l'identifiant fourni ne correspond pas au format d'identifiant attendu. 404 si le membre n'existe pas {[membres:{id:1,pseudo:'Javier', …}]}

NB : WIP, je complète dès que je peux. Si vous avez des remarques vous pouvez m'en faire part dès maintenant.


  1. j'avais dans l'idée un petit projet de stats d'activité du site en temps réel. D'abord parce que c'est rigolo, ensuite pour bosser un peu avec des technos que j'ai envie de mieux m'approprier : serveur asynchrone et websockets. L'avantage pour ZdS c'est qu'il suffit de servir les données via API et je m'occupe de les récupérer périodiquement une seule fois pour tous les clients connectés à un instant t évitant ainsi la surcharge engendrée par une page de stats par client qui interrogerait périodiquement les APIs du site. 

+8 -0

Je serai plus pour avoir un domaine api.zestedesavoir.com plutôt qu'une url type zestedesavoir.com/api/v1. Avec éventuellement un header X-ZdS-Api-v1 si on veut absolument renseigner la vesion de l'api utilisée. Mais je trouve cette pratique un peu désuette…

+0 -0

Le /v1/ me semble important pour ne pas péter du jour au lendemain les applis qui tournent sur cette version le jour où on publie une v2 non-rétrocompatible. C'est plus souple, ça nous laisse libres quand on voudra faire la v2, et ça garantit la pérennité des applis externes, ce qui sera apprécié tant par leurs developpeurs que leurs utilisateurs.

Après, entre un sous-domaine api ou un /api/, je m'en fous un peu : ça ne changera rien à l'utilisation.

+7 -0

J'osais pas le proposer pour le sous-domaine mais je préfère également, tant qu'à faire.

Pour le numéro de version je pense que c'est réellement nécessaire pour les raisons qu'a évoquées nohar. Rien qu'en ce qui concerne le profil utilisateur, il n'est pas exclu que le modèle de données change et il arrivera qu'on ne maintienne pas l'ancien modèle (soyons honnête, c'est chiant et pour pas grand chose).

Avec un numéro de version indiqué et clair dans la doc on est armé pour faire cela proprement (le serveur renvoie un 301 moved permanently sur /api/v1 par exemple). Si c'est documenté, le développeur saura à quoi s'en tenir plutôt que de passer à côté d'une modif du modèle et se demander pourquoi diable le compte Google+ de l'utilisateur ne s'affiche plus.

Ensuite, dans l'URI ou via un en-tête personnalisé… Vaste débat que voilà… Dans l'article dont j'ai donné le lien, le mec prêche la version dans l'URI, pour assurer ce qu'il appelle l'explorabilité des ressources entre les versions. Par contre il donne un lien vers stackoverflow dans lequel les utilisateurs sont beaucoup plus enclins à ne pas versionner dans l'URI.

Les deux me vont hein, qu'on soit bien d'accord. Et si j'ai fait le choix de versionner l'URL plutôt qu'un en-tête c'est parce que je ne connais pas les outils de reporting que vous utilisez pour explorer les logs du serveur HTTP (nginx si je ne m'abuse).

Typiquement, un jour, il se peut qu'on cherche à savoir combien de requêtes par jour sont effectués sur telle version de l'API (une version dépréciée par exemple). Si vous possédez un outil puissant, qui vous permet de filtrer facilement sur un en-tête HTTP alors pas de problème, sinon, filtrer sur un path c'est bien évidemment plus simple.

De même, Mr X développe un client tiers utilisant l'API, son site bug pas mal, mais on aimerait quand même bien savoir quelle version de l'API il utilise, c'est plus immédiat de regarder les URL d'appel que d'aller chercher dans les headers (si c'est une appli web la différence est très minime).

Voilà, j'ai pas du tout d'avis arrêté sur la question et je suis preneur de toute remarque sur ce point particulier, histoire d'avoir des avis frais sur la question :)

Merci d'avoir réagi ;)

+3 -0

Les puristes de REST te diront que ça polle l'URI.

En REST une URI doit décrire une ressource, dans sa totalité. Mais du coup le numéro de version pollue-t-il ? Non, selon eux, puisqu'il fait partie de la description de la ressource ("Les messages, d'un membre, avec le modèle v1"). Par contre le préfixe /api/ n'a, lui, pas de sens à proprement parler au niveau de la ressource.

Selon moi c'est des vraiment une considération de bout de chandelle, mais je trouve ça plus élégant quand "api" est dans le sous-domaine (c'est complètement suggestif, non argumenté, mais j'assume :) ).

+2 -0

Ou preprod_api… Vu que c'est uniquement à des fins de dev, ce n'est pas non plus un problème majeur.

Clairement, je pense que cette question relève complètement du détail devant le reste de la proposition de Javier. Ce serait peut-être pas mal de voir déjà les points sur lesquels tout le monde est d'accord, les inclure dans la ZEP, puis poursuivre la discussion sur les points à résoudre (dont celui-là).

+4 -0

Ensuite, comme j'ai la p'tite impression que je risque d'être le premier client de l'API1 (Andr0 peut-être ?) et qu'on n'est jamais mieux servi que par soi-même ;) je propose d'avancer un peu sur une API en lecture, très simple, l'API /membres.

Javier

C'est le meilleur moyen de se retrouver avec une v2 de ton API non rétro compatible avec la v1. L'intéret de faire un workflow complet sur un module c'est qu'on constitue un cas d'usage clair et qui te force à prendre en compte tous les aspects. C'est une vision à moyen terme, efficace, plus flexible et qui ouvre les portes à une v2 rétro compatible.

Pour le reste, comme nohar l'a bien expliqué pour ce qui est des sous domaines et de la version, ça relève du détail. Le plus gros du travail reste à faire : c'est à dire préciser chaque url, ses entrées, ses sorties, et les traitements à faire.

Oui mais il n'y a pas vraiment d'intérêt (aucune utilisation interessante) à sortir une API v1 dont l'unique but c'est la lecture sur des membres, surtout si elle n'est pas rétro compatible.

ça serait du temps perdu de développement sans compter le temps de gestion des doubles versions d'API par la suite.

Je te laisse avancer sur la spec du coup firm1 et spécifier l'intégralité des entrées sorties de tous les modules.

On n'a visiblement pas la même vision de l'évolution d'une API et du reste, j'ai sans doute une assez mauvaise vision du code serveur.

Je respecte ça, pas de soucis.

+0 -0

Je te laisse avancer sur la spec du coup firm1 et spécifier l'intégralité des entrées sorties de tous les modules

Javier

Je précise bien qu'ici je ne propose que d'aborder un seul module a la fois. Étant donné que le module des membre me semble assez pauvres en information ça serait dommage de commencer par là, pu lier l'API et se rendre compte finalement en passant a un module plus complet (comme celui des forums) on s'est planté dans certaines choses parce que le cas traité en premier était trop petit.

Et aussi parce que je ne vois pas de cas d'usage d'une API qui délivre la liste des membres simplement

Mmh, avec un peu de recul je pense qu'on s'est pas bien compris et que c'est de ma faute en fait.

Quand je parle "d'API v1 qui délivre la liste des membres en lecture seule", en réalité, il faut plus voir ça comme un brouillon d'API.

Et justement, personne n'utilisera cette API à part nous pour des tests et éventuellement moi parce que ça m'amuse. Et je pense qu'il ne faut surtout pas se dire "on va tout casser c'est la catastrophe". A la rigueur on la tag v0, ou bêta, ou draft, peu importe, mais mon idée c'est que si on attend de spécifier à fond un module de cet ampleur on ne le fera jamais.

A la rigueur, le seul point sur lequel je comprends ton hésitation c'est sur l'écriture qui demande authentification et qu'il faudrait en effet spécifier très tôt.

J'ai mis au point pas mal d'API, et ce qui nous a poussé à changer de version ce sont réellement des changements profonds de structure des données renvoyées (du style : enveloppe ou pas). Ou de choix techniques forts : comment on gère le polling différentiel ou la pagination.

Les path c'est généralement pas un soucis, les query parameters s'ajoutent généralement naturellement au besoin sans casser la rétrocompatibilité.

Après, j'ai très certainement une vision un peu biaisée de la conception d'API dans un contexte Java/Grails, dans lequel j'ai des services qui font cela pour moi de façon très simple (annotations, intercepteurs, …). Et j'ai sans doute du mal à voir où est le risque de se planter ? Est-ce-que c'est dans l'implémentation ? Ou vraiment dans la spécification ? Si c'est dans le premier cas alors je m'efface, parce que je ne connais pas les difficultés qui pourraient être rencontrées et je comprends très bien que ça puisse être le cas.

Si c'est le second cas de figure alors j'aimerais que tu m'éclaires un peu plus que "le cas est trop petit" et qu'on réfléchisse tous les deux à ce qui pourrait poser problème et qu'on trouve des solutions.

+0 -0

En effet, il y'a eu quiproquo

Si c'est le second cas de figure alors j'aimerais que tu m'éclaires un peu plus que "le cas est trop petit" et qu'on réfléchisse tous les deux à ce qui pourrait poser problème et qu'on trouve des solutions.

Javier

C'est bien le second cas (la spécification). Sortir une Vx d'une API signifie qu'elle sera utilisé potentiellement par une ou plusieurs personne. Et si ce dernier doit modifier ses appels à chaque realease, ce n'est pas très cool pour lui.

Si on décide par exemple d'implémenter une API en lecture seule uniquement pour des membres, on pourrait décider de certains noms de paramètres GET qui pourraient rentrer en conflits avec d'autres, si on a penser à toute la chaine. C'est donc pour ça que quitte à penser à toute la chaine, autant la spécifier entièrement.

Si on décide par exemple d'implémenter une API en lecture seule uniquement pour des membres, on pourrait décider de certains noms de paramètres GET qui pourraient rentrer en conflits avec d'autres, si on a penser à toute la chaine. C'est donc pour ça que quitte à penser à toute la chaine, autant la spécifier entièrement.

firm1

Avec d'autres quoi ? J'ai du mal à te suivre.

+1 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte