19/11/2018
Start-up civique experte en intelligence collective qui développe des applications participatives.
Questionnaire
Boîte à idées
Appel à projets
Interpellation
🍺 🍕
On sponsorise des bières et de pizzas, donc restez jusqu'à la fin ! 😄
Allez, on passe au sujet ➡️
Une API est un contrat avec nos utilisateurs !
Pour ça, notre équipe technique a besoin de :
Apporter des nouvelles fonctionnalités et améliorations
Ne pas modifier l'existant :
Ne pas casser les intégrations existantes des utilisateurs
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. 😱
Il faut donc prétendre avoir une API parfaite à chaque version, ce qui prend du temps…
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.
Objectifs ✌️
Le serveur qui exprime des possibilités sous forme d'un schéma.
Les clients sélectionnent simplement
ce dont ils ont besoin.
Petit tuto
⬇️
La suite ➡️
type Vote {}
scalar DateTime
Par défaut les types scalaires présents sont : String, Int, Float, Boolean et ID.
type Vote {
createdAt: DateTime
published: Boolean
}
type Author {
name: String
}
type Vote {
author: Author
}
type Query {
userHasVote(username: String!): Boolean
votes(published: Boolean): [Vote]
}
interface Node {
id: ID!
}
type Vote implements Node {
id: ID!
value: VoteValue!
}
enum VoteValue { YES NO MITIGE }
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.
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.
type Contribution {}
type Vote {
# What we have
contribution: Contribution
}
interface Votable {}
type Vote {
# What we want
votable: Votable
}
Breaking : Ici on supprime l'argument requis contributionId. 😱
type Query {
# What we have
votes(contributionId: ID!): [Vote]
}
type Query {
# What we want
votes(contributionIds: [ID!]): [Vote]
}
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).
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.
Un bon compromis pour une API publique.
type Consultation {
votesCount @deprecated(reason: 'Field will be removed.')
}
Utilisation d'une directive
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 :
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 !
On va voir ensemble plusieurs moyens de prévention…
Mais pas un méthode miracle (désolé) !
Bonus: Ne pas copier le schéma de sa BDD 😂
Connaître son domaine métier (vraiment)
Connaître très bien GraphQL
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.
Parfois, être spécifique peut nous sauver plus tard. (ex: AddVoteContributionEvent vs VoteEvent)
🤔 Déprécier juste pour un nom est un changement bloquant qui ne donne pas de valeur aux clients, évitez-le à tout prix.
There are only two hard things in Computer Science: cache invalidation and naming things.
Phil Karlton
Conseil: Limiter les scalaires au strict nécessaire.
type Event {
name: String
startTime: Date
endTime: Date
}
type TimeRange {
start: Date
end: Date
}
type Event {
name: String
time: TimeRange
}
Est-ce qu'un champ scalaire suffit, un champ type ne serait pas mieux ?
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 :
Par exemple avec base64 :
VXNlcjp1c2VyMQ==
{
'type': 'User',
'id': 'user1'
}
Adopter ce pattern dès le début permet d'éviter d'avoir à modifier vos ids 😨
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 | 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. |
Inspirez vous des API publiques existantes.
(et même de leurs erreurs !)
✅ On s'assure que le schéma est bien à jour à chaque commit.
Expliquer le problème
✌️ Aucun risque de merger un Breaking Change !
Public
Internal
Public
Internal
👩💻 Pour faciliter la maintenance au sein de notre équipe le schéma interne étend le schéma public.
Déplacer un type ou un champ du schéma Internal au Public
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.
Evidemment l'objectif est que tout ce qui passe par Preview
arrive tel quel dans Public.
Qui utilise encore le champ déprécié ?
Combien de fois le champ a été utilisé dernièrement ?
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
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.
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.