Skip to main content

SpazioCodice

Leçons apprises, réflexions, sur GraphQL et REST, les deux paradigmes d'API les plus populaires.

GraphQL, REST : prendre le meilleurs des deux

L’article résume les leçons tirées d’un projet qui applique GraphQL et REST (Representational State Transfer) comme paradigmes pour fournir des services web.

L’objectif est d’illustrer et de motiver le contexte et les facteurs qui ont conduit à l’adoption de ces paradigmes.

Pourquoi avons-nous choisi GraphQL ?

GraphQL fournit un contrôleur principal au-dessus de diverses sources de données, qu’il s’agisse d’API, de bases de données ou d’applications arbitraires qui fournissent des données via un protocole interopérable.

Une approche centrée sur GraphQL offre de nombreuses simplifications en ce qui concerne l’accès aux données et la manipulation par rapport au paradigme bien connu de REST.

Client-driven vs Server-driven

Un système RESTful modélise un domaine donné avec une variété de points de terminaison centrés sur les ressources. Bien que cela offre une séparation claire des préoccupations concernant la cohésion du système (l’infrastructure permet de comprendre rapidement comment traiter une ressource), les choses deviennent plus compliquées lorsque nous commençons à travailler en profondeur : par exemple, qu’en est-il de l’échange entre le client et le serveur ? Et qu’en est-il des charges utiles des messages ?

Les requêtes GET sont utilisées pour récupérer les représentations des ressources. Supposons que nous voulions visiter

https://myapi.org/people/273

L’intention est claire : nous voulons obtenir une représentation de cette ressource. Le payload attendu pourrait ressembler à ceci :

				
					{
    "firstName": "James",
    "lastName": "Smith",
    ... (other fields)
    "pets": [
        "https://myapi.org/pets/992",
        "https://myapi.org/pets/192",
        "https://myapi.org/pets/61"
    ]
}
				
			

Le point que nous souhaitons mettre en évidence est le suivant : à moins que l’API n’implémente une capacité côté client pour contrôler le contenu de la ressource représentée, il est facile de tomber dans les scénarios indésirables suivants :

Over-fetching

Nous avons seulement besoin du nom de la personne, mais la réponse contient d’autres informations qui ne nous intéressent pas. En plus d’augmenter le payload de l’échange, le serveur peut avoir dépensé plus de ressources que celles réellement nécessaires.

Under-fetching

Nous avons besoin du nom de la personne et du nom de tous les animaux qu’elle possède. Le nom de la personne est présent. Et les animaux ? Nous avons besoin de trois requêtes supplémentaires (une pour chaque animal), avec une forte probabilité de tomber dans trois scénarios d’over-fetching (rappelons-nous que nous avons seulement besoin du nom de l’animal).

GraphQL résout ces deux problèmes en permettant explicitement au client de modéliser la forme de la réponse. La requête serait :

				
					{
    person(id:"273") {
        firstName
        lastName
        pets {
            name
        }
    }
}
				
			

Cela indique clairement ce que recherche le client. Voici la réponse :

				
					{
    "data": {
        "person": {
            "name": "James Smith",
            pets: [
                { "name": "Bobby" },
                { "name": "Fuffy" },
                { "name": "Lilli" }
            ]
        }
    }
}
				
			

L’interaction avec GraphQL est client-driven : la requête contient des détails sur l’opération et la forme de la réponse souhaitée.

Système de types

Revenons à notre exemple : https://myapi.org/people/273.

Supposons que nous soyons côté client, pouvons-nous faire une hypothèse sur la réponse que nous allons obtenir ? Non, nous avons dit que cela pourrait être :

				
					{
    "firstName": "James",
    "lastName": "Smith",
    ...
    "pets": [
        "https://myapi.org/pets/992",
        "https://myapi.org/pets/192",
        "https://myapi.org/pets/61"
    ]
}
				
			

mais cela pourrait aussi être quelque chose comme ceci :

				
					{
    "firstName": "James",
    "lastName": "Smith",
    ...
    "pets": [
        {
            "uri":"https://myapi.org/pets/992",
            "name": "Bobby",
            ...
        },
        ...
    ]
}
				
			

ou même :

				
					{
    "name": "James Smith",
    "pets": "https://myapi.org/people/273/pets"
}
				
			

Toutes ces réponses sont valides ; à part le protocole formel (JSON), le contenu réel dépend entièrement du serveur.

Est-ce important ? Oui, parce que la forme des données guide l’interaction entre le client et le serveur : rappelez-vous, nous cherchons le nom de la personne et le nom de leurs animaux :

  • dans le premier exemple de réponse, nous avons besoin de quatre requêtes
  • dans le deuxième exemple, une seule requête et nous avons toutes les données dont nous avons besoin
  • dans le troisième exemple, nous avons besoin de cinq requêtes

