MAINTENIR ET FAIRE ÉVOLUER UNE API

25/10/2018

Aurélien David

  • CTO à Cap Collectif
  • Ancien JoliCodeur
  • Créateur d'Epicerie Radar 🍺

Start-up civique experte en intelligence collective qui développe des applications participatives.

Consultation

Budget participatif

Questionnaire

  • 23 personnes 🏃
  • 8 développeurs 👩‍💻

Une API est un contrat avec nos utilisateurs !

😀Evoluer le contrat

Pour ça, notre équipe technique a besoin de :

  • Flexibilité
  • Liberté
  • Rapidité

Apporter des nouvelles fonctionnalités et améliorations

😨 Maintenir le contrat

Ne pas modifier l'existant :

  • Le corps des réponses
  • Les paramètres des requêtes
  • Les fonctionnalités générales

Ne pas casser les intégrations existantes des utilisateurs

Gestion des versions

Pour les utilisateurs, chaque montée de version est un important chantier de mise à jour. 🚧

Problème : On ne peut pas faire de changements importants après la publication. 😱

  • /api/v1
  • /api/v2
  • /api/v3

Il faut donc prétendre avoir une API parfaite à chaque version, ce qui prend du temps…

 Gestion des versions 

Evolution continue

On modifie notre API de façon progressive, en ajoutant des nouveautés et en dépréciant des usages.

C'est beaucoup plus proche de notre modèle de développement… On ne repense pas notre modèle tous les ans, il évolue petit à petit.

1️⃣ Eviter autant que possible de casser les intégrations existantes.

 

2️⃣ Rendre simple l'évolution de l'API pour les développeurs.

 

3️⃣ Rendre simple l'évolution des clients pour les utilisateurs.

 

Evolution continue

Objectifs

GraphQL ?

Le serveur qui exprime des possibilités sous forme d'un schéma.

Les clients sélectionnent simplement
ce dont ils ont besoin.

GraphQL Schema Language

type Vote {}

scalar DateTime

Types

Par défaut les types scalaires présents sont : ​String, Int, Float, Boolean et ID.

GraphQL Schema Language

type Vote {
    createdAt: DateTime
    published: Boolean
}

Champs

GraphQL Schema Language

type Author {
    name: String
}

type Vote {
    author: Author
}

Relations

GraphQL Schema Language

type Query {
    userHasVote(username: String!): Boolean
    votes(published: Boolean): [Vote]
}

Arguments

GraphQL Schema Language

interface Node {
    id: ID!
}

type Vote implements Node {
    id: ID!
    value: VoteValue!
}

Interfaces

Enumération


enum VoteValue { YES NO MITIGE }

GraphQL Schema Language

schema {
    query: Query
    mutation: Mutation
}

type Query {
    vote(id: ID!): Vote
}

type Mutation {
    addVote(contribution: ID!, value: VoteValue!): Vote
}

On définit les champs demandables à la racine d'une query.

Qu'est ce qu'un changement bloquant (BC) ?

C'est un changement qui pourrait nécessiter une action de la part de nos utilisateurs.

 

Breaking: modification qui rompt les requêtes existantes vers l'API GraphQL.


Dangereux: modification qui ne casse pas les requêtes existantes mais qui peut affecter le comportement d'exécution des clients.

Ex: Remplacer un type

type Contribution {}

type Vote {
    # What we have
    contribution: Contribution
}
interface Votable {}

type Vote {
    # What we want
    votable: Votable
}

Ex: Modifier un argument

Breaking : Ici on supprime l'argument requis contributionId.

L'ajout de contributionIds non-requis est à lui seul non-bloquant.

type Query {
    # What we have
    votes(contributionId: ID!): [Vote]
}
type Query {
    # What we want
    votes(contributionIds: [ID!]): [Vote]
}

Gestion des BCs

Choix d'une politique

Facebook's GraphQL schema (over 4 years old, 1,000s of types at this point, and under active change by 100s of engineers) has never needed a versioned breaking change, and still supports 4-year old shipped versions of iOS and Android apps (which unfortunately are still being used).

 

Chez Facebook on ne casse jamais le schéma… 😱

Ok, mais ça reste une API interne…

We'll announce upcoming breaking changes at least three months before making changes to the GraphQL schema, to give integrators time to make the necessary adjustments. Changes go into effect on the first day of a quarter (January 1st, April 1st, July 1st, or October 1st). For example, if we announce a change on January 15th, it will be made on July 1st.

Chez GitHub on déprécie puis on supprime au bout de 3 à 6 mois. 🏄

Un bon compromis pour une API publique.

type Consultation {
    votesCount @deprecated(reason: 'Field will be removed.') 
}

Déprécier avec GraphQL

Utilisation d'une directive


Communiquer sur les BCs

Url dédiée ~ Compte Twitter ~ Flux RSS

Field `votesCount` will be removed. Use `votes.totalCount` instead. In preparation for an upcoming change to the way we expose counters, this field will only be available inside a connection. Removal on 2019-01-01 UTC.

 Inclure les informations utiles… Pourquoi ? Comment remplacer ? Quand ?

Soigner les messages de dépréciations :

