Changements d'API

Dépréciation GraphQL sans numéro de version

6 min de lecture

Une API REST peut publier /v2/ à côté de /v1/ et laisser chaque appelant migrer à son propre rythme. GraphQL a un schéma sur un endpoint, et chaque client, l’app mobile sur le build de l’année dernière et le dashboard interne déployé ce matin, interroge le même graphe. Il n’y a pas d’URL à forker. Déprécier un champ signifie le marquer comme déprécié sur place, dans un schéma dont tout le monde dépend déjà, ce qui rend la discipline différente de REST même si le problème sous-jacent, dire aux appelants que quelque chose va disparaître, est le même que couvre dépréciation d’API en général.

Comment GraphQL marque-t-il un champ comme déprécié, s’il n’y a pas de version à incrémenter ?

Avec la directive @deprecated, appliquée directement au champ :

type Product {
  price: Float @deprecated(reason: "Use priceV2 for multi-currency support.")
  priceV2: Money
}

Le champ reste interrogeable. Il ne disparaît pas, ne renvoie pas de 404, ne change pas de comportement ; il porte juste une note lisible par machine que la plupart des outils GraphQL, GraphiQL, Apollo Studio, les linters de schéma, montreront à quiconque parcourt le schéma ou écrit une requête contre lui. C’est tout le mécanisme. Il n’y a pas d’endpoint de dépréciation séparé, pas d’en-tête, pas de document compagnon exigé par la spec, ce qui est à la fois l’attrait et le piège : la directive est facile à ajouter et facile à ignorer, parce que rien ne force un client à la regarder.

Est-ce que quelqu’un voit vraiment la raison de la dépréciation ?

Seulement ceux qui utilisent le schéma directement, par introspection ou un éditeur conscient du schéma, et c’est un public plus petit que les lecteurs habituels d’un changelog d’API. Une app mobile construite contre une requête il y a six mois a déjà cuit cette requête dans son binaire ; elle continuera à demander price et continuera à recevoir une réponse, déprécié ou non, jusqu’à ce que quelqu’un reconstruise l’app avec le nouveau champ et publie une mise à jour. La directive dit à une développeuse qui écrit du nouveau code de ne pas utiliser l’ancien champ. Elle ne fait rien pour le client déjà déployé et en fonctionnement.

MécanismeQui il atteint
Directive @deprecatedDéveloppeuses parcourant le schéma ou écrivant de nouvelles requêtes
Échecs CI du linter de schémaL’équipe propriétaire du code client, si elle en fait tourner un
Une entrée de changelogQuiconque la lit, y compris une équipe cliente sans linter
Rien (le champ fonctionne juste)Un client déjà construit utilisant l’ancien champ

Un champ déprécié devrait-il quand même avoir une entrée de changelog ?

Oui, et elle fait plus de travail que la directive seule, parce qu’un changelog atteint des gens que la directive ne peut pas : une équipe partenaire qui consomme le graphe sans parcourir son schéma, un client construit contre une copie en cache du schéma vieille de plusieurs mois, quiconque ne le remarquerait qu’en lisant de la prose. Changelog d’API couvre en général ce qu’une entrée doit à un appelant ; une entrée GraphQL doit une chose que REST n’a rarement besoin d’expliciter, parce que les appelants REST la déduisent du numéro de version : si l’ancien champ fonctionne encore aujourd’hui, fonctionne encore avec un avertissement, ou a effectivement cessé de renvoyer des données. La directive seule ne répond à rien de tout ça pour une lectrice qui n’a jamais ouvert le schéma.

Quand est-il vraiment sûr de retirer un champ du schéma ?

Seulement une fois que les logs de requêtes montrent que personne ne le demande plus, ce qui est une question d’usage, pas de calendrier. Un champ peut porter @deprecated pendant un an et rester structurant pour un client jamais reconstruit ; le retirer selon un calendrier fixe, comme le fait souvent un Sunset REST, casse ce client sans aucun avertissement sur lequel il puisse agir, parce que GraphQL ne lui donne rien sur quoi agir au-delà de la directive qu’il n’a jamais lue. Journalisez l’usage au niveau du champ avant de vous engager sur une date de retrait, et traitez tout compte de requêtes non nul comme une pause, pas un compte à rebours.

Ajouter un champ porte-t-il le même risque que dans une API REST ?