Comment GraphQL gère-t-il cela ? Il fournit une fonctionnalité appelée introspection, qui est l’un des éléments clés du protocole : un client peut émettre une requête de métadonnées spécifique pour récupérer le schéma qui guide l’interaction client-serveur.

L’introspection du schéma permet aux clients de savoir à l’avance comment une personne ou un animal est structuré, ses propriétés, ses opérations, ses paramètres et ses types de retour.

Opérations

REST définit un modèle d’interaction des ressources basé sur les méthodes HTTP. Bien que le RFC du protocole de transfert hypertexte soit très détaillé, nous ne pouvons pas nier qu’il n’a pas été créé en pensant à REST.

Cela génère souvent de la confusion lorsqu’il faut modéliser les interactions en lien avec le mappage des méthodes HTTP. Si les questions ci-dessous vous semblent familières, vous comprenez ce que je veux dire :

  • Quelle est la différence entre POST et PUT ?
  • Que faire si mon interaction est un UPSERT (par exemple, créer l’entité si elle n’existe pas, la mettre à jour si elle existe) ? POST ou PUT ?
  • Modèles de domaine organisés en hiérarchie, un mélange d’agrégations et de compositions : quelle est l’approche correcte pour créer les points de terminaison ?
  • Même point de terminaison, même ressource, paramètres différents qui influencent le comportement ?

Malheureusement, il n’est pas facile de trouver les bonnes réponses ; parfois, ce n’est pas un scénario oui/non, et vous pourriez même trouver des solutions raisonnables aux deux extrêmes.

GraphQL fournit une sémantique pour modéliser les opérations : il introduit trois types de niveau supérieur qui encapsulent le concept d’interaction :

  • Query : pour la récupération synchrone des données
  • Mutation : pour les changements d’état du modèle de domaine
  • Subscription : pour la récupération asynchrone des données

Pourquoi avons-nous choisi REST ?

Le projet où nous avons appliqué la plupart des considérations discutées dans cet article implémente une forte connexion avec les Linked Data et le Web Sémantique.

L’organisation, l’identification et la représentation des ressources sont des sujets cruciaux qui sont un peu éloignés des beautés d’accès aux données décrites dans la section GraphQL.

REST, à notre avis, est un gagnant de ce côté-là : le paradigme est centré sur la ressource, et le style architectural sous-jacent organise naturellement le domaine autour du concept de ressources d’information identifiables, organisables et représentables.

Client-driven vs Server-driven (partie 2)

Le système de types statique imposé par le paradigme GraphQL est une excellente chose, mais dans certains cas, il soulève des questions sur l’interopérabilité.

Les points de terminaison REST peuvent être intégrés en supposant des capacités d’interaction HTTP très basiques : dans un scénario minimal/trivial où un client souhaite récupérer des données, il a besoin :

D’autre part, une connexion GraphQL suppose du côté du demandeur des capacités plus sophistiquées en termes de compréhension du protocole, d’introspection, et d’interaction des requêtes/réponses. C’est le prix à payer pour déplacer côté client la responsabilité de définir la réponse et la forme de l’interaction.

Déréférencement d'URI

Le World Wide Web (WWW) est un espace d’information dans lequel les ressources sont identifiées par des identifiants globaux appelés Uniform Resource Identifiers (URI). Les ressources ont une ou plusieurs représentations qui peuvent être accédées via HTTP.

Déréférencer une URI signifie récupérer une représentation de la ressource en utilisant son URI.

C’est là que REST intervient : les points de terminaison sont liés aux ressources ; elles sont naturellement associées aux URIs ; déréférencer une URI est la première chose triviale qu’un client peut faire dans ce contexte.

Nous pourrions même créer un système GraphQL qui utilise des URIs comme identifiants pour les entités. Il n’y a rien de mal à cela ; cependant, cela n’est pas strictement imposé par le protocole, ce qui pourrait laisser place à des implémentations différentes et ambiguës.

Négociation du Type de Contenu

D’après le paragraphe précédent :

Les ressources ont une ou plusieurs représentations qui peuvent être accédées via HTTP

Lorsqu’une URI est déréférencée, le client et le serveur peuvent décider de la représentation de la ressource à utiliser parmi celles disponibles, en utilisant deux types possibles de négociation de contenu : la négociation côté serveur et la négociation côté agent.

À la fin de l’interaction, le serveur retourne la représentation de la ressource en utilisant le type de média négocié et ses capacités.

Il devrait être assez clair que tout ce mécanisme suppose un système où les ressources sont organisées et identifiées par des URIs. Comme nous l’avons dit, les URIs sont le bloc de construction d’une infrastructure RESTful.

