Blog // Exirel.me

Les versions et les API (partie 1)

Par Florian Strzelecki - 00:00 - 16.01.2014

Tags : Programmation, Bonne pratique, Technique, API, Version

Vaste sujet que la gestion des versions dans le domaine des API, et cette fois encore ce billet fait suite à une discussion que j'ai eu sur Twitter. Tout a commencé par un tweet d'@edasfr (compte protégé), dont vous pouvez trouver le blog dont je conseille la lecture à cette adresse : n.survol.fr.

Comme les tweets ne suffisent pas, voici un billet qui, je l'espère, vous éclairera sur la gestion des versions et des URLs pour vos (futures) API.

Je ne prétends ni avoir la réponse, ni détenir de vérité absolue : par mon partage d'expérience j'espère seulement vous donner les bonnes informations pour que vous répondiez vous-même à la question.

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

Mais de quoi est-il question ?

Une API, ce n'est jamais qu'une interface, et dans notre cas, une interface passant par HTTP et ses bonnes vieilles méthodes (GET, POST, PUT, etc.). De facto, il peut exister moins, autant, ou plus d'interfaces que d'applications.

Mais de quelles applications parlons-nous ? C'est là, à mon avis, que se trouve le premier élément de réponse. Vous ne pouvez pas aborder le problème des versions sans savoir dans quel contexte vous vous situez.

Ensuite, qu'est-ce qui est versionné ? Il y a l'application, l'interface, les formats de données, et les données elle-même.

Si vous croisez les types d'applications avec les types de versions, cela donne un large panel de cas à traiter, ce qui n'est pas une mince affaire. J'ai remarqué que la tentation est forte de résoudre un problème par la mauvaise solution, en se trompant simplement sur "qu'est-ce qui doit être versionné", et "de quoi dépendent les versions".

Dans cette première partie, j'aborde le cas d'une application versionnée et distribuée à des clients qui installent chacun leur propre instance, et peuvent utiliser chacun une version différente.

L'application fournie un service via une interface (ou plusieurs)

Imaginez que vous développiez une application, au hasard, un SIGB : cette application va être installée plusieurs fois, dans des conditions diverses et variées, que vous ne pouvez pas totalement contrôler.

Votre application a donc plusieurs versions, mais votre client n'en installe qu'une seule, dans son environnement. D'autres clients utilisent d'autres versions, chacune installée dans son environnement client.

Chacun de ses clients a le même besoin : accéder aux services fournis par votre application ; et pour cela il développe (ou utilise) des clients en suivant votre documentation. Bien sûr, vous pouvez fournir un client, un SDK, et tout un tas de choses pour simplifier son travail (et notamment les versions), mais c'est une autre histoire.

