Débutant

Initiation à GraphQL

Nous utilisons les API pour pratiquement tout aujourd'hui, cela permet à des applications de communiquer entres elles, c'est aussi une façon d'exposer des données publiquement comme le fait la RATP par exemple ou Github.

API est l'acronyme de Application Programming Interface, c'est tout simplement une interface qui permet aux applications d'interagir entres elles. Afin de vulgariser la chose, imaginer une commande dans un restaurant, vous choisissez votre repas sur un menu et vous demandez au serveur de vous l'apporter. Dans cet exemple, le menu représente la documentation de votre API, le serveur représente l'API et votre demande au serveur représente la requête HTTP vers l'API.

Construire une API en soit n'est pas si compliqué, la complexité réside dans la compréhension de celle-ci par, entre autres, les développeurs qui l'utiliseront, que ce soit des développeurs interne à l'entreprise ou autres. L'API doit être bien documenté et intuitive. Ce n'est pas tout le temps le cas, malheureusement.

Le style d'architecture le plus utilisé aujourd'hui pour le développement d'API est REST. Mais REST c'est quoi ?

API REST

REST est un style d'architecture logicielle définissant un ensemble de contraintes à utiliser pour créer des services web. Elle a été définit en 2000 par Roy Fielding.

REST est l'acronyme de Representational State Transfer, ce qui signifie que lorsqu'on fait un appel à une API REST, le serveur transfert au client la représentation de l'état de la ressource demandée. La ressource est n'importe quel objet que l'API renvoie, cela peut être un user, un hôtel...
La représentation d'état est souvent en JSON mais peut aussi être en XML ou HTML.

Pour que le serveur puisse répondre vous devez lui fournir certaines choses :

  • L'identifiant de la ressource demandée. C'est l'URL de la ressource qui porte aussi le nom de endpoint
  • L'opération que le serveur doit exécuter sur cette ressources sous la forme d'un verbe HTTP (GET, POST, PUT, DELETE...)

Par exemple si on souhaite utiliser l'API Github pour récupérer les données d'un utilisateur (moi) on fera un appel vers le endpoint /users avec la méthode GET comme suit :


{
    'login': 'LamineMbn',
    'id': 15983057,
    'node_id': 'MDQ6VXNlcjE1OTgzMDU3',
    'avatar_url': 'https://avatars3.githubusercontent.com/u/15983057?v=4',
    'gravatar_id': '',
    'url': 'https://api.github.com/users/LamineMbn',
    'html_url': 'https://github.com/LamineMbn',
    'followers_url': 'https://api.github.com/users/LamineMbn/followers',
    'following_url': 'https://api.github.com/users/LamineMbn/following{/other_user}',
    'gists_url': 'https://api.github.com/users/LamineMbn/gists{/gist_id}',
    'starred_url': 'https://api.github.com/users/LamineMbn/starred{/owner}{/repo}',
    'subscriptions_url': 'https://api.github.com/users/LamineMbn/subscriptions',
    'organizations_url': 'https://api.github.com/users/LamineMbn/orgs',
    'repos_url': 'https://api.github.com/users/LamineMbn/repos',
    'events_url': 'https://api.github.com/users/LamineMbn/events{/privacy}',
    'received_events_url': 'https://api.github.com/users/LamineMbn/received_events',
    'type': 'User',
    'site_admin': false,
    'name': 'Lamine BENDIB',
    'company': '@SoatGroup ',
    'blog': '',
    'location': 'Paris, France',
    'email': null,
    'hireable': null,
    'bio': null,
    'public_repos': 15,
    'public_gists': 1,
    'followers': 1,
    'following': 1,
    'created_at': '2015-11-23T15:16:17Z',
    'updated_at': '2019-01-28T19:16:33Z'
}

REST est largement utilisé de nos jours mais malheureusement les principes de REST ne remplissent pas certains besoins du Web et des applications mobiles. En développant une API REST vous allez rencontrer certains soucis, dont nous allons parler tout de suite.

Limites des API REST

Plusieurs endpoint :