En revanche, GraphQL a un format de réponse fixe (JSON) pour retourner les réponses.

Pseudo-Schemaless 

Dans la section GraphQL, nous avons mentionné que la capacité d’introspection, basée sur un schéma déclaratif, était l’un des facteurs clés qui nous ont poussés à adopter GraphQL.

Cependant, nous avons rencontré des scénarios où cette contrainte était plus un inconvénient qu’un avantage. Voici la description de l’un d’entre eux.

Share-VDE est une initiative basée sur des bibliothèques qui regroupe les catalogues bibliographiques et les fichiers d’autorité d’une communauté de bibliothèques dans un environnement de découverte partagé basé sur des données liées.

Les utilisateurs peuvent rechercher dans un vaste catalogue via une interface simple et intuitive.

Dans le cadre des services de recherche de la plateforme, il existe une fonction “expliquer” qui fournit des informations sur les relations entre un ou plusieurs termes et une entité donnée.

Un chemin de lecture typique est le suivant :

  • Requête de recherche : l’API de recherche permet de chercher en utilisant un ou plusieurs termes
  • Réponse de recherche : elle consiste en une liste classée de zéro, un ou plusieurs résultats
  • Requête d’explication : pour chaque résultat, il est possible d’invoquer l’API “expliquer” pour obtenir la relation entre l’entité et les termes de recherche saisis
  • Réponse d’explication : elle contient l’entité cible et ses relations avec les termes demandés.

Le modèle de domaine sous-jacent est profondément imbriqué ; l’explication ci-dessus nécessite une structure flexible pour accueillir cet enchevêtrement.

Par exemple, une recherche pour Dodgson, Charles Lutwìdge pourrait produire deux résultats :

À première vue, il n’est pas immédiatement clair pourquoi ces deux résultats apparaissent. C’est pourquoi nous avons implémenté la fonction d’explication : elle peut fournir des informations utiles comme :

  • “Alice au pays des merveilles est une œuvre écrite par Dodgson, Charles Lutwìdge.”
  • “Carroll, Lewis est aussi connu sous le nom de Dodgson, Charles Lutwìdge.”

Dans les exemples ci-dessus, la “distance” entre la cible et les entités liées est différente :

  • L’œuvre (“Alice au pays des merveilles”) a une relation avec une autre entité (une personne, l’auteur) qui a un nom variant correspondant aux termes de recherche.
  • La personne (Lewis Carroll) ne fournit pas de relation avec d’autres entités : les termes de recherche correspondent à une forme variant du nom de l’auteur.

Le niveau d’imbrication arbitraire que nous pourrions avoir dans la réponse d’explication est difficile à modéliser en utilisant un système typé comme GraphQL ; en revanche, un style REST moins rigide permet de mettre en œuvre cette structure sous forme d’une simple carte de cartes. La première réponse d’explication est :

				
					/opuses/401/explanation?terms=Dodgson Charles Lutwìdge

{
  "meta": {
    "aut": {
      "label": "author",
      "language": "eng",
      "type": "Role"
    }
  },
  "aut": [
    {
      "nameAlternative": "Dodgson Charles Lutwìdge"
    }
  ]
}
				
			

Et voici la réponse pour Lewis Carroll :

 

				
					/opuses/203/explanation?terms=Dodgson Charles Lutwìdge

{
    "nameAlternative": "Dodgson Charles Lutwìdge"
}
				
			

Conclusions

L’implication implicite entre GraphQL et REST dans le titre de cet article n’est pas une coïncidence : internet regorge de billets de blog qui comparent ces deux technologies comme si elles étaient des alternatives opposées.

Dans cette contribution sans prétention, nous avons listé et expliqué les raisons qui nous ont conduits à adopter ces deux technologies. Le niveau d’abstraction a été défini aussi haut que possible pour simplifier la lecture, sans rapporter les détails internes du projet dans lequel nous avons mis en œuvre tout ce que nous avons décrit.

Cependant, si vous avez des questions spécifiques ou si vous êtes intéressé par ce que nous faisons, n’hésitez pas à nous contacter.

À la prochaine, et rappelez-vous, tout retour est chaleureusement accueilli !

References

Facebook API Graph [1]

GraphQL: a data query language [2]

Graphql.org [3]

Hypertext Transfer Protocol — HTTP/1.1 [4]

Dereferencing HTTP URIs [5]

HTTP/1.1: Content Negotiation [6]

Share this post

Laisser un commentaire

En savoir plus sur SpazioCodice

Abonnez-vous pour poursuivre la lecture et avoir accès à l’ensemble des archives.

Poursuivre la lecture