GraphQl – Wrapper une API Rest

Prérequis

  • connaissance standard rest
  • javascript
  • graphQl

Ressources :

Propos de base

Aujourd’hui je vais vous parler de GraphQl et plus particulièrement de comment intégrer GraphQl autour une infrastructure existante.
Arriver sur un projet où tout est à construire de zéro, c’est rare et bien souvent nous allons devoir faire avec de l’existant.
Cet existant est parfois adéquat pour ce qui est déjà en place mais ne s’adapte pas spécialement aux nouveaux besoins : par exemple un site pensé pour le desktop, qui souhaite se diversifier et proposer une expérience mobile.

Dans cet article je vais me baser sur une contrainte rencontrée lors du développement de ma PWA (en rapport avec le jeu Magic The Gathering).

Pour en savoir plus sur mon application il faudra être patient et attendre un futur article à ce sujet.

Contrainte

Mon projet, propose une expérience sur desktop et sur mobile. Il est dépendant d’une API externe sur laquelle je n’ai aucun contrôle, je suis un simple consommateur de cette API.
Cette API n’est pas officielle au jeu Magic The Gathering mais appartient à une société qui propose une marketplace.

Problématique

J’utilise l’API pour afficher des informations sur une carte Magic, notamment le prix.
Pour obtenir cette information je dois requêter cette url :

GET https://api.cardmarket.com/ws/v2.0/products/{idProduct}

Qui me retourne un json avec tout ce dont j’ai besoin

{
    "product": {
        "idProduct": "",    // Product ID
        "priceGuide": {     // Price guide entity
            "SELL": "",
            ...
        },
        ...
    }
}

Jusque là aucun problème, mais petite subtilité, le idProduct utilisé n’est pas le même que l’ID de la carte officielle, mais bien un ID spécifique à la société qui tient la marketplace.
J’ai donc besoin de récupérer cette information au préalable pour pouvoir obtenir le prix.

GET https://api.cardmarket.com/ws/v2.0/products/find?search={cardName}&idGame={idGame}&idLanguage={idLanguage}

Ce qui me retourne un tableau de product car un nom de carte n’est pas unique et sans le prix.

{
    "product": [
        {
            "idProduct": "",    // Product ID
            "enName": "",       // English Name
            "locName": ""       // Specific lang name
            ...
        }
    ]
}

Je suis donc contraint de faire un premier appel, d’attendre le résultat et de faire un second appel avec idProduct afin d’obtenir l’information que je souhaite. C’est long, c’est coûteux pour l’utilisateur. Ce n’est pas optimisé.
Cette façon de faire apporte quelques problématiques :

  • Si entre le premier appel et le second appel l’utilisateur perd la connexion, je dois lui signaler qu’une erreur s’est produite. Je dois être en mesure de relancer juste le second appel. Rien d’insurmontable mais en terme d’UX on peut mieux faire.
  • Si je fais cet appel coté client, j’ai deux appels GET qui partent, on multiplie par le nombre d’utilisateurs, ça fait beaucoup.
  • Si je contacte directement depuis le client l’API, je récupère plusieurs informations dont je n’ai pas forcément besoin

Le top pour moi serait d’avoir les deux résultats dès le premier appel sous cette forme :

{
    "product": [
        {
            "idProduct": "",    // Product ID
            "enName": "",       // English Name
            "locName": "",       // Specific lang name
            "priceGuide": {     // Price guide entity
                "SELL": "",
                ...
            },
            ...
        },
        {
            "idProduct": "",    // Product ID
            "enName": "",       // English Name
            "locName": "",       // Specific lang name
            "priceGuide": {     // Price guide entity
                "SELL": "",
                ...
            },
            ...
        }
    ]
}

Solutions

Pour obtenir ce résultat, je vais procéder en deux étapes :

  • Déporter ces appels coté serveur, l’utilisateur ne fera qu’une requête
  • Utiliser le serveur GraphQl déjà présent pour créer des modèles et obtenir seulement l’information dont j’ai besoin

Créations du modèle et resolver

Tout d’abord je crée le modèle que je vais utiliser, en me basant sur les propriétés renvoyées par l’API.

