Blog // Exirel.me

Les versions et les API (partie 2)

Par Florian Strzelecki - 17:23 - 16.01.2014

Tags : Web, Bonne pratique, Technique, API, Version, HTTP, Hypermedia

Si vous ne l'avez pas déjà fait, je vous invite à lire la partie 1, qui parle d'applications installées plusieurs fois, dans des environnements clients spécifiques à chacun, et où, finalement, le numéro de version dans l'URL n'a pas beaucoup de sens.

Sommaire

  1. Partie 1 : L'application fournie un service via une interface (ou plusieurs)
  2. Partie 2 : Le service est fourni par une (ou plusieurs) applications

Le service est fourni par une (ou plusieurs) applications

Dans la partie 1 donc, j'ai présenté un exemple de SIGB, qu'il faut installer dans un environnement client. Cette fois, je vais prendre le même SIGB, mais avec un seul serveur centralisé : vous êtes alors en position de fournisseur de service, comme peut l'être Facebook, Twitter, Google, et bien d'autres.

Dans ce contexte, il existe des différences fondamentales avec le contexte précédent :

  1. Chaque version de l'application n'est installée qu'une seule fois (par vous)
  2. Les mises à jour sont indépendantes de la volonté des clients
  3. Chaque client décide à quel moment il exploite un changement rétro-compatible
  4. Chaque changement non rétro-compatible impose une adaptation du client (immédiate ou différée)

Les contraintes et les attentes n'étant pas les mêmes, la notion de version peut être traitée différemment.

Version d'API ou d'application ?

Dans ce cas de figure, l'application n'expose plus directement sa version aux clients, et chaque version d'une interface doit être alors vue comme une interface différente.

C'est d'ailleurs ce qui s'est passé avec le passage à la Graph API de Facebook :

https://api.facebook.com/ (avant)
https://graph.facebook.com/ (après)

C'est aussi une partie de la réponse d'@edasfr à ma première partie :

Le numéro de version (qu’il soit dans l’URL ou ailleurs) est là pour dissocier deux appels et savoir ce que l’API doit répondre. On met habituellement des numéros de version ou des dates parce que c’est plus simple et évite des incompréhensions, mais l’important est juste que l’appel soit différent.

Ce en quoi il a tout à fait raison : si l'interface change, alors quelque part, c'est comme une "nouvelle" interface. L'appeler "/v1" et "/v2", ou, comme Facebook, changer le nom de domaine, cela revient exactement au même. Personnellement, j'ai une préférence pour l'approche de Facebook.

La version des interfaces ne répond pas aux mêmes règles lorsque vous êtes le fournisseur unique du service.

Version d'interface dans l'URL

Reprenons notre exemple de SIGB. Vous fournissez un service à plusieurs clients, en hébergeant vous mêmes votre application et donc vos interfaces.

Dans votre application version 1.0, vous fournissez cette URL :

/v1/books (application v1.0)

Puis, dans une version 1.1 de l'application, vous ajoutez une URL, mais rien n'a encore changé pour la première. Vous êtes devant un choix, car vous pouvez faire ceci :

/v1/books (application v1.0)
/v1/authors(application v1.0 et v1.1)

Ou bien ceci :

/v1/books (application v1.0)
/v1.1/books(application v1.1)
/v1.1/authors(application v1.1)

Dans le premier cas, vous mettez simplement à jour votre application, qui fournis une interface rétro-compatible avec la précédente. Comme le dit @edasfr, mettre un "v1" ou "/totoro" est identique, et un identifiant opaque ferait tout aussi bien l'affaire.

Dans le second cas, vous installez deux fois votre application, une fois par version. Vos clients qui veulent profiter de la v1.1 peuvent utiliser la nouvelle URL ainsi que l'ancienne : dans tous les cas ils doivent s'adapter, le changement d'URL n'ayant aucun coût supplémentaire pour eux.

Si le client n'a besoin de rien, le client ne devrait pas avoir à s'adapter pour rien.

Je fais ici un parallèle avec ce que je disais dans la partie 1 :

Vous évitez à vos clients, de facto, de faire des modifications lorsqu'ils n'en ont aucun besoin.

Ma principale préoccupation, ici, c'est le client, car c'est pour lui que l'API existe.

Conduire le changement

J'en arrive (enfin) à la question d'origine d'@edasfr :

Ma question initiale était de savoir si je mettais la version dans l’url ou les entêtes.

Pour répondre à cette question, je pense qu'il faut répondre à celle-ci en premier :

Quelle conduite du changement voulez-vous mettre en place ?