Dans ce cadre là, mettre un numéro de version dans vos URLs est risqué - mais possible - car cela peut devenir pénible de gérer des migrations en fonction des cas qui se présentent. Voyons justement quelque uns de ces cas possibles (et en l'occurence, vécus).

Rien ne change, mais tout change

Imaginez, dans votre première version d'application (votre v1), que vous ayez cette URL à disposition des clients :

/v1/books (application v1.0)

Puis, dans une version suivante, vous devez ajouter une nouvelle fonctionnalitée qui ne change pas la précédente. Votre premier choix est de changer le numéro de version de toutes les URLs pour cette nouvelle version de l'application, ce qui vous donne ceci :

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

Après installation de la mise à jour de son application, votre client vous envoie un email d'insultes, les clichés ayant la vie dure et les clients forcément prompt à l'insulte (gratuite l'insulte, toujours) :

Malheur à vous ! Ça ne marche plus, et j'ai perdu tous mes livres !

Vous lui expliquez qu'il doit changer son numéro de version, et que tout ira bien : la première URL n'a pas changée. Et en prime, vous lui offrez la seconde. Vous, je ne sais pas, mais moi en tant que client je détesterais vivre ça : j'ai déjà 3 bibliothèques de quartier qui m'ont appelé en urgence ce matin, et je n'ai pas un seul développeur sous la main pour réparer le problème.

Bon, d'accord, en tant que client, j'aurais pu anticiper ce changement, c'est peut-être aussi un petit peu ma faute (et puis mettre en production un vendredi soir aussi...). Mais la documentation me dit qu'il s'agit d'une version mineure "compatible" avec les précédentes : je suis perdu, aidez-moi !

Vous pensez que ça ne vous arrivera jamais ? Mon expérience me dit que vous ne pouvez pas compter sur votre chance pour vous en sortir, car cela ne marche qu'un temps.

On peut toujours rejeter la faute sur le client, mais est-ce vraiment une bonne idée ?

Juste un ajout

Bon, la prochaine fois, vous faites autrement : vous ne changez pas le numéro de version tant que vous ne cassez rien. Ce qui vous amène à ceci :

/v1.1/books (application v1.1 et v1.2)
/v1.1/authors (application v1.1 et v1.2)
/v1.1/libraries (application v1.2 uniquement)

Vous publiez votre nouvelle version de votre application, certains de vos clients font la mise à jour, d'autres pas. Bien sûr, vous précisez dans la documentation que la dernière URL n'est disponible qu'après la mise à jour vers la version 1.2 de l'application : c'est bien, vous êtes prévenant.

Dans ce cas très précis, je pense que vous avez raison d'avoir cette approche, qui est la moins mauvaise selon moi. Car, si vous regardez bien, votre documentation est moins liée à la version de votre API qu'à la version de votre application.

Mais, dans ce cadre là, à quoi vous sert le numéro de version dans l'URL ? Globalement : à rien.

Si ce qui compte est la version de l'application, alors les URLs n'ont pas besoin de version.

Que vos URLs commencent par "/bonjour" ou bien par "/v1.1", c'est du pareil au même : vous ne donnez aucune information intéressante à vos clients, qui devront, dans un cas comme dans l'autre, vérifier ce qui change et ce qui est compatible avec la nouvelle version de l'application.

Sauf qu'en retirant le numéro de version, vous évitez à vos clients, de facto, de faire des modifications lorsqu'ils n'en ont aucun besoin.

Et une très mauvaise idée pour la route

Ceux qui me suivent sur Rennes le savent : j'ai une dent contre l'API de @starbusmetro, notamment à cause du système de version des URLs (mais n'allez pas instuler leurs CM, ils sont vraiment très sympas et ils n'y peuvent rien). Pour les curieux, la documentation est disponible en ligne et les données sont en Open-Data.

Pour comparaison, voici ce leur stratégie de version des URLs donne dans notre cas :

/v1/books (application v1, v1.1 et v1.2)
/v1.1/authors (application v1.1 et v1.2)
/v1.2/libraries (application v1.2 uniquement)

C'est à dire qu'au tout début il n'existe que /v1, puis par la suite cohexistent /v1 et /v1.1. Sauf que vous ne pouvez pas faire ça :

/v1.1/books
/v1.2/books

Toute mes excuses pour cette grossièreté, mais je trouve ça complètement con, en plus d'être pénible dès que vous devez créer un client pour appeler l'API (je parle d'expérience). Pas de constante générique dans le code, il faut connaître la version de chacune des commandes... il pourrait y avoir /parici ou bien /parla comme préfixe que cela ne changerait rien du tout : le numéro de version des URLs n'a ici aucun sens, et il est trompeur.

Le nombre des critiques que j'adresse à cet API dépasse largement le cadre des versions. Et ce n'est pas le sujet...

Et le format alors ?

Bon, vous n'avez pas eu cette mauvaise idée, alors nous allons revenir à votre application v1.2 : vous avez introduit un autre changement (chut, c'est moi qui raconte l'histoire), cette fois invisible dans les URLs, qui est un changement "plus ou moins léger" de format. D'ailleurs, ce n'est pas la première fois, mais vous avez tout documenté officiellement, et personne ne s'est encore plaint.

Avec la v1 de votre application, lorsque quelqu'un demandait les livres il obtenait ceci :

<?xml version="1.0" encoding="UTF-8"?>
<books version="1.0">
    <book id="ma-vie-mon-oeuvre">
        <title>Le narcissisme pour les nuls</title>
    </book>
</books>

Avec la v1.1 de votre application, il obtenait alors ceci (rétro-compatible) :

<?xml version="1.0" encoding="UTF-8"?>
<books version="1.1">
    <book id="ma-vie-mon-oeuvre">
        <title>Le narcissisme pour les nuls</title>
        <author>Moi-même</author>
    </book>
</books>

Et enfin dans cette dernière v1.2 (non rétro-compatible) :

<?xml version="1.0" encoding="UTF-8"?>
<books version="2.0">
    <book id="ma-vie-mon-oeuvre">
        <title>Le narcissisme pour les nuls</title>
        <isbn />
        <abstract>Ma vie, mon oeuvre, en long en large et en travers.</abstract>
        <authors>
            <author>Moi-même</author>
            <author role="trad">Mon alter-ego</author>
        </authors>
        <libraries>
            <library>À côté de chez moi.</library>
            <library>Et un peu plus loin aussi.</library>
        </libraries>
    </book>
</books>

Avec cet exemple simple, vous devriez entrevoir la longue liste des questions à prendre sérieusement en considération : comment gérer la version du format par rapport à celle de l'application ? Comment assurer la retro-compatibilité ? Comment indiquer la version du format ? Etc.

D'ailleurs, vous l'aurez sans doute remarquer, mais le format 2.0 n'aurait pas dû être utilisé dans la version 1.2, car il amène un changement non retro-compatible plutôt gênant. À la place, il aurait fallu préférer ceci :

<?xml version="1.0" encoding="UTF-8"?>
<books version="2.0">
    <book id="ma-vie-mon-oeuvre">
        <title>Le narcissisme pour les nuls</title>
        <isbn />
        <abstract>Ma vie, mon oeuvre, en long en large et en travers.</abstract>
        <author>Moi-même</author>
        <author role="trad">Mon alter-ego</author>
        <libraries>
            <library>À côté de chez moi.</library>
            <library>Et un peu plus loin aussi.</library>
        </libraries>
    </book>
</books>

Dans le cas présent, les versions du format peuvent se résumer à la version de l'application, et pas tellement à celle de l'API :

Les versions de formats qui dépendent de la version de l'application doivent être traitées comme le reste de l'application.

Si vous gérez correctement la version de l'application, alors mécaniquement le changement de version du format n'est pas un problème.

La version de l'application

Que ce soit pour les URLs ou pour les formats, finalement, cela revient à traiter la version de l'application de façon intelligente. L'interface n'est plus alors qu'une fonctionnalité comme les autres, dont la gestion est au même niveau que les autres fonctionnalités : en gérant correctement sa numérotation, vous pouvez finalement gérer correctement les versions de vos interfaces.

Prenez les SGBD : que ce soit MySQL, Oracle, ou Postgresql, à chaque nouvelle version il faut vérifier ce qui est compatible de ce qui ne l'est pas. La différence entre une 8.0 et une 8.1 sera probablement invisible pour vos applications dépendantes, alors que la différence entre une 8.0 et une 9.0 sera probablement une autre histoire.

Pour autant vous n'avez pas besoin de préciser, lorsque vous faites une requête SQL, le numéro de version du SGBD. Ce parallèle me sert à conclure ainsi cette partie :

Alignez vos changements avec la version de votre application, ni plus, ni moins, sans décoration inutile.

Considération annexe

Vous pourriez, à la lecture de cette première partie, rétorquer que rien ne vous empêche de fournir ces URLs ci pour votre application version 1.2 :

/v1/books
/v1.1/books
/v1.1/authors
/v1.2/books
/v1.2/authors
/v1.2/libraries

Oui, vous pouvez, mais par expérience, je pense que :

  1. Cela complique inutilement le développement (augmentation mécanique du nombre de lignes et de tests)
  2. Les risques et complications sont multipliés par le nombre de versions à gérer
  3. Vous devrez sans cesse assurer que le nouveau code est capable de gérer toutes les versions, et donc les comportements
  4. Des soucis de performances peuvent être à prévoir
  5. Cela réduit considérablement les possibilités d'un changement d'architecture interne

Je pense donc que, oui, c'est une solution au problème, mais une solution qui amène sont lots de nouveaux problèmes qui peuvent coûter très cher, surtout à long terme.