Vous aurez plusieurs endpoint (point d'entrée de votre API)


GET /books    #Liste des livres
GET /books/2  #Livre avec id=2
PUT /books/1  #Mise à jours livre id=2

N+1 call :

Ce problème survient quand nous n'avons pas toutes les données nécessaires pour afficher l'UI (User Interface), par exemple nous souhaitons afficher une liste de livres avec le détail de chacun, nous allons faire un GET /books et voilà la réponse du service :


{
    'books':[
        {
            'id':1,
            'isbn':523685941
        },
        {
            'id':2,
            'isbn':845236952
        },
        {
            'id':3,
            'isbn':986532671
        }
    ]
}


Nous n'avons pas assez de détails sur les livres, juste l'id et l'isbn, donc pour chaque livre nous devons faire un autre appel HTTP pour avoir le détail... C'est tout de suite très gourmand en ressources.
Vous allez me dire "Pourquoi ne pas rajouter plus de détails dans GET /books ?" et je vais vous répondre que c'est ce qui se fait aujourd'hui sauf qu'on tombe facilement sur le troisième point dont je vais vous parler.

Over-fetching :

Le over-fetching c'est tout simplement le fait de récupérer plus de données que nécessaires. Plus on rajoute des données dans un endpoint pour éviter de faire plusieurs appels API, plus la taille de la réponse grossie et plus l'utilisateur se retrouve avec des données dont il n'a pas besoin.

Il y a évidemment des solutions pour contourner ces limites mais elles sont souvent fastidieuses à mettre en place. J'ai lu récemment un article sur le passage à GraphQL chez Paypal, je vous conseille d'y jeter un coup d'œil pour voir les optimisations qu'ils ont essayé de faire sur leur API REST avant de passer sur GraphQL.

GraphQL parlons-en.

GraphQL

GraphQL est un langage de requête développé par Facebook et utilisé en interne depuis 2012, il est sous licence open-source depuis 2015.
GraphQL permet de développer des APIs, il a été conçu dans l'optique de palier aux soucis de REST. Nous verrons, avec un peu de pratique, les avantages de GraphQL. Pour plus d'informations je vous conseille de jeter un coup d'œil au site officiel.

Ce que nous allons faire ensemble c'est de créer un serveur GraphQL afin de pouvoir faire des requêtes. Il existe de nombreuses plateformes nous permettant de faire cela, pour ce tuto nous allons nous focaliser sur l'implémentation de nos Query et Mutation (ne vous inquiétez je vais en parler plus bas 😉 ) et non sur la configuration du serveur, pour cela nous allons utiliser Graphpack qui permet de créer un serveur GraphQL avec un minimum de configuration.

Il faut savoir aussi que GraphQL peut se mettre en façade d'APIs existantes, c'est d'ailleurs ce que nous allons voir ici.

Assez parler théorie, rentrons dans le vif du sujet.

Let's start

Rien de mieux que la pratique pour comprendre un nouveau concept, j'ai mis à votre disposition le projet sur Github. Afin de suivre pas-à-pas ce tuto, clonez le projet et mettez-vous sur le dossier start. Pour les moins patients d'entre vous, la version finale se trouve, vous l'aurez deviné, sous le dossier final.

Regardons ensemble l'arborescence du projet :


root
|───datasources
|    |─characters.js
|───src
|    |─resolvers.js
|    |─schema.graphql
|───package.json


Dans le package.json je n'ai fait que suivre la documentation de Graphpack pour les dépendances et les scripts.
Parlons plutôt des deux autres fichiers du projet :

  • schema.graphql : C'est ici que nous allons mettre la définition des modèles ainsi que les différents type de requêtes que nous allons exposer.
  • resolvers.js : Les resolvers servent grossièrement à faire le mapping entre le schéma et le code, pour dire, par exemple, que telle requête va exécuter telle logique.
  • Le dossier datasources contiendra les sources de données.

Que direz-vous d'un petit "Hello world!" pour commencer ?

Hello world!

La première chose à faire c'est de modifier notre schema.graphql, nous allons créer une requête hello qui renvoie un simple string. Pour cela GraphQL a son propre langage pour les schémas (SDL — Schema Definition Language), extrêmement simple à utiliser et à comprendre et qui est complétement agnostique aux Frameworks, nous en parlerons plus en détail plus tard, now let's write :

schema.graphql :


type Query {
   hello: String
}


Maintenant il faut définir ce que la query hello() doit renvoyer et c'est là que les resolvers entrent en jeu :

resolvers.js :


const resolvers =  {
   Query: {
      hello: () => 'Hello world!'
   }
}
export default resolvers;

Voilà c'est tout :), maintenant il suffit de lancer le server :


npm i (si ce n'est pas déjà fait)
npm run dev
...
...
Server ready at http://localhost:4000

Je vous laisse tester votre requête, vous verrez que c'est assez intuitif. Pendant ce temps je vais vous faire un topo sur la partie client.

C'est ici que nous allons définir nos query
Ici la réponse de notre appel sera afficher
On trouvera dans cette zone, la description de notre schéma, en gros notre documentation

Maintenant que nous avons fait notre Hello world, nous allons passer au vif du sujet, faire une API en GraphQL. Comme source de données nous allons nous baser sur une API REST qui nous renvoie la liste des personnages de Game Of Thrones (oui il y en a beaucoup)

API Game Of Thrones

Configuration du client HTTP :

Tout d'abord nous allons installer un client Http afin de pouvoir faire des requêtes vers l'API REST. J'ai opté pour axios. Mettez-vous sur le projet (dossier start) et lancez cette commande => npm i axios. La dépendance va être automatiquement ajouté à votre package.json.

Maintenant on va créer un fichier js à la racine du dossier datasources, qu'on va nommer gameOfThronesApi.js ou ce que vous voulez, j'avoue que je ne suis pas très inspiré 😀


root
|───datasources
|    |gameOfThronesApi.js


Ce fichier va nous permettre de configurer notre client Http une seule fois et de le réutiliser pour tous les appels que nous allons faire vers cette API.

Passons à la configuration, pour cela, rajoutez ce code dans gameOfThronesApi.js :


// # gameOfThronesApi.js

const axios = require('axios');

module.exports = axios.create({
    baseURL: 'https://api.got.show/api/general'
});


Nous avons juste définie la baseURL, on peut aussi configurer d'autres choses, mais ce n'est pas le but de ce tuto, je vous laisse lire la documentation d'axios si vous avez envie d'en savoir un peu plus.

Maintenant que nous avons configuré notre client, nous allons pouvoir récupérer de la donnée. Nous allons implémenter un des DAO qui nous permettra de faire ça.

Implémentation d'un DAO

Nous allons modifier le fichier characters.js afin d'implémenter les méthodes d'accès aux données et mapper l'objet de retour en un objet interne :


const gameOfThronesApi = require('./gameOfThronesApi');
 
export default class Characters {
 
    async getAllCharacters() {
        const characters = await gameOfThronesApi.get('/characters');
        return Array.isArray(characters.data) ? characters.data.map(character => this.characterReducer(character)) : [];
    }
 
    characterReducer(character) {
        return {
            id: character._id || 0,
            male: character.male,
            house: character.house,
            slug: character.slug,
            name: character.name,
            books: character.books || [],
            titles: character.titles
        };
    }
}

Rien de bien fameux, on importe notre client HTTP gameOfThronesApi et dans la méthode getAllCharacters() on fait un get vers le endpoint /characters après ça on transforme l'objet de retour en un objet interne qui sera utilisé plus tard.

Donc là nous avons notre source de données, il ne reste plus qu'à la rendre disponible à travers GraphQL. Pour cela nous allons commencer par mettre à jour notre schéma.

Mis à jour du schéma GraphQL

Dans notre schéma nous allons définir nos objets de sortie ainsi que leur type en utilisant le GraphQL SDL :


type Query {
    characters : [Character]
}

type Character {
    id:       ID!,
    male:      Boolean
    house:     String,
    slug:      String,
    name:      String,
    books:     [String],
    titles:    [String]
}

Ici nous avons deux parties distinctes :

  • type Query : C'est ici que nous définirons nos requêtes et ce sont ces requêtes qui seront déclarées dans nos resolvers, ici la requête characters nous renvoie une liste de types Character ([Character]). Ce type est défini plus bas.
  • type Character : C'est un type d'objet GraphQL que nous avons créé qui est formé de fields ayant un type d'objets standard. Le seul type un peu "bizarre" est ID!, c'est pour définir l'identifiant unique de l'objet, le ! signifie que l'objet ne peut être null. Pour plus de détails, je vous conseille de lire cette documentation.

Maintenant que nous avons les sources de données et le format des objets de sortie, il nous reste juste à implémenter nos resolvers pour "mapper" la requête à la donnée.

Implémentation des resolvers

Chaque resolver accepte 4 arguments :


fieldName: (parent, args, context, info) => result

  • parent : L'objet qui contient le résultat renvoyé par le resolver parent (nous ne l'utiliserons pas ici).
  • args : Un objet qui contient les arguments passés dans la requête. Par exemple characters(name: "Aegon"), l'objet args aura pour valeur {"name": "Aegon"}.
  • context : C'est un objet partagé par tous les resolvers, il peut contenir par exemple les informations d'authentification.
  • info : Contient les informations sur le statut de l'exécution de la requête.

Pour le moment nous n'allons utiliser aucun de ces arguments, du coup on mettra des parenthèses vides 😀


// # resolvers.js
import Characters from './datasources/characters';

let characters = new Characters();

const resolvers =  {
    Query: {
        characters: async () => characters.getAllCharacters(),
    }
}

export default resolvers;


Ici on dit que pour la query characters il faut exécuter la fonction characters.getAllCharacters() tout simplement.

Relançons le server :


npm run dev
...
...
Server ready at http://localhost:4000

Vous pouvez tester vos requêtes en utilisant l'autocomplétion. Vous remarquerez que le client GraphQL vous impose de rentrer exactement les champs dont vous avez besoin et ce pour éviter de faire du over-fetching.

Vous constaterez aussi que l'on reçoit en retour exactement ce qu'on demande et on peut aussi aller un peu plus loin en changeant les noms des attributs de retour. Testez cette requête par exemple :


query {
    characters {
        nom: name
        livres: books
    }
}

Voici la réponse reçue :


'data': {
    'characters': [
        {
            'nom': 'Abelar Hightower',
            'livres': [
                'The Hedge Knight'
            ]
        },
        {
            'nom': 'Addam Frey',
            'livres': [
                'The Mystery Knight'
            ]
        },
        ...
    ]
}

Maintenant nous allons voir comment passer des arguments dans notre requête. Que faire si nous souhaitons faire une recherche par nom ? Tout d'abord nous allons implémenter la méthode qui fait cela dans character.js, toujours en utilisant l'API REST mais cette fois nous allons appeler ce endpoint :


// character.js
async getCharacterByName(name) {
    const characters = await gameOfThronesApi.get(`/characters/${name}`);
    return this.characterReducer(characters.data.data);
}

Ensuite nous allons modifier notre Query GraphQL comme suit :


// schema.graphql
type Query {
    characters : [Character]
    character(name: String!) : Character
}


On passe tout simplement un argument non null de type String à notre requête et elle nous renvoie un objet de type Character.
Finalement, et c'est la partie la plus intéressante, nous allons ajouter un resolver dans resolver.js :


// resolver.js
const resolvers =  {
    Query: {
        characters: async (parent, args, context, info) => characters.getAllCharacters(),
        character: async (parent, {name}) => characters.getCharacterByName(name)
    }
}

On met notre argument en deuxième position dans notre liste d'argument et le tour est joué, nous pouvons faire nos requêtes :


query {
    character(name: 'Aegon Targaryen') {
        name
        male
        titles
    }
}

Et on reçoit la réponse :


{
    'data': {
        'character': {
            'name': 'Aegon Targaryen (son of Aenys I)',
            'male': true,
            'titles': [
                'Prince'
            ]
        }
    }
}

Vous remarquerez que l'API que nous utilisons ne fait pas réellement une recherche par nom, elle renvoie le premier personnage dans la liste et c'est tout, mais bon ce n’est pas le sujet 😀

Nous avons vu ce qu'était une Query GraphQL, elle nous permet de récupérer de la donnée (l'équivalent du GET en REST). Que faire si nous souhaitons modifier de la data ? Eh bien on utilise ce qu'on appelle les Mutations.