Ces deux questions sont intrinsèquement liées : si vous préférez tout rendre explicite, la façon la plus rapide et simple pour vous est d'avoir un numéro de version dans l'URL, comme je l'ai montré dans les exemples précédents.

Dans ce cas là, il ne vous reste plus qu'à déterminer quand vous publiez une nouvelle version, quand vous rendez obsolète une version antérieure, et quelle version vous continuez de maintenir et supporter (SAV, bug fix, documentation, formation, etc.).

Cela vous facilite évidement la vie : vos clients n'ont jamais à remettre en question leur façon de faire et vous non plus. Dans le cas d'une interface qui change une fois tous les deux ans, avec un support sur 5 ans, c'est parfaitement compréhensible.

Moins les changements d'interfaces sont fréquents, plus il est facile de gérer des URLs différentes.

Choisir la bonne approche

Si vous avez besoin d'apporter des évolutions fréquentes - par exemple tous les 2-3 mois, voire tous les mois - cette attitude peut vite vous apporter quelques soucis, notamment pour le support et sa durée :

Je sais, @edasfr va me rétorquer :

Si l’objet est de dire que maintenir des versions parallèles coûte (plus) cher, alors oui : Il faut effectivement vérifier la compatibilité à chaque changement, maintenir potentiellement plusieurs codes, et en gardant la compatibilité on se contraint forcément à ne pas tout changer en big-bang.

Ce que je souhaite souligner encore une fois, c'est le point de vue du client, de son éducation et de sa perception du changement :

  1. Doit-il s'inquiéter ou non de changement fréquents ? Sont-ils tous rétro-compatibles ?
  2. Combien de temps a-t-il devant lui pour prévoir ses migrations ?
  3. Va-t-il être réfractaire aux migrations parce qu'elles sont trop fréquentes, ou justement trop espacées ?
  4. Aura-t-il l'habitude du changement ? Quelles pratiques doit-il acquérir pour vivre les migrations de façon sereine ?

Je ne peux pas répondre à ces questions, car les réponses dépendent de votre service. Mais pensez-y : quelle attitude doit avoir votre client ? Comment devrait-il percevoir les changements dans vos interfaces ?

Néanmoins, je peux proposer une autre approche que le numéro de version (entête ou URL peu importe), qui s'attache plus à une conduite du changement perpétuel, et où, finalement, tous les changements sont perçus comme étant rétro-compatibles.

Dans un monde idéal

Mettons désormais que vous n'ayez plus de version dans vos URLs. D'ailleurs, vous n'avez plus aucun numéro de version nulle part, car vous n'en sentez pas le besoin.

Dans un monde idéal, vous n'aurez que des changement rétro-compatibles "simples", car ce qui existe est déjà complet. Vous pouvez donc effectuer ces mises à jour sereinement :

Dans tous ces cas, vous n'aurez pas de problèmes à conduire le changement. Un client qui souhaite une nouvelle fonctionnalité n'a plus qu'à attendre et effectuer de nouveaux développement pour profiter des mises à jour.

Mais un jour, vous vous rendez-compte qu'une de vos URLs n'est pas pertinente, ou que vous "dupliquez" un peu trop le contenu. Il est temps de trouver autre chose, car les changements qui s'annoncent rendent plus difficile la rétro-compatibilité.

Redirection

Mettons que vous ayez ceci :

/books/type/novel
/books/type/novel/authors
/books/type/history
/books/type/history/authors
/books/type/science
/books/type/science/authors
/books/type/politic
/books/type/politic/authors
# and so on...

Et qu'à force d'augmenter la liste des genres disponibles, vous pensez à regrouper toutes les URLs de genre sous une autre arborescences (et en ajoutant quelques URLs) :

/type/novel
/type/novel/authors
/type/history
/type/history/authors
/type/science
/type/science/authors
/type/politic
/type/politic/authors

Cela vous permez d'avoir une classification qui vous convient mieux : un genre est ainsi lié autant aux auteurs qu'aux livres. Il pourrait même l'être aux bibliothèques, et à d'autres choses encore... Quoi qu'il en soit : vous avez une réorganisation des URLs en tête.

Toujours dans ce monde un peu idéal, vous ne répondez qu'à des requêtes de type GET, alors la solution est plutôt simple : effectuer des redirections permanentes (301).

L'inconvénient bien entendu, c'est qu'il faut maintenir une liste de redirection, et s'assurer qu'il n'y a pas de conflit avec d'autres URLs ; mais ce n'est rien qu'une gestion sérieuse ne saurait gérer. Il faut aussi penser à prévenir vos clients sur la durée de validités des redirections : 2 mois ? 6 mois ? 1 an ? C'est à vous de voir.

