Concevoir l'application pour qu'elle fonctionne avec différents serveurs GraphQL
« Coder contre des interfaces, pas des implémentations » est la pratique d'invoquer une fonctionnalité non pas directement, mais à travers un contrat qui énumère les entrées requises et la sortie attendue, tout en cachant la façon dont l'implémentation est réalisée. Cette stratégie aide à découpler l'application d'une implémentation, d'un fournisseur ou d'une pile spécifique, permettant de les échanger sans avoir à modifier le code de l'application.
Nous pouvons également appliquer cette stratégie avec GraphQL. GraphQL peut agir comme intermédiaire entre l'application et le serveur, nous permettant d'exécuter toutes les modifications nécessaires uniquement sur les requêtes GraphQL, en maintenant la logique métier intacte.
Une requête GraphQL agit comme une interface entre le client et le serveur. Lors de l'exécution d'une requête, le serveur GraphQL la traitera et retournera les données requises au client. D'où proviennent les données ? Comment ont-elles été obtenues ? Le client ne le sait pas, et ne s'en soucie pas.

La réponse à la requête aura la même forme que la requête. Pour cette requête GraphQL :
{
post(by: { id: 1 }) {
id
title
}
}...la réponse sera :
{
"data": {
"post": {
"id": 1,
"title": "Hello world!"
}
}
}Étant donné la même requête avec des paramètres différents, les données retournées seront différentes, mais la forme sera constante. Cela signifie que, tant que la requête ne change pas, l'application n'a pas besoin de modifier sa logique concernant la façon de lire et de traiter les données, et de même peu importe quel serveur GraphQL exécute la requête.
Et ainsi nous pouvons échanger un serveur GraphQL contre un autre en toute transparence.
Les requêtes dépendent du schéma GraphQL
Maintenant, le dernier paragraphe est un peu trop optimiste, car la requête GraphQL peut avoir besoin de changer selon le serveur GraphQL. Pour être plus précis, la requête est basée sur le schéma GraphQL, et si différents serveurs exposent des schémas différents, alors la requête sera différente aussi.
Par exemple, un serveur GraphQL qui utilise la Cursor Connections Specification peut exécuter la requête suivante :
{
categories(first: 10000) {
edges {
node {
categoryId
description
id
name
slug
}
}
}
}Et un autre serveur qui utilise la pagination à la WordPress (comme Gato GraphQL) exécutera la même requête ainsi :
{
postCategories(pagination: { limit: 10000 }) {
id
description
globalID
name
slug
}
}Nous pouvons apprécier les différences entre les deux requêtes :
| Caractéristique | Serveur #1 | Serveur #2 |
|---|---|---|
| Champ des catégories d'article | categories | postCategories |
| Argument de champ pour limiter le nombre de résultats | first | pagination.limit |
Le champ id d'un objet représente | son ID global unique | son ID unique pour son type |
| Forme de la requête | plus profonde à cause de edges.node | plus plate |
Remplacer la requête du premier serveur par l'équivalente du second à l'intérieur de l'application ne fonctionnera pas seul. C'est parce que la logique accédera toujours aux données de la réponse selon la forme et les champs de la requête originale.
Une solution possible est de remplacer également la logique pour récupérer les données dans le client. Par exemple, la logique suivante :
const categories = data?.data.categories.edges.map(({ node = {} }) => node);...peut être remplacée ainsi :
const categories = data?.data.postCategories;Mais c'est précisément ce que nous voulons éviter. Nous voulons limiter les changements au strict minimum, en modifiant uniquement l'interface (la requête GraphQL) et en maintenant la logique métier sans la modifier.
Heureusement, il est possible de combler les différences en modifiant uniquement les requêtes GraphQL, en suivant ces étapes :
- Garder les requêtes GraphQL séparées de l'application
- Adapter les noms des champs via des alias
- Adapter la forme de la réponse via un champ
self
Voyons comment, via ces 3 étapes, nous pouvons adapter une application pour pointer vers un autre serveur GraphQL.
Garder les requêtes GraphQL séparées de l'application
Séparer les requêtes GraphQL de la logique de l'application implique :
- Stocker chaque requête GraphQL (ou un groupe d'entre elles) dans un fichier séparé, et tous dans un dossier spécifique
- Exporter les requêtes et les importer dans l'application
Par exemple, nous pouvons placer chaque requête GraphQL dans un fichier séparé sous src/data, et l'exporter :
// file `src/data/categories.js`
export const QUERY_ALL_CATEGORIES = gql`
{
categories(first: 10000) {
edges {
node {
databaseId
description
id
name
slug
}
}
}
}
`;L'application peut ensuite importer et utiliser la requête GraphQL :
import { QUERY_ALL_CATEGORIES } from 'data/categories';
export async function getAllCategories() {
const apolloClient = getApolloClient();
const data = await apolloClient.query({
query: QUERY_ALL_CATEGORIES,
});
const categories = data?.data.categories.edges.map(({ node = {} }) => node);
return {
categories,
};
}Grâce à cette configuration, toutes les modifications ne doivent être effectuées que sur les fichiers sous src/data.
Adapter les noms des champs via des alias
Un alias de champ peut être utilisé pour renommer un champ dans la réponse du deuxième serveur GraphQL vers le nom de ce champ dans le premier serveur.
Ainsi, les champs postCategories, id et globalID peuvent être récupérés en utilisant les noms attendus par l'application : categories, categoryId et id respectivement :
{
categories: postCategories(pagination: { limit: 10000 }) {
categoryId: id
description
id: globalID
name
slug
}
}Veuillez noter que le champ categories a l'argument first, tandis que son champ correspondant postCategories utilise l'argument pagination.limit. Cependant, comme les arguments de champ ne sont pas reflétés dans le nom du champ dans la réponse, nous n'avons pas à nous en préoccuper.
Adapter la forme de la réponse via un champ self
Le défi final est un peu plus délicat : nous devons modifier la forme de la réponse, en ajoutant les niveaux supplémentaires pour edges et node provenant de la spec Cursor Connections.
Pour y parvenir, nous allons introduire un champ self dans tous les types du schéma GraphQL, qui renvoie le même objet sur lequel il est appliqué :
type QueryRoot {
self: QueryRoot!
}
type Post {
self: Post!
}
type User {
self: User!
}Le champ self permet d'ajouter des niveaux supplémentaires à la requête sans quitter l'objet interrogé. En exécutant cette requête :
{
__typename
self {
__typename
}
post(by: { id: 1 }) {
self {
id
__typename
}
}
user(by: { id: 1 }) {
self {
id
__typename
}
}
}...cela produit cette réponse :
{
"data": {
"__typename": "QueryRoot",
"self": {
"__typename": "QueryRoot"
},
"post": {
"self": {
"id": 1,
"__typename": "Post"
}
},
"user": {
"self": {
"id": 1,
"__typename": "User"
}
}
}
}Maintenant, nous pouvons utiliser self pour ajouter artificiellement les niveaux nodes et edge :
{
categories: self {
edges: postCategories(pagination: { limit: 10000 }) {
node: self {
categoryId: id
description
id: globalID
name
slug
}
}
}
}Le type de l'objet dans le schéma GraphQL pour edges et pour self est évidemment différent. Mais cela n'a pas d'importance pour l'application, car elle n'interagit pas avec l'objet réel modélisé dans le serveur GraphQL. Au lieu de cela, elle reçoit les données sous forme d'objet JSON, et cette portion de données pour un champ provenant d'un objet PostConnection ou d'un objet Post sera la même.
Veuillez noter que le champ categories est résolu via self et edges est résolu via postCategories, et non l'inverse. Cela permet de maintenir la cardinalité des éléments retournés correspondant à celle définie par les champs utilisant la spec Cursor Connections :
type RootQuery {
categories: RootQueryToCategoryConnection
}
type RootQueryToCategoryConnection {
edges: [RootQueryToCategoryConnectionEdge]
}
type RootQueryToCategoryConnectionEdge {
node: Category
}Si la requête GraphQL adaptée était inversée (c'est-à-dire en interrogeant categories: postCategories et edges: self), l'accès aux données échouerait, car data.categories serait un tableau, de sorte que data.categories.edges lancerait une erreur lors de l'exécution :
const categories = data?.data.categories.edges.map(({ node = {} }) => node);Adapter toutes les requêtes
Après avoir appliqué la même stratégie à toutes les requêtes GraphQL dans src/data, l'application peut facilement passer d'un serveur GraphQL à un autre.