GraphQL
GraphQL est un langage de requêtes pour API permettant de récupérer exactement les données demandées au format JSON.
Informations
Date de publication :
Date de modification :
Catégories : graphql, base de donnée, requête, json
Auteur :
meezyr

GraphQL est un langage de requêtes pour API qui permet d'interroger et de manipuler des données, c'est-à-dire que le client demande précisément les champs dont il a besoin et n'obtient que ces données dans un unique fichier JSON, évitant ainsi le sur-chargement ou le sous-chargement d'informations. Pour en savoir plus sur GraphQL, consultez la documentation officielle.
Fonctionnement général
Lorsqu'un service GraphQL est déployé (généralement accessible via un point d'entrée HTTP unique sur un serveur web), il réceptionne les requêtes émises par les clients pour les valider puis les exécuter. La phase de validation garantit que l'opération demandée cible uniquement les types et les champs enregistrés dans le schéma de l'API. Une fois la requête validée, le serveur exécute les fonctions de résolution associées pour construire le résultat attendu.
Exemple d'une requête GraphQL simple :
{
me {
name
}
}
Résultat renvoyé par le serveur :
{
"data": {
"me": {
"name": "Luke Skywalker"
}
}
}
Si vous souhaitez faire évoluer votre API et inciter les clients à employer de nouvelles structures de champs, vous pouvez marquer un champ comme obsolète à l'aide de la directive @deprecated :
type User {
fullName: String
nickname: String
name: String @deprecated(reason: "Use `fullName`.")
}
Pour élaborer une requête GraphQL, la démarche pas à pas est la suivante :
- Ouvrir l'objet racine représenté par les accolades
{}. - Cibler le champ souhaité depuis la racine (ex:
hero). - Définir la sélection de sous-champs sur l'objet retourné (ex:
nameetappearsIn).
Ce qui donne la structure suivante :
{
hero {
name
appearsIn
}
}
Schémas et système de typage
Le système de types de GraphQL définit l'ensemble des données qu'un client peut interroger. Cet ensemble de règles et de définitions constitue le schéma de l'API. C'est à partir de ce contrat de schéma que les applications clientes peuvent formuler leurs requêtes.
Types d'objets et champs
Les éléments fondamentaux d'un schéma GraphQL sont les types d'objets. Un type d'objet représente une structure de donnée récupérable, composée de multiples champs. Par exemple :
type Character {
name: String!
appearsIn: [Episode!]!
}
Chaque champ présent dans un type d'objet GraphQL peut accepter zéro, un ou plusieurs arguments. C'est le cas du champ length présenté ci-dessous :
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}
Tout schéma GraphQL doit comporter un point d'entrée pour les opérations de lecture. Par convention, ce type racine est un objet standard nommé Query. Ainsi, face à la requête suivante :
Opération :
{
droid(id: "2000") {
name
}
}
Réponse :
{
"data": {
"droid": {
"name": "C-3PO"
}
}
}
Cela implique que le schéma du service GraphQL contient la définition d'un type Query possédant le champ droid :
type Query {
droid(id: ID!): Droid
}
Les 6 types fondamentaux de GraphQL
Le langage de schéma GraphQL repose sur 6 catégories de types :
- Scalar Type (ex:
scalar Date) - Object Type (ex:
type Character) - Interface Type (ex:
interface Character) - Union Type (ex:
type Character) - Enum Type (ex:
enum Episode) - Input Object Type (ex:
input ReviewInput)
Directives de schéma
Lorsque les arguments de champs ne permettent pas de couvrir un besoin d'exécution ou qu'un comportement transverse doit être appliqué sur plusieurs éléments, les directives entrent en jeu. Précédées du symbole @, elles modifient le traitement d'une partie du schéma ou d'une opération.
Exemple d'utilisation :
type User {
fullName: String
name: String @deprecated(reason: "Use `fullName`.")
}
Déclaration de la directive au niveau du schéma :
directive @deprecated( reason: String = "No longer supported" ) on FIELD_DEFINITION | ENUM_VALUE
Documentation et commentaires
Descriptions de schéma
GraphQL prend en charge la documentation intégrée pour clarifier le rôle des types, champs et arguments. Les descriptions sont rédigées en syntaxe Markdown et peuvent être sur une seule ligne (guillemets simples "...") ou sur plusieurs lignes (triples guillemets """...""") :
"""
A character from the Star Wars universe
"""
type Character {
"The name of the character."
name: String!
}
Commentaires de code
Vous pouvez ajouter des commentaires dans le code source d'un schéma ou d'une requête en utilisant le caractère #. Ces lignes sont ignorées lors de l'exécution :
# This line is treated like whitespace and ignored by GraphQL
type Character {
name: String!
}
Gestion des Requêtes (Queries)
GraphQL orchestre trois grands types d'opérations : les requêtes (query), les mutations (mutation) et les abonnements (subscription).
Voici des exemples de structures de requêtes simples :
{
hero {
name
friends {
name
}
}
}
{
human(id: "1000") {
name
height(unit: FOOT)
}
}
Pour rendre le code plus explicite et faciliter le débogage, il est vivement recommandé de nommer l'opération et d'indiquer explicitement le mot-clé query :
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
Pagination des résultats
Pour restreindre le nombre d'éléments renvoyés, vous pouvez transmettre un argument de limite tel que first. La requête ci-dessous permet de récupérer les 2 premiers éléments :
query {
hero {
name
friends(first: 2) {
name
}
}
}
Afin de gérer une pagination robuste, une couche d'abstraction par "arêtes" (edges) et "nœuds" (nodes) peut être introduite. Chaque arête fournit ainsi le nœud de donnée et son curseur de pagination correspondant :
query {
hero {
name
friends(first: 2) {
edges {
node {
name
}
cursor
}
}
}
}
Pour obtenir les métadonnées de navigation (connaître l'existence d'une page suivante ou récupérer les curseurs de fin), ajoutez le champ pageInfo :
query {
hero {
name
friends(first: 2) {
totalCount
edges {
node {
name
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
Exemple complet d'une requête paginée avec curseur (after) et sa réponse JSON associées :
Opération :
query {
hero {
name
friendsConnection(first: 2, after: "Y3Vyc29yMQ==") {
totalCount
edges {
node {
name
}
cursor
}
pageInfo {
endCursor
hasNextPage
}
}
}
}
Réponse :
{
"data": {
"hero": {
"name": "R2-D2",
"friendsConnection": {
"totalCount": 3,
"edges": [
{
"node": {
"name": "Han Solo"
},
"cursor": "Y3Vyc29yMg=="
},
{
"node": {
"name": "Leia Organa"
},
"cursor": "Y3Vyc29yMw=="
}
],
"pageInfo": {
"endCursor": "Y3Vyc29yMw==",
"hasNextPage": false
}
}
}
}
}
Utilisation des Alias
Les alias s'avèrent indispensables lorsqu'il est nécessaire de renommer les clés dans la réponse ou d'interroger un même champ plusieurs fois avec des arguments différents dans une unique requête.
Opération :
query {
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}
Réponse :
{
"data": {
"empireHero": {
"name": "Luke Skywalker"
},
"jediHero": {
"name": "R2-D2"
}
}
}
Variables de requêtes
Au lieu de hardcoder les arguments directement dans la requête, vous pouvez déclarer des variables dynamiques précédées du symbole $.
Opération :
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
Réponse (avec le dictionnaire de variables {"episode": "JEDI"}) :
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
Fragments
Les fragments permettent d'encapsuler des ensembles de champs réutilisables afin d'éviter la répétition de code dans les requêtes complexes.
Opération :
query {
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
appearsIn
friends {
name
}
}
Réponse :
{
"data": {
"leftComparison": {
"name": "Luke Skywalker",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
},
{
"name": "C-3PO"
},
{
"name": "R2-D2"
}
]
},
"rightComparison": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
],
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}
En savoir plus sur la documentation officielle des fragments : https://graphql.org/learn/queries/#fragments
Directives dans les requêtes
Les directives permettent de conditionner dynamiquement la structure des résultats renvoyés en fonction de variables booléennes (par exemple pour alterner entre un affichage condensé et une vue détaillée).
Opération :
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
Réponse (avec les variables {"episode": "JEDI", "withFriends": false}) :
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
Directives natives supportées :
@include(if: Boolean): Inclut le champ dans le résultat uniquement si l'argument vauttrue.@skip(if: Boolean): Exclut le champ du résultat si l'argument vauttrue.
Mutations : Altération des données
Toutes les opérations modifiant l'état des données (création, mise à jour, suppression) doivent employer le mot-clé mutation.
Création de données
Pour insérer de nouvelles entités, formulez une mutation recevant les données d'entrée via des variables :
Opération :
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
Variables transmises :
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
Réponse :
{
"data": {
"createReview": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
}
Mise à jour de données
Exemple de mise à jour d'un enregistrement existant :
Opération :
mutation UpdateHumanName($id: ID!, $name: String!) {
updateHumanName(id: $id, name: $name) {
id
name
}
}
Réponse (avec variables {"id": "1000", "name": "Luke Starkiller"}) :
{
"data": {
"updateHumanName": {
"id": "1000",
"name": "Luke Starkiller"
}
}
}
Mutations spécifiques vs génériques
Contrairement aux architectures REST où la mise à jour s'effectue généralement par l'envoi global d'une ressource via une requête HTTP PATCH ou PUT, GraphQL encourage l'utilisation de mutations ciblées pour chaque intention métier. Il est donc préférable de définir des champs précis comme updateHumanName ou rateFilm plutôt qu'une mutation générique updateHuman.
Opération :
mutation RateFilm($episode: Episode!, $rating: FilmRating!) {
rateFilm(episode: $episode, rating: $rating) {
episode
viewerRating
}
}
Réponse (avec variables {"episode": "EMPIRE", "rating": "THUMBS_UP"}) :
{
"data": {
"rateFilm": {
"episode": "EMPIRE",
"viewerRating": "THUMBS_UP"
}
}
}
Suppression de données
Exemple de suppression d'une ressource (équivalent conceptuel d'une requête HTTP DELETE) :
Opération :
mutation DeleteStarship($id: ID!) {
deleteStarship(id: $id)
}
Réponse (avec variables {"id": "3003"}) :
{
"data": {
"deleteStarship": "3003"
}
}
Exécution séquentielle des champs de mutation
Une même mutation peut contenir plusieurs champs. C'est une différence majeure par rapport aux requêtes classiques : alors que les champs d'une query sont exécutés en parallèle par le serveur, les champs d'une mutation sont exécutés strictement de manière séquentielle, l'un après l'autre.
Opération :
mutation {
firstShip: deleteStarship(id: "3001")
secondShip: deleteStarship(id: "3002")
}
Réponse :
{
"data": {
"firstShip": "3001",
"secondShip": "3002"
}
}
Abonnements (Subscriptions)
Les abonnements permettent aux clients de maintenir une connexion en temps réel avec le serveur GraphQL pour recevoir des notifications d'événements dès qu'ils surviennent.
Souscription aux mises à jour en temps réel
Les champs d'abonnement se définissent de la même façon que les champs des types Query et Mutation, mais s'appuient sur le type racine Subscription :
type Subscription {
reviewCreated: Review
}
Techniquement, les abonnements reposent généralement sur un système sous-jacent de type Publish/Subscribe (Pub/Sub) pour diffuser dynamiquement les flux de données aux clients connectés.
Exemples d'abonnements côté client :
subscription NewReviewCreated {
reviewCreated {
rating
commentary
}
}
subscription FriendListUpdated($id: ID!) {
humanFriendsUpdated(id: $id) {
name
friends {
name
}
}
}
Validation et Exécution
Phase de validation
Avant d'être traitée, une requête GraphQL doit satisfaire deux exigences : respecter une syntaxe valide et se conformer strictement aux types et contraintes énoncés dans le schéma grâce au système de typage fort.
Phase d'exécution et résolveurs
Lors du traitement, chaque champ est associé à une fonction d'exécution appelée "résolveur" (resolver). Par exemple, pour un champ human(id: ...) appartenant au type Query, la fonction de résolution se charge d'interroger la base de données puis d'instancier l'objet correspondant :
function resolveHuman(obj, args, context, info) {
return context.db.loadHumanByID(args.id).then(userData => new Human(userData));
}