Mutation

Les mutations sont l'équivalent du POST, PUT et DELETE en REST. Elles sont utilisées pour écrire, modifier ou supprimer de la donnée. Regardons ensemble comment s'en servir.

D'abord, nous allons créer dans le dossier datasources un fichier houses.js qui contiendra une (petite) liste des maisons de Game of Thrones :


let houses = [
   { id: 1, name: 'House Stark', words: 'Winter is Coming' },
   { id: 2, name: 'House Targaryen', words: 'Fire and Blood' },
   { id: 3, name: 'House Baratheon', words: 'Ours is the Fury' },
   { id: 4, name: 'House Greyjoy', words: 'We Do Not Sow' },
];

export default houses;

Ensuite nous allons mettre à jour le schéma pour définir les requêtes qui nous permettront plus tard de modifier les données de cette liste :


//  schema.graphql
type Mutation {
    """
    Create a new house
    """
    createHouse(id: ID!, name: String!, words: String): [House]!,

    """
    Update an existing house
    """
    updateHouse(id: ID!, name: String!, words: String): [House]!,

    """
    Delete a house
    """
    deleteHouse(id: ID!): [House]!,
}

type House {
    id:       ID!,
    name:      String,
    words:      String
}

Vous remarquerez que j'ai rajouté des lignes entourées de """, c'est pour documenter notre API. Vous verrez cette description dans le client GraphQL, section "Schema".

