Utiliser plusieurs API GraphQL avec Urql
Si vous êtes familier avec GraphQL, vous avec sûrement déjà eu besoin de récupérer des données sur des services tiers en parallèle de votre API GraphQL principale. Dans cet article, je résume comment j'ai réussi à utiliser plusieurs API GraphQL via un seul et unique client Urql.
Introduction
Récemment, j’ai dû migrer ma stack sous Apollo Client vers une autre librairie GraphQL. En cause, des problématiques de performances en partie dues à la façon dont la libraire parcourt l'arbre et les requêtes, provoquant des baisses de score Google lighthouse et empêchant la bonne indexation des pages sur la SERP Google. La librairie est lourde, et possède trop d’outils que je n’utilise pas.
En remplacement, j’ai opté pour Urql. Basée sur des observables, et plutôt légère, c'est une librairie avec une vision à l'opposé de ce que propose Apollo : à la place d’un outil « tout en un » pour répondre à toutes les problématiques, je compose moi-même mon « menu » avec des modules supplémentaires que j'ajoute en fonction de mes besoins. Cette approche me laissant le soin de contrôler finement ce qui est ajouté dans mon bundle front-end.
Mais voilà, une des fonctionnalité d’Apollo Client que j’apprécie beaucoup, c’est la composition des
Link, me permettant, en autre, de créer plusieurs
HttpLinkpour plusieurs sources de données. Si vous êtes habitué à utiliser GraphQL, vous avez peut-être déjà dû vouloir récupérer de la donnée en parallèle de votre API GraphQL principale (d'un service tiers par exemple). Malheureusement, je n'ai pas trouvé d'information en ligne expliquant comment faire la même chose en passant par Urql. La plupart des réponses se résumant à dire « pourquoi utiliser plusieurs sources, ce n'est pas dans l’esprit GraphQL » (ce qui est vrai, en soit, mais je n'ai pas toujours le temps créer un serveur GraphQL pour fédérer deux sources de données).
Donc, si vous souhaitez savoir si il est possible de gérer plusieurs sources de données avec Urql et comment le faire, ne cherchez plus vous avez trouvé l'article qu'il vous faut.
Pré-requis : installation des modules nécessaires
Urql est un client GraphQL qui repose sur une collection de modules qui se connectent entre eux. Comme j’ai pu vous le présenter en introduction, c’est ce qui nous permet de composer la liste exhaustive des outils dont on va se servir. Voyez ces modules comme des opérateurs dans le monde des observables. Ça tombe bien, c’est exactement ce qui fait le fonctionnement de la librairie.
Ces modules, appelés des
exchange, apportent les briques qui modifient les requêtes aux serveurs GraphQL.
L’ordre est important et doit être logique en fonction des transformations que vous souhaitez appliquer. D'après la documentation officielle, il faut toujours commencer par les tâches synchrones pour finir par celles asynchrones.
Lorsque vous configurez un client Urql, vous avez accès, par défaut, à certains exchange :
dedupExchange
: assure que des requêtes identiques ne sont pas exécuté en doublecacheExchange
: met le résultat des requêtes en cachefetchExchange
: se charge d'exécuter la requête HTTP (en utilisant l'APIfetch
)
Pour notre problématique, nous aurons besoin d’un
exchangequi n'est pas présent dans le core Urql :
authExchange.
De base, il permet de configurer facilement un système d'authentification (par exemple via JWT), mais il va surtout nous servir à modifier le contexte de notre requête à la volée.
Commençons par l’installer :
# Avec Yarn$ yarn add @urql/exchange-auth# Avec NPM$ npm install @urql/exchange-auth
Et importons le :
import { authExchange } from '@urql/exchange-auth'
Création du client GraphQL
Cette fonction devra retourner la méthode de création du client Urql avec les bons paramètres de configuration et les différents
exchangeque vous souhaitez implémenter :
import { Client, createClient } from 'urql'export function createGraphqlClient(): Client {return createClient({url: 'your-api-url', // Le client que vous souhaitez utiliser "par défaut"fetchOptions: {credentials: 'same-origin',method: 'GET',headers: {Authorization: `Bearer ...`,'Content-Type': 'application/json',}},preferGetMethod: true,exchanges: [],})}
Il nous suffit ensuite d’ajouter les
exchangeUrql de "base" (en savoir plus) :
import { ..., dedupExchange, cacheExchange, fetchExchange } from 'urql'export function createGraphqlClient(): Client {return createClient({//...exchanges: [dedupExchange,cacheExchange,fetchExchange],})}
Ajout de l'exchange d'authentification
Une fois le client configuré avec les options de base, on peut s'attaquer à l'ajout de
authExchange.
L’idée globale est de calquer la méthode qu’on utiliserait avec Apollo Client (et les
Link) pour gérer plusieurs sources de données. (Si vous souhaitez en savoir plus sur cette méthode, personnelement je me sers de l’article de Jamal Mashal que vous trouverez ici).
Pour résumer, on va ajouter dans le contexte de chaque requête (via le client ou via une intégration comme les hooks React) une clé
clientName, optionnelle, qui, servira de condition pour la modification du reste du contexte (URL de la requête et potentiellement des headers si besoin d'un token d'authentification).
L'exchange
authExchangeprend en paramètre un objet qui nécessite 2 clés au minimum :
addAuthToOperationet
getAuth.
Pour
addAuthToOperation:
addAuthToOperation({ operation }) {// si la clé clientName n'existe pas dans le contexte, on retourne l'opération couranteif (!operation.context.clientName) {return operation}const { clientName, fetchOptions } = operation.contextconst options = typeof fetchOptions === 'function' ? fetchOptions() : fetchOptions ?? {}switch (clientName) {case 'endpoint-a': {const headers = {...options.headers,Authorization: `Bearer ...`,}const context = {...operation.context,url: 'endpoint-a-url',fetchOptions: {...options,headers,}}return makeOperation(operation.kind, operation, context)}case 'endpoint-b': {const headers = {...options.headers,Authorization: `Bearer ...`,}const context = {...operation.context,url: 'endpoint-b-url',fetchOptions: {...options,headers,}}return makeOperation(operation.kind, operation, context)}default: {throw new Error(`Unexpected object: ${clientName}`)return operation}}}
Ajoutez autant de cas que de sources de données nécessaires.
Pour
getAuthdans notre exemple ce sera simple :
getAuth: null
Si besoin, vous pouvez y ajouter de la logique, comme récupérer un token avec une promesse par exemple :
getAuth: async () => {const token = await getToken()return { token }},addAuthToOperation({ authState, operation }) {const token = authState.token// ...}
Exemple de requête avec le client GraphQL
Pour utiliser la bonne API avec le client GraphQL :
function getDataFromEndpointA() {const { data } = await client.query(myQueryForEndpointA, {}, {clientName: 'endpoint-a',}).toPromise()return data}function getDataFromEndpointB() {const { data } = await client.query(myQueryForEndpointB, {}, {clientName: 'endpoint-b',}).toPromise()return data}
Exemple de requête avec les hooks React
Pour utiliser la bonne source en passant par une intégration du client Urql (comme via les hooks React) :
const [{ error, data, fetching }] = useQuery(myQueryForEndpointA,{} // cet objet contient les variables de la requête,context: useMemo(() => ({clientName: 'endpoint-a'}), []),})const [{ error, data, fetching }] = useQuery(myQueryForEndpointB,{} // cet objet contient les variables de la requête,context: useMemo(() => ({clientName: 'endpoint-b'}), []),})
Note : pensez à utiliser
useMemoquand vous définissez le contexte pour éviter de rentrer dans une boucle infinie avec React. (voir l'issue sur Github)
Conclusion
Vous devriez maintenant avoir suffisamment d’informations pour mettre en place un client Urql qui gère plusieurs sources de données.
Notes :
le code actuel n’est pas 100% « type safe » puisque la clé
clientName
n'existe pas dans le typage Typescript du contexte Urql,ajouter une source de donnée augmente la complexité dans votre gestion des typages si vous utilisez
graphql-codegen
. On en parlera dans un prochain article.
Pour vous simplifier la vie, j’ai créé ce Gist avec l’ensemble des bouts de code de l’article. N'hésitez pas à y ajouter un commentaire si vous souhaitez apporter votre avis ou votre aide.
Et si jamais vous avez la moindre question (ou si vous souhaitez juste discuter), vous pouvez me contacter via Twitter.
FAQ en rapport avec l'article
Qu'est-ce qu'Urql ?
Urql est un client GraphQL hautement personnalisable. C'est une alternative, plus légère, fonctionnelle et facile à utiliser, à Apollo Client. Il est pensé pour être à la fois facile à utiliser pour les novices de GraphQL, et à la fois prêt pour supporter des applications nécessitant une infrastructure GraphQL plus poussée.
Qui suis-je ?
Je m'appelle Antoine Lin, j'ai 25 et je vis près de Paris. J'ai grandi en Seine-et-Marne et j'ai fait mes études à l'école HETIC, Montreuil. Je suis développeur indépendant, et depuis 2015, j'accompagne des marques et des influenceurs en leur livrant des expériences web sur-mesure.
Quelle est mon activité ?
Je suis développeur front-end, je m'occupe de créer des expériences web (de la création du parcours utilisateur, à la connexion avec des services en passant par l'intégration des interfaces). Il m'arrive de faire du back-end et du devops au besoin aussi.
Les dernières publications
Que ce soit du choix de la plateforme, à la façon dont je gère mes données avec DatoCMS, ou pour parler des diverses difficultés que j'ai pu rencontrer. Je reviens, dans cet article, sur les détails de l'ouverture de mon blog, antoinelin.com.