Qu’est-ce que GraphQL ?
GraphQL est un langage de requête et un runtime pour les API. Au lieu d’exposer de nombreux endpoints fixes, un serveur GraphQL expose un unique schema typé, et les clients envoient des requêtes pour sélectionner précisément les champs dont ils ont besoin. Le serveur résout chaque champ et renvoie une réponse dont la structure correspond exactement à la requête.
Facebook l’a conçu en 2012 pour résoudre un problème lié au mobile : les endpoints REST renvoyaient soit trop de données, soit pas assez, et récupérer toutes les informations liées à un écran nécessitait de multiples allers-retours. GraphQL a corrigé ces deux problèmes en permettant au client de décrire ses besoins en données dans un seul document.
Le schéma
Le schéma est le contrat. Il définit les types ainsi que les opérations disponibles.
# schema.graphql
type User {
id: ID!
name: String!
email: String
posts(first: Int = 10): [Post!]!
}
type Post {
id: ID!
title: String!
body: String!
author: User!
}
type Query {
user(id: ID!): User
posts(limit: Int): [Post!]!
}
type Mutation {
createPost(input: CreatePostInput!): Post!
}
Le symbole ! marque un champ comme non nul. Les types s’organisent en graphe, c’est pourquoi une seule requête peut naviguer d’un utilisateur vers ses articles, puis revenir vers les auteurs. Le schéma est introspectable, ce qui permet aux outils de générer automatiquement la documentation, les types et l’autocomplétion.
Requêtes
Une requête sélectionne des champs à partir du type de requête racine, en utilisant des arguments et des variables.
# posts.graphql
query RecentPosts($limit: Int!) {
posts(limit: $limit) {
id
title
author {
name
}
}
}
La réponse reflète exactement la sélection : aucun champ supplémentaire, aucun champ manquant. Les variables permettent de rendre les requêtes réutilisables et permettent au serveur de valider les types d’arguments. Les fragments permettent d’extraire des sélections répétées pour les réutiliser.
# fragment.graphql
fragment PostCard on Post {
id
title
author { name }
}
query Feed {
posts { ...PostCard }
}
Mutations et subscriptions
Une mutation modifie des données et renvoie une structure définie, incluant souvent l’objet mis à jour afin que le client puisse actualiser son cache.
# create.graphql
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
}
}
Une subscription diffuse des mises à jour en temps réel via une connexion persistante, comme pour de nouveaux messages ou des notifications en direct. Utilisez-la lorsque le serveur doit pousser des données vers le client ; pour des rafraîchissements occasionnels, le polling ou un refetch est plus simple.
Resolvers
Les resolvers sont le point de rencontre entre votre schéma et vos données. Chaque champ possède un resolver qui retourne sa valeur.
// resolvers.js
const resolvers = {
Query: {
user: (_parent, { id }, { db }) => db.users.findById(id),
posts: (_parent, { limit = 10 }, { db }) => db.posts.findMany({ limit }),
},
User: {
posts: (user, { first }, { db }) =>
db.posts.findMany({ where: { authorId: user.id }, limit: first }),
},
Mutation: {
createPost: (_parent, { input }, { db }) => db.posts.create(input),
},
};
L’objet context transporte les ressources partagées, telles que la base de données et l’utilisateur authentifié. Les resolvers doivent rester concis, et les vérifications d’autorisation doivent être effectuées ici ou dans la couche de service qu’ils appellent — jamais côté client.
Le problème N+1
Le piège de performance le plus courant avec GraphQL est le problème des requêtes N+1. Si une requête retourne dix articles et que le resolver de l’auteur de chaque article interroge la base de données, vous exécutez onze requêtes.
DataLoader résout ce problème en regroupant (batching) et en mettant en cache les appels au sein d’une seule requête.
// loaders.js
import DataLoader from "dataloader";
function createUserLoader(db) {
return new DataLoader(async (ids) => {
const users = await db.users.findByIds(ids);
return ids.map((id) => users.find((u) => u.id === id));
});
}
Le resolver de l’auteur appelle ensuite loaders.user.load(post.authorId), et DataLoader regroupe tous les chargements du tick en une seule requête. C’est l’optimisation la plus importante pour un serveur GraphQL.
Clients et mise en cache
Les bibliothèques client telles qu’Apollo Client et urql gèrent la mise en cache, la normalisation, les états de chargement et d’erreur, ainsi que l’intégration avec les frameworks UI. Un cache normalisé stocke les entités par type et par id ; ainsi, une mutation qui renvoie un post mis à jour actualise automatiquement toutes les requêtes qui y faisaient référence.
Comme GraphQL utilise généralement un unique point de terminaison POST, la mise en cache HTTP est moins efficace qu’avec REST. Les clients compensent cela avec des caches normalisés, et les serveurs utilisent des requêtes persistantes (persisted queries) et la mise en cache des réponses. Concevez vos mutations pour qu’elles renvoient les objets modifiés afin que le cache client puisse rester cohérent.
GraphQL ou REST ?
GraphQL est idéal lorsque :
- Les clients ont besoin de champs différents selon les écrans.
- Un écran nécessite des données provenant de nombreuses ressources liées.
- Vous souhaitez une API unique, typée et auto-documentée.
REST est plus simple quand :
- Les ressources correspondent clairement à des endpoints.
- Le cache HTTP et les codes de statut sont primordiaux.
- L’API est petite et stable.
De nombreuses équipes utilisent les deux, en privilégiant GraphQL pour l’API produit et REST pour les webhooks, l’upload de fichiers et les endpoints publics simples.
Bonnes pratiques
- Concevez le schéma en fonction des besoins du client, et non des tables de la base de données.
- Utilisez les types non-null de manière réfléchie et gérez les versions du schéma via des dépréciations.
- Gardez vos resolvers légers et déportez la logique métier dans des services.
- Résolvez le problème N+1 avec DataLoader dès le départ.
- Ajoutez des limites de profondeur et de complexité aux requêtes pour éviter les abus.
- Retournez les objets modifiés depuis les mutations pour garantir la cohérence du cache.
- Utilisez des requêtes persistantes (persisted queries) en production pour réduire la taille des payloads.
Erreurs courantes
- Exposer directement le schéma de la base de données comme schéma GraphQL.
- Ignorer le problème N+1 jusqu’à l’effondrement des performances.
- Récupérer toutes les données dans chaque requête, perdant ainsi l’avantage principal.
- Faire confiance à l’autorisation fournie par le client ou aux permissions au niveau des champs.
- Construire des requêtes profondément imbriquées sans limite de profondeur.
- Supposer que le cache HTTP fonctionne de la même manière qu’avec REST.
Et après ?
GraphQL est un moyen puissant de concevoir des API centrées sur les clients. Appuyez-vous sur le protocole HTTP, construisez le serveur avec Node.js et consommez-le depuis React à l’aide d’une bibliothèque client. Ensuite, modélisez un écran que vous connaissez bien sous forme de schéma et de requête, et observez avec quelle fluidité les besoins en données sont satisfaits.