const MkmProduct = new GraphQLObjectType({
  name: "MkmProduct",
  description: "Something that you used to know",
  fields: () => ({
    idProduct: {
      type: GraphQLID,
    },
    name: {
      type: GraphQLString,
      description: "Name of the card",
      resolve: (obj) => obj.enName,
    },
    image: {
      type: GraphQLString,
      description: "Path to image",
    },
    priceGuide: {
      type: new GraphQLObjectType({
        name: "PriceGuide",
        fields: () => ({
          SELL: { type: GraphQLFloat }, // Average price of articles ever sold of this product
          LOW: { type: GraphQLFloat }, // Current lowest non-foil price (all conditions)
          LOWEX: { type: GraphQLFloat }, // Current lowest non-foil price (condition EX and better)
          LOWFOIL: { type: GraphQLFloat }, // Current lowest foil price
          AVG: { type: GraphQLFloat }, // Current average non-foil price of all available articles of this product
          TREND: { type: GraphQLFloat }, // Current trend price
          TRENDFOIL: { type: GraphQLFloat },
        }),
      }),
      resolve: (product) => fetchCardDetails(product.idProduct), // Fetch the product with the idProduct `product.idProduct`,
    },
  }),
});

On voit bien dans mon model que j’utilise la propriété resolve ce qui me permet de faire du mapping sur les propriétés. Je n’ai pas besoin de enName pour l’instant donc je « mapp » la valeur dans l’attribut name – qui est ce que j’utilise dans mon application.

    name: {
      type: GraphQLString,
      description: "Name of the card",
      resolve: (obj) => obj.enName,
    },

On remarque aussi que j’utilise le resolver afin de faire mon appel à l’API et obtenir la donnée qui me permet d’alimenter la propriété priceGuide

priceGuide: {
      type: new GraphQLObjectType({
        name: "PriceGuide",
        fields: () => ({
            ...
        }),
      }),
      resolve: (product) => fetchCardDetails(product.idProduct), // Fetch the product with the idProduct `product.idProduct`,
    },

Au final mon model permet de gérer le mapping de propriété et de faire la composition de ma donnée via un second appel. On pourrait imaginer faire un appel en bdd pour alimenter encore une autre propriété par exemple.

Ensuite une fois mon modèle prêt, je peux l’utiliser dans la RootQuery et je me sers encore une fois du resolve disponible pour indiquer que la data doit être récupérée via un appel à l’API – grâce aux méthodes fetchCardData et fetchCardDetails.

const RootQuery = new GraphQLObjectType({
  name: "RootQueryType",
  fields: {
    mcmCards: {
      //allcard
      type: new GraphQLList(MkmProduct),
      args: {
        name: {
          type: GraphQLString,
        },
      },
      resolve: (root, arg) => fetchCardData(arg), // Fetch the idProduct of card from the REST API,
    },
  },
});

Je vous ajoute ici les deux méthodes que j’appelle et les sous méthodes utilisées pour préparer l’appel.

// permet de générer mon token auth demandé par l'api
function getInit(url) {
  return Connector.getMkmHeader(url);
}
// permet de faire mon appel GET à l'api avec les paramètres nécessaire, et retourne la réponse en json
function fetchResponseByURL(relativeURL) {
  const url = new URL(`${MKM_URL}${relativeURL}`);
  return fetch(url, {
    method: "get",
    headers: getInit(url),
  })
    .then((res) => res.json())
    .catch((err) => console.log(err));
}
// retourne le tableau de produit, que me renvoie l'api
function fetchCardData({ name }) {
  const uri = `products/find?search=${name}&idGame=1&idLanguage=1`;
  return fetchResponseByURL(uri).then((json) => json.product);
}
// retourne pour chaque produit le priceGuide
function fetchCardDetails(idProduct) {
  const uri = `products/${idProduct}`;
  return fetchResponseByURL(uri).then((json) => json.product.priceGuide);
}

Résultat

Je peux désormais tester ma requête :

Et j’obtiens le résultat escompté !

Donc grâce à un seul appel coté client, j’arrive à obtenir la concaténation du résultat de deux appels à une API Rest.
Cela permet rapidement et facilement d’adapter de l’existant à de nouveaux besoins, sans pour autant tout remettre en question et devoir changer un comportement existant.

Le mot de la fin

Cette solution permet une mise en oeuvre rapide et fonctionnelle, même si on pourra toujours trouver plus optimisé ou plus propre.
Selon les priorités de votre projet, pouvoir gagner du temps et avoir quelque chose de fonctionnel, peut s’avérer plus utile que de dépenser du temps et des ressources dans une lourde refonte. #QuickAndDirty

S’abonner
Notifier de
guest

1 Commentaire
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
trackback

[…] #GraphQl Cyril partage son expérience pour intégrer GraphQl autour une infrastructure existante. kanoma.fr/blog/graphql-w… #developer #technology […]