Maintenant que tout est prêt nous allons implémenter nos resolvers :


// resolvers.js
const resolvers = {
    Query: {
        characters: async (parent, args, context, info) => characters.getAllCharacters(),
        character: async (parent, {name}) => characters.getCharacterByName(name),
        houses: () => houses
    },
    Mutation: {
        createHouse: (parent, {id, name, words}) => {
            let newHouse = {id, name, words};
            houses.push(newHouse);
            return houses;
        },
        updateHouse: (parent, {id, name, words}) => {
            let houseToUpdate = houses.find(house => house.id == id);
            houseToUpdate.name = name;
            houseToUpdate.words = words ? words : houseToUpdate.words;
            return houses;
        },
        deleteHouse: (parent, {id}) => {
            let houseIndex = houses.findIndex(house => house.id == id);
            console.log(houses)

            if (houseIndex === -1) {
                throw new Error('House not found.');
            }

            return houses;
        }
    }
}

Nous avons créé un nouvel attribut Mutation qui contient nos resolvers avec les arguments que nous définirons en entrée.

Lançons notre server (npm run dev) et regardons ce que cela donne avec la requête suivante :


mutation{
    createHouse(id: 5, name: 'House SOAT', words: 'In sharing we trust'){
        name
        words
    }
}

Résultat :