Habituer le client à avoir des redirections permet une conduite du changement plus souple.

Hypermedia, hyperlien, hyper silencieux

Bon, il est bel et bon d'avoir des URLs qui ne changent pas trop, et qui peuvent s'utiliser avec des redirections. Cependant, les redirections ne font pas toujours bon ménage avec les traitements POST, PUT, DELETE, etc. car la plupart des clients, en suivant une redirection, vont effectuer un GET sur la nouvelle URL - ce qui n'est pas très malin, mais j'ai suffisamment de rapports de bugs pour dire que c'est un vrai problème à prendre en compte.

C'est là qu'intervient Hypermedia, et les liens entre les documents. Imaginez que vous ayez, dans la première version, ce document Book :

<?xml version="1.0" encoding="UTF-8"?>
<book id="ma-vie-mon-oeuvre">
    <title>Le narcissisme pour les nuls</title>
    <types>
        <type href="/books/type/history">History</type>
        <type href="/books/type/politic">Politic</type>
    </types>
</book>

Grâce à la présence du href, vous pouvez communiquer facilement les nouvelles URLs :

<?xml version="1.0" encoding="UTF-8"?>
<book id="ma-vie-mon-oeuvre">
    <title>Le narcissisme pour les nuls</title>
    <types>
        <type href="/type/history">History</type>
        <type href="/type/politic">Politic</type>
    </types>
</book>

Cependant, cela ne fonctionne que sous certaines conditions : vos clients doivent être habitués à suivre des liens plutôt que la documentation. Ou plutôt, la documentation doit explicitement indiquer comment passer d'un document à un autre, en insistant sur les liens.

De cette façon, vous conduisez une partie du changement de façon souple et légère.

Hyper problème

La structure des URLs, c'est déjà un premier pas. Cependant, ce n'est suffisant que dans un monde idéal, dans le monde réel c'est une toute autre paire de manche - @edasfr le souligne ici encore :

Crédo perso : Il faut l'éviter le plus longtemps possible mais un jour tu finiras forcément par avoir besoin d'un changement incompatible. Autant le prévoir aujourd'hui.

Par exemple, une évolution de la structure des données : c'est une modification du format. J'ai déjà parlé du cas rétro-compatible, alors ne parlons ici que des changements non rétro-compatible.

J'ai déjà soulevé rapidement la question du format, qui implique nécessaire un changement de version, mais pas forcément de l'interface : l'interface peut être capable de répondre dans différents formats, sans changer ses URLs ni le comportement.

Dans un contexte hypermedia, avoir des numéros de versions pour gérer des formats incompatibles pose un problème : un client doit accepter tous les changements, ou aucun changement. Soit il utilise la v1 et ne change rien, soit il utilise la v2 et doit accepter tous les changements.

Exemple concret, un livre en V1.0 de l'application, sur l'URL /v1/book/ma-vie-mon-oeuvre :

<?xml version="1.0" encoding="UTF-8"?>
<book id="ma-vie-mon-oeuvre">
    <title>Le narcissisme pour les nuls</title>
    <types>
        <type href="/v1/books/type/history">History</type>
        <type href="/v1/books/type/politic">Politic</type>
    </types>
</book>

Et un genre dans la même version /v1/books/type/history :

<?xml version="1.0" encoding="UTF-8"?>
<type id="history">
    <name>History</name>
    <books>
        <book id="ma-vie-mon-oeuvre" href="/v1/book/ma-vie-mon-oeuvre" />
    </book>
</type>

Puis en V1.1 de l'application, sur l'URL /v1/book/ma-vie-mon-oeuvre :

<?xml version="1.0" encoding="UTF-8"?>
<book id="ma-vie-mon-oeuvre">
    <title lang="fr_FR">Le narcissisme pour les nuls</title>
    <types>
        <type href="/v1/type/history">History</type>
        <type href="/v1/type/politic">Politic</type>
    </types>
</book>

Dans cette version, le lien vers un genre retourne exactement le même résultat qu'avant (aucun changement).

Et un livre en V2, sur l'URL /v2/book/ma-vie-mon-oeuvre :

<?xml version="1.0" encoding="UTF-8"?>
<book id="ma-vie-mon-oeuvre">
    <titles>
        <title lang="fr_FR">Le narcissisme pour les nuls</title>
        <title lang="en_EN">Narcissism for dummies</title>
    </title>
    <types>
        <type href="/v2/type/history">History</type>
        <type href="/v2/type/politic">Politic</type>
    </types>
</book>