Moins, pour un nouveau champ, parce qu’un client GraphQL ne reçoit que les champs qu’il demande explicitement. Ajouter priceV2 à côté de price ne peut pas casser une requête existante de la façon dont ajouter un champ à une réponse JSON REST peut casser un désérialiseur strict, parce que rien ne force le client à demander le nouveau champ. Ajouter une valeur à une énumération existante est l’exception qui vaut la peine d’être nommée dans le même souffle : un client qui teste exhaustivement chaque valeur d’énumération, ce que les langages fortement typés encouragent, casse dès qu’une nouvelle valeur arrive, que ce soit demandé par une requête ou non. La sécurité ne vaut que pour les champs et les membres d’union auxquels un client choisit d’adhérer ; elle ne vaut pas pour un ensemble fermé qu’un client énumère à la main dans son code.

Qu’est-ce qu’une entrée de changelog GraphQL a besoin que n’a pas une entrée REST ?

La forme de la requête, pas seulement le nom du champ, parce que « le champ price est déprécié » manque de la pièce dont un appelant a réellement besoin : quels types et quelles requêtes le touchent. Une entrée utile nomme le type, le champ, le champ de remplacement et, si vous pouvez le générer, les requêtes réelles en production qui demandent encore l’ancienne forme. Cette dernière pièce, relier l’avis de dépréciation à l’usage réel, est ce que les appelants REST obtiennent gratuitement des logs serveur sur une URL et que les appelants GraphQL n’obtiennent pas, parce que chaque requête frappe le même endpoint quoi qu’elle demande.

Autre chose qu’un champ peut-il porter la directive @deprecated ?

Les valeurs d’énumération, en utilisant la même directive directement sur la définition de la valeur plutôt que sur celle du champ :

enum ShippingMethod {
  STANDARD
  EXPRESS
  OVERNIGHT @deprecated(reason: "Use EXPRESS with priority: true instead.")
}

La spec définit @deprecated pour exactement deux emplacements, une définition de champ ou une valeur d’énumération, et rien d’autre à ce jour dans la version stable ; la dépréciation au niveau des arguments et des champs d’entrée n’existe que dans un langage de brouillon plus récent, pas dans ce que la plupart des serveurs implémentent aujourd’hui. Une valeur d’énumération marquée ainsi reste une valeur légale qu’un serveur peut encore renvoyer ou accepter, la même promesse de non-rupture que fait un champ déprécié, ce qui la rend sûre à livrer avant de vraiment retirer la valeur.

FAQ

GraphQL supporte-t-il quelque chose comme un en-tête Sunset pour tout un endpoint ? Non, parce qu’il n’y a généralement qu’un seul endpoint. Le calendrier de dépréciation vit au niveau du champ, dans le texte de raison de la directive @deprecated et dans quel que soit le changelog ou guide de migration qu’une équipe publie à côté, pas dans un en-tête de réponse qu’un client peut lire de façon programmatique.

Un champ déprécié peut-il être retiré puis réajouté plus tard avec un type différent ? Seulement sous un nouveau nom de champ. Réintroduire le même nom de champ avec un type changé est exactement le changement cassant que le cycle de dépréciation existe pour éviter ; donnez au remplaçant son propre nom, comme le fait priceV2, et laissez l’ancien s’éteindre complètement avant que le nom soit libre pour réutilisation.

Le texte de raison @deprecated devrait-il pointer vers l’entrée de changelog ? Oui, quand l’outillage de schéma le supporte. Le champ raison accepte une simple chaîne de caractères, et une URL à l’intérieur de cette chaîne est le chemin le plus court entre une développeuse fixant une sortie d’introspection et l’explication plus complète qu’une entrée de changelog peut donner.

Un changement de schéma GraphQL est-il jamais rétrocompatible d’une façon dont REST ne l’est pas ? Les changements additifs de champ, oui, pour la raison ci-dessus : les clients n’obtiennent que ce qu’ils demandent. Les nouvelles valeurs d’énumération sont l’exception, parce qu’un client qui énumère un ensemble fermé peut casser sur une valeur qu’il n’attendait pas. Les retraits et les changements de type sont exactement aussi cassants que leurs équivalents REST.


Les affirmations techniques de cet article n'ont pas été vérifiées de façon indépendante. Si quelque chose est faux, dis-le-nous et nous le corrigerons.

À voir sur changeloop : Documentation développeurs, Comparatif d'outils de changelog

changeloop
L'équipe qui construit un changelog qui boucle la boucle. Tes utilisateurs demandent, ton équipe livre, la personne qui a demandé est prévenue.