{
    'data': {
        'createHouse': [
            {
                'name': 'House Stark',
                'words': 'Winter is Coming'
            },
            {
                'name': 'House Targaryen',
                'words': 'Fire and Blood'
            },
            {
                'name': 'House Baratheon',
                'words': 'Ours is the Fury'
            },
            {
                'name': 'House Greyjoy',
                'words': 'We Do Not Sow'
            },
            {
                'name': 'House SOAT',
                'words': 'In sharing we trust'
            }
        ]
    }
}

On voit bien qu'une nouvelle maison a été ajouté.

Conclusion

Nous avons créé un serveur GraphQL, utilisé les Query pour récupérer de la donnée et les Mutations pour modifier ces données. Nous avons également vu les avantages de GraphQL par rapport à REST, notamment le fait de récupérer exactement ce que l'on souhaite en un seul appel. Plus de over-fetching ni de N+1 call.

Pour aller plus loin je vous conseille le tuto sur le site d'Apollo. Apollo étant une implémentation de GraphQL permettant la création d'un client/serveur, d'ailleurs Graphpack est basé dessus.
Nous verrons d'autres aspects plus poussés dans un prochain article, stay tuned 😉

© SOAT
Toute reproduction interdite sans autorisation de l’auteur.

Nombre de vue : 161

AJOUTER UN COMMENTAIRE