Mais cette fois-ci, en V2 de l'application avec l'URL /v2/type/history le format du genre change :

<?xml version="1.0" encoding="UTF-8"?>
<type id="history">
    <names>
        <name lang="fr_FR">Histoire</name>
        <name lang="en_EN">History</name>
    </names>
    <books>
        <book id="ma-vie-mon-oeuvre" href="/v2/book/ma-vie-mon-oeuvre" />
    </book>
</type>

Si le client souhaite utiliser le second format des livres (parce qu'il veut gérer plusieurs langues), il ne peut plus suivre les URLs sans devoir aussi changer sa gestion du genre. Cet exemple est simple : il n'y a que deux ressources. Mais imaginez plutôt ce qui arrivera avec des dizaines, voire des centaines de ressources différentes ?

Personnellement, c'est à ce moment là que je remets le plus en question le mécanisme de versions différentes dans les URLs, car je ne pense pas qu'il faille forcer la main des clients pour des mises à jours aussi importante. Oui, vous pouvez toujours fournir un support sur le long terme, oui vous pouvez toujours effectuer des rustines et du portage pour assurer une rétro-compatibilité complexe, mais je pense qu'il est possible de prévoir le changement autrement.

Accept, Content-Type and Vary header

Le mécanisme HTTP pour donner des informations sur ce que le client demande comme format est d'utiliser des entêtes HTTP : Accept pour la requête, Content-Type et Vary pour la réponse.

Reprenons notre exemple de requête et de version, et voyons quelles requêtes un client devra effectuer :

GET /book/ma-vie-mon-oeuvre
Accept: vnd.example-com.foo+xml; version=2.0

Et la réponse sera donc :

HTTP/1.1 200 OK
Vary: Content-Type
Content-Type: vnd.example-com.foo+xml; version=2.0

<?xml version="1.0" encoding="UTF-8"?>
<book id="ma-vie-mon-oeuvre">
    <titles>
        <title lang="fr_FR">Le narcissisme pour les nuls</title>
        <title lang="en_EN">Narcissism for dummies</title>
    </title>
    <types>
        <type href="/type/history">History</type>
        <type href="/type/politic">Politic</type>
    </types>
</book>

Puis il suivra le lien vers le genre, mais avec une entête un peu différente dans sa requête :

GET /type/history
Accept: vnd.example-com.foo+xml; version=1.0

Et la réponse sera :

HTTP/1.1 200 OK
Vary: Content-Type
Content-Type: vnd.example-com.foo+xml; version=1.0

<?xml version="1.0" encoding="UTF-8"?>
<type id="history">
    <name>History</name>
    <books>
        <book id="ma-vie-mon-oeuvre" href="/book/ma-vie-mon-oeuvre" />
    </book>
</type>

Vous donnez ainsi le choix à votre client : il peut adapter son code à la version 2.0 des livres, et conserver son code pour la version 1.0 des genres.

Bien entendu, si vous estimez que la version 1.0 doit disparaître dans le mois à venir, votre client devra faire la modification rapidement et donc la question ne se pose pas vraiment. Mais vous ouvrez des possibilités très intéressantes :

  1. Le client peut adapter morceau par morceau son usage de l'API, sur le long terme.
  2. Le client est habitué à des mises à jours fréquentes sans pour autant augmenter le risque lié à cette fréquence.
  3. Vous pouvez exposer une version bêta et ne finalement mettre qu'une partie en production, celle que les clients trouveront utile/pratique/fonctionnelle (par rapport à d'autres nouveautés non plaisante) : les client seront déjà prêt à vous suivre.

Le point bonus, à mon avis, est que vous pourrez toujours ajouter un type de format avec la même méthode, il suffit de changer les types de contenu possible :

vnd.example-com.foo+xml
vnd.example-com.foo+json
vnd.example-com.foo+csv
vnd.example-com.foo+gtfs

Vouloir, pouvoir, et réalité

Dans tout ce que vous venez de lire, il faut y voir une volonté, une orientation qui considère avant tout les bonnes pratiques et l’éducation, avant de considérer la facilité et le contrôle.

De fait, dans la réalité, vous n'aurez pas toujours un choix facile à faire : si vos clients n'ont jamais été habitués à conduire un quelconque changement, et s'ils ne sont pas ouvert un seul instant sur l'usage raisonné et moderne de HTTP, vous aurez tout le mal du monde à faire autrement qu'avec une version dans l'URL.

Mais - et qu'y puis-je ? - je suis un indécrottable optimiste, convaincu que l'éducation fait des miracles, et que les bonnes pratiques triompherons toujours des difficultés et de la paresse intellectuelle.