Mise en place d'un Serveur GraphQL et Implémentation des Resolvers
Introduction
Bienvenue dans cette leçon fondamentale de notre parcours sur le développement d'APIs robustes avec GraphQL. Dans les chapitres précédents, nous avons exploré les principes de base de GraphQL, son langage de définition de schéma (SDL) et les avantages qu'il offre par rapport aux APIs REST traditionnelles. Maintenant que vous comprenez le "quoi" (le schéma et les requêtes), il est temps de plonger dans le "comment" : comment construire un serveur GraphQL fonctionnel et comment connecter votre schéma à vos sources de données réelles via les resolvers.
Cette leçon vous guidera étape par étape dans la mise en place d'un serveur GraphQL basé sur Node.js et Apollo Server, la librairie de facto pour la construction de serveurs GraphQL. Nous détaillerons ensuite le rôle crucial des resolvers et vous montrerons comment les implémenter pour servir des données complexes et imbriquées.
À la fin de cette leçon, vous serez capable de :
- Mettre en place un serveur GraphQL de base.
- Comprendre le rôle et la signature des fonctions de resolver.
- Implémenter des resolvers pour des requêtes simples et des champs imbriqués.
- Connecter votre schéma GraphQL à des données concrètes (ici, des données en mémoire pour la démonstration).
1. Rappel Rapide : Le Schéma GraphQL (SDL)
Avant de construire notre serveur, rappelons l'importance capitale du schéma GraphQL. Le schéma est la pierre angulaire de votre API GraphQL. Il définit la structure de toutes les données que les clients peuvent interroger, les types de données disponibles, les relations entre ces types et les opérations (requêtes, mutations, souscriptions) que les clients peuvent effectuer.
Voici un exemple simple de schéma que nous allons utiliser comme base :
# Définition d'un type 'Book'
type Book {
id: ID!
title: String!
author: String! # Pour l'instant, l'auteur est un simple String
}
# Définition des opérations de requête (Query)
type Query {
books: [Book!]! # Retourne une liste de livres
book(id: ID!): Book # Retourne un livre par son ID
}
type Book: Définit un nouvel objetBookavec les champsid,titleetauthor.ID!,String!: Indiquent que le champidest de typeIDet qu'il est non-nullable (le!).type Query: Est un type spécial qui définit les points d'entrée pour la lecture de données.books: Un champ qui renverra une liste d'objetsBooknon nuls ([Book!]!).book(id: ID!): Book: Un champ qui accepte un argumentidde typeIDnon nul et renvoie un seul objetBook.
Le schéma est la promesse de votre API. Les resolvers sont la réalisation de cette promesse.
2. Choix de la Pile Technologique : Apollo Server
Pour construire notre serveur GraphQL en Node.js, nous utiliserons Apollo Server. C'est une librairie puissante, flexible et très populaire, maintenue par l'équipe d'Apollo, qui offre un écosystème complet pour le développement GraphQL (outils de développement, de production, de performance, etc.).
Apollo Server peut être intégré avec divers frameworks HTTP populaires comme Express, Koa ou Hapi. Pour simplifier, nous utiliserons sa version autonome.
3. Mise en Place du Serveur GraphQL (Apollo Server)
3.1. Prérequis
Assurez-vous d'avoir Node.js (version 14 ou supérieure recommandée) et npm (ou yarn) installés sur votre machine.
3.2. Initialisation du Projet et Installation
Commencez par créer un nouveau répertoire de projet et initialisez-le :
mkdir graphql-server-lesson
cd graphql-server-lesson
npm init -y
Maintenant, installez les dépendances nécessaires : apollo-server (le serveur GraphQL) et graphql (la librairie core pour la manipulation des schémas GraphQL).
npm install apollo-server graphql
3.3. Création du Serveur de Base
Créez un fichier index.js (ou server.js) dans votre répertoire de projet.
// index.js
// 1. Importation des modules nécessaires
const { ApolloServer, gql } = require('apollo-server');
// 2. Définition de notre schéma GraphQL (SDL)
// Nous utilisons la fonction `gql` pour parser la chaîne de caractères en un objet de schéma GraphQL.
const typeDefs = gql`
type Book {
id: ID!
title: String!
author: String!
}
type Query {
books: [Book!]!
book(id: ID!): Book
}
`;
// 3. Création de données mock pour notre exemple
// En production, ces données proviendraient d'une base de données, d'une autre API, etc.
const books = [
{
id: '1',
title: 'Le Seigneur des Anneaux',
author: 'J.R.R. Tolkien',
},
{
id: '2',
title: 'Le Guide du voyageur galactique',
author: 'Douglas Adams',
},
{
id: '3',
title: 'Fondation',
author: 'Isaac Asimov',
},
];
// 4. Définition des resolvers (les fonctions qui "résolvent" les données pour chaque champ du schéma)
// Pour l'instant, c'est un objet vide. Nous le détaillerons dans la section suivante.
const resolvers = {
Query: {
// Les resolvers pour nos champs 'books' et 'book' iront ici
books: () => books, // Un simple exemple direct
book: (parent, args) => {
// args contient les arguments passés à la requête, par exemple { id: '1' }
return books.find(book => book.id === args.id);
},
},
// On pourrait avoir des resolvers pour d'autres types ici, par exemple Book: { ... }
};
// 5. Instanciation d'ApolloServer
// L'instance prend un objet de configuration avec `typeDefs` (le schéma) et `resolvers`.
const server = new ApolloServer({ typeDefs, resolvers });
// 6. Lancement du serveur
server.listen().then(({ url }) => {
console.log(`🚀 Serveur GraphQL prêt à l'adresse : ${url}`);
console.log(`Explorez votre API avec Apollo Studio Explorer (anciennement GraphQL Playground)`);
});
Pour lancer ce serveur, exécutez dans votre terminal :
node index.js
Vous devriez voir un message indiquant que le serveur est prêt, généralement à http://localhost:4000/. Ouvrez cette URL dans votre navigateur. Vous devriez être accueilli par l'Apollo Studio Explorer (anciennement GraphQL Playground), une interface web interactive pour tester vos requêtes GraphQL.
- Explication du code :
const { ApolloServer, gql } = require('apollo-server');: Importe la classeApolloServeret la fonctiongql(pour parser le SDL).const typeDefs = gql... : Définit le schéma de notre API en utilisant le SDL. La fonctiongqlest cruciale ici car elle transforme la chaîne de caractères en un objet de schéma compréhensible par Apollo Server.const books = [...]: Un tableau JavaScript simulant une base de données ou une autre source de données.const resolvers = {...}: C'est le cœur de notre leçon ! C'est un objet qui map les champs de notre schéma à des fonctions qui fournissent les données. Nous reviendrons en détail sur sa structure et son fonctionnement.const server = new ApolloServer({ typeDefs, resolvers });: Crée une instance du serveur Apollo, en lui passant notre schéma (typeDefs) et nos fonctions de résolution (resolvers).server.listen().then(...): Démarre le serveur HTTP. Par défaut, il écoute sur le port 4000.
4. Comprendre les Resolvers
Les resolvers sont des fonctions JavaScript (ou TypeScript) qui sont responsables de récupérer les données réelles pour chaque champ de votre schéma GraphQL. Chaque champ de votre typeDefs a potentiellement besoin d'une fonction de resolver correspondante dans l'objet resolvers.
Quand un client envoie une requête GraphQL, le serveur parcourt le schéma, identifie les champs demandés et exécute les resolvers correspondants pour collecter les données.
4.1. Structure d'un Objet de Resolver
L'objet resolvers est structuré pour refléter votre schéma GraphQL. Il contient des propriétés qui correspondent aux types définis dans votre typeDefs, tels que Query, Mutation (pour les écritures), Subscription (pour les événements en temps réel) et tous les autres types d'objets personnalisés (par exemple, Book, Author).
const resolvers = {
Query: {
// Les resolvers pour les champs de votre type Query
},
Mutation: {
// Les resolvers pour les champs de votre type Mutation
},
// Si vous avez des champs imbriqués qui nécessitent une logique de résolution spécifique
// par exemple, le champ `author` dans le type `Book` si `author` était un objet `Author`
Book: {
// Les resolvers pour les champs spécifiques au type Book
},
// ... et ainsi de suite pour chaque type complexe de votre schéma
};
4.2. Signature d'une Fonction de Resolver
Chaque fonction de resolver reçoit quatre arguments principaux :
-
parent(ouroot):- C'est la valeur renvoyée par le champ parent.
- Pour les resolvers de niveau supérieur (sous
QueryouMutation),parentest généralementundefinedou un objet racine vide. - C'est essentiel pour les resolvers de champs imbriqués, car
parentcontiendra les données de l'objet parent.
-
args:- Un objet contenant tous les arguments passés à ce champ dans la requête GraphQL.
- Par exemple, pour la requête
book(id: "1"),argsserait{ id: "1" }.
-
context:- Un objet partagé par tous les resolvers exécutés pour une opération donnée.
- C'est l'endroit idéal pour injecter des ressources partagées comme les connexions à la base de données, les APIs externes, les informations d'authentification de l'utilisateur, etc. Apollo Server permet de le configurer facilement.
-
info:- Contient des informations sur l'état de l'exécution de la requête, y compris l'arbre de syntaxe abstraite (AST) de la requête elle-même.
- Moins couramment utilisé pour les resolvers simples, mais utile pour des optimisations avancées (comme le dataleader pour éviter le problème N+1) ou la journalisation.
Une fonction de resolver peut retourner :
- Une valeur directe (string, number, object, array).
- Une
Promisequi résout à une valeur. C'est très courant car les opérations de base de données ou les appels d'API sont souvent asynchrones.
5. Implémentation des Resolvers - Exemples Pratiques
Reprenons notre exemple de serveur et enrichissons-le. Nous allons ajouter un type Author et relier les Books aux Authors, ce qui nous permettra de démontrer les resolvers imbriqués.
5.1. Schéma GraphQL Enrichi
D'abord, mettons à jour notre schéma (typeDefs) pour inclure le type Author et les relations :
# typeDefs (dans index.js)
const typeDefs = gql`
type Book {
id: ID!
title: String!
# L'auteur n'est plus un simple String, mais un objet Author
author: Author!
}
type Author {
id: ID!
name: String!
# L'auteur peut avoir plusieurs livres
books: [Book!]!
}
type Query {
books: [Book!]!
book(id: ID!): Book
authors: [Author!]! # Nouvelle requête pour récupérer tous les auteurs
author(id: ID!): Author # Nouvelle requête pour récupérer un auteur par son ID
}
`;
5.2. Données Mock Enrichies
Nous aurons besoin de données mock qui reflètent cette nouvelle structure :
// Données mock (dans index.js, avant les resolvers)
const authors = [
{ id: '1', name: 'J.R.R. Tolkien' },
{ id: '2', name: 'Douglas Adams' },
{ id: '3', name: 'Isaac Asimov' },
];
const books = [
{
id: '1',
title: 'Le Seigneur des Anneaux',
authorId: '1', // Lien vers l'ID de l'auteur
},
{
id: '2',
title: 'Le Guide du voyageur galactique',
authorId: '2',
},
{
id: '3',
title: 'Fondation',
authorId: '3',
},
{
id: '4',
title: 'Le Hobbit',
authorId: '1',
},
];
// Pour simuler une base de données ou un service de données,
// nous pouvons encapsuler l'accès aux données dans un objet 'dataSources'
// qui sera ensuite passé via le contexte aux resolvers.
const dataSources = {
getBooks: () => books,
getBookById: (id) => books.find(book => book.id === id),
getAuthors: () => authors,
getAuthorById: (id) => authors.find(author => author.id === id),
getBooksByAuthorId: (authorId) => books.filter(book => book.authorId === authorId),
};
5.3. Implémentation Détaillée des Resolvers
Maintenant, implémentons les resolvers pour correspondre à notre nouveau schéma.
// Resolvers (dans index.js)
const resolvers = {
Query: {
// Résolveur pour le champ 'books' sous Query
// Récupère tous les livres.
// context.dataSources nous permet d'accéder à nos fonctions de récupération de données.
books: (parent, args, context) => context.dataSources.getBooks(),
// Résolveur pour le champ 'book' sous Query
// Récupère un livre par son ID. L'ID est passé via les 'args'.
book: (parent, args, context) => context.dataSources.getBookById(args.id),
// Résolveur pour le champ 'authors' sous Query
// Récupère tous les auteurs.
authors: (parent, args, context) => context.dataSources.getAuthors(),
// Résolveur pour le champ 'author' sous Query
// Récupère un auteur par son ID.
author: (parent, args, context) => context.dataSources.getAuthorById(args.id),
},
// Résolveurs pour le type 'Book'
// Ces resolvers sont appelés pour les champs *imbriqués* du type Book.
// Le 'parent' ici est l'objet Book qui a été résolu par un resolver parent (par exemple, Query.book ou Query.books).
Book: {
// Résolveur pour le champ 'author' dans le type Book
// Le 'parent' est l'objet Book courant (ex: { id: '1', title: '...', authorId: '1' }).
// Nous utilisons parent.authorId pour trouver l'auteur correspondant.
author: (parent, args, context) => {
return context.dataSources.getAuthorById(parent.authorId);
},
},
// Résolveurs pour le type 'Author'
// Similaire au type Book, ces resolvers sont appelés pour les champs imbriqués du type Author.
Author: {
// Résolveur pour le champ 'books' dans le type Author
// Le 'parent' est l'objet Author courant (ex: { id: '1', name: '...' }).
// Nous utilisons parent.id pour trouver tous les livres de cet auteur.
books: (parent, args, context) => {
return context.dataSources.getBooksByAuthorId(parent.id);
},
},
};
// Mise à jour de l'instanciation du serveur pour inclure le contexte
const server = new ApolloServer({
typeDefs,
resolvers,
// La fonction `context` est appelée pour chaque requête et son retour est l'objet `context`
// qui sera disponible pour tous les resolvers de cette requête.
context: () => ({
dataSources: dataSources // Nous passons notre objet de sources de données
})
});
Explication Détaillée des Nouveaux Resolvers :
Query.booksetQuery.book: Fonctionnent de la même manière que précédemment, mais utilisent maintenantcontext.dataSourcespour récupérer les données, ce qui est une bonne pratique pour centraliser l'accès aux données.Query.authorsetQuery.author: Sont de nouveaux resolvers de niveau supérieur pour récupérer des listes ou des auteurs spécifiques, basés sur notre nouveau schéma.Book.author: C'est un resolver de champ imbriqué. Lorsque le serveur résout un objetBook(par exemple, après une requêtebooks), s'il détecte que le client a demandé le champauthorde ceBook, il va appeler ce resolverBook.author.- L'argument
parentcontiendra l'objetBookactuel (ex:{ id: '1', title: 'Le Seigneur des Anneaux', authorId: '1' }). - Nous utilisons
parent.authorIdpour trouver l'objetAuthorcorrespondant et le retourner. GraphQL se chargera de mapper les champs de l'objetAuthorretourné aux champs demandés par le client (par exemple,name).
- L'argument
Author.books: Un autre resolver de champ imbriqué. Lorsque le serveur résout un objetAuthor, s'il détecte que le client a demandé le champbooksde cetAuthor, il appelle ce resolverAuthor.books.- L'argument
parentcontiendra l'objetAuthoractuel (ex:{ id: '1', name: 'J.R.R. Tolkien' }). - Nous utilisons
parent.idpour filtrer tous les livres et retourner ceux qui correspondent à cet auteur.
- L'argument
L'importance de parent pour les resolvers imbriqués est capitale. C'est ainsi que GraphQL construit le "graph" de données, en passant les résultats des resolvers parents aux resolvers enfants.
6. Tester votre Serveur GraphQL
Lancez votre serveur avec node index.js. Rendez-vous à l'adresse http://localhost:4000/.
Dans l'Apollo Studio Explorer, vous pouvez exécuter des requêtes comme :
Requête pour tous les livres avec leurs auteurs :
query GetBooksWithAuthors {
books {
id
title
author { # Le champ 'author' du type Book
id
name
}
}
}
Requête pour un auteur et ses livres :
query GetAuthorWithBooks {
author(id: "1") { # Demande l'auteur avec l'ID '1'
name
books { # Le champ 'books' du type Author
title
}
}
}
Vous devriez voir les données structurées exactement comme vous les avez demandées, démontrant le bon fonctionnement de vos resolvers.
Conclusion
Félicitations ! Vous avez franchi une étape cruciale dans le développement d'APIs GraphQL. Vous avez appris à :
- Mettre en place un serveur GraphQL de base avec Apollo Server.
- Comprendre que le schéma est la définition du contrat de votre API.
- Découvrir les resolvers comme étant les fonctions qui fournissent les données réelles pour chaque champ de votre schéma.
- Maîtriser la signature des resolvers (
parent,args,context,info) et l'importance de l'argumentparentpour la résolution des champs imbriqués. - Implémenter des resolvers pour des requêtes de niveau supérieur (
Query) ainsi que des resolvers de champs imbriqués pour construire des graphes de données complexes. - Passer des dépendances (comme des sources de données) via le contexte du serveur, une pratique recommandée pour la gestion des ressources.
Les resolvers sont le pont entre votre schéma déclaratif et vos sources de données impératives. Que vos données proviennent d'une base de données, d'une autre API REST, d'un fichier CSV ou d'un service de micro-services, les resolvers sont l'endroit où vous définissez la logique pour les récupérer et les formater selon votre schéma GraphQL.
Dans les prochaines leçons, nous explorerons les mutations pour la modification de données, la gestion des erreurs, l'authentification et l'autorisation, et comment connecter votre serveur GraphQL à de véritables bases de données.