Ne pas communiquer seulement sur les changements bloquants 😜

Inciter vos utilisateurs à utiliser les nouvelles fonctionnalités, c'est leur donner un intérêt à ne plus utiliser les parties dépréciées !

Eviter les BCs

On va voir ensemble plusieurs moyens de prévention

Mais pas un méthode miracle (désolé) !

Prévention #1

Designer son schéma pour les évolutions !

Bonus: Ne pas copier le schéma de sa BDD 😂

Connaître son domaine métier (vraiment)

Connaître très bien GraphQL

La base

Il est plus facile d'ajouter que de modifier.

L'ajout de nouveaux champs et types n'impactera pas les requêtes existantes ✌️

Avec une approche REST (classique) on augmente le payload de tous les clients 😞

L'ajout d'un nouveau champ est la manière la plus simple d'éviter un BC.

Bien nommer les choses

Parfois, être spécifique peut nous sauver plus tard. (ex: AddVoteContributionEvent vs VoteEvent)

 

Est-ce qu'un champ scalaire suffit, un champ type ne serait pas mieux ?

 

Limiter les scalaires au strict nécessaire.

🤔 Déprécier juste pour un nom est un changement bloquant qui ne donne pas de valeur aux clients, évitez-le à tout prix.

Utiliser la spécification Relay

Les types d'objet métier principaux doivent toujours implémenter Node.

Toujours vérifier si les champs de type liste doivent être paginés ou non. Si oui, faire une Connection.

Chaque mutation doit avoir son propre type Input et Payload.

Elle est conçue pour les évolutions :

Nullable vs Non null

Rendre systématiquement tout nullable est donc un moyen d'avoir un schéma plus fort !

Mais c'est embêtant pour le client qui doit vérifier «null» partout et votre schéma perd de son sens.

Si tout est non-null la moindre erreur génère un effet boule de neige…  

Rendre un champ non-null, nullable est un BC 😨.

Même déprécié on doit toujours fournir une valeur. 🤦‍♂️

Nullable vs Non null

Nullable Non-Null
La colonne en BDD est nullable ? Si ça ne fait pas sens que le champ ou l'objet existe sans valeur.
Dépendance d'un service externe ?
Une chance (même infime) que ça devienne null ?
La plupart du temps pour une relation vers un autre objet. Souvent le cas pour un attribut, donc un type scalaire.

Entrainez votre équipe au design

Inspirez vous des API publiques existantes.

 (et même de leurs erreurs !)

Prévention #2

Eviter les changements bloquants accidentels…

Enregistrer le schéma avec le code.

✅ On s'assure que le schéma est bien à jour à chaque commit.

Repérer les changements bloquants sur le schéma

Expliquer le problème

L'intégration continue échoue en cas de changement bloquant !

✌️ Aucun risque de merger un Breaking Change !

Utilisez-le, vous aussi !

Prévention #3

Gérer les cas où on est pas tout à fait sûr…

Public

Internal

Diviser notre API en schémas

Public

Internal

👩‍💻 Pour faciliter la maintenance au sein de notre équipe le schéma interne étend le schéma public.

Diviser notre API en schémas

Déplacer un type ou un champ du schéma Internal au Public

Comment gèrer les cas où on est pas tout à fait sûr ?

C'est normal de ne pas être sûr.
 

Soyons transparents.

La confiance en la conception de son API prend du temps.

 

On peut s'autoriser des petites erreurs. 😉

Public

Internal

Preview

L'utilisation du schéma Preview permet de mettre à disposition des fonctionnalités sans être contraint par la procédure des BCs.

Diviser notre API en schémas (suite)

Evidemment l'objectif est que tout ce qui passe par Preview

arrive tel quel dans Public.

Prévention #4

Un peu de magie avec GraphQL !

Qui utilise encore le champ déprécié ?

Combien de fois le champ a été utilisé dernièrement ?

Juste avant de supprimer…

Query

Analytics

query {
  consultations {
     title
     votesCount
  }
}

Pas de magie mais un peu d'analyse… 😉

Type: Query
Field: consultations
Client: 1234

Type: Consultation
Field: title
Client: 1234

Type: Consultation
Field: votesCount
Client: 1234

Connaitre les statistiques d'utilisation de son schéma

C'est la clé de la confiance dans l'évolution continue de votre API !

GraphQL le permet car c'est le client qui décrit les besoins.

Combien de fois le champ a été utilisé dernièrement ?

Exemple de ce qui a été développé à GitHub.

Qui utilise encore le champ déprécié ?

Hey [my-api-client] we notice that you are using our deprecated field `votesCount` on type `Consultation`. This field will be removed in 2 weeks, please use `votes.totalCount` instead !

🚀 Génération d'un message personnalisé

Déprécier est difficile et vient toujours avec un coût.

L'évolution continue en résumé…

La réussite dépend des processus et outils mis en place.

Une approche plus humaine et réaliste que le versioning.

Présentation inspirée du travail fait par

Marc-André Giroux et d'autres à GitHub et Shopify.

 

Allez voir leur travail ;-)

Merci ! Questions ?

Aurélien David - @spyl94