Ce workshop préparé au sein de JS-Republic par Michael Romain et Mathieu Breton a pour but de vous apprendre à utiliser GraphQL.
D'une durée approximative de 3h, ce workshop vous guidera étape par étape dans la migration d'un blog basique fait avec une API REST vers une implémentation full GraphQL. Chaque partie débutera par une courte présentation faite par l'animateur du workshop afin d'expliquer les points qui seront traités dans celle-ci, puis les candidats suivront l'énoncé pour accomplir la partie en question. Une fois l'étape terminée, l'animateur répondra aux questions et passera à la partie suivante.
Chaque étape vous démontrera les avantages (et inconvénients) de GraphQL comparé à REST pour les mêmes besoins. Ce workshop est conçu pour des développeurs JavaScript, débutants en GraphQL.
Pour commencer, assurez-vous d'avoir les pré-requis ci-dessous puis procéder à l'installation du projet de workshop. Une description du projet vous attend une fois l'installation terminée.
Pour suivre ce workshop, vous aurez besoin :
- De connaissances confirmées dans le langage JavaScript, en NodeJS et en développement Front-End.
- D'une prémière expérience avec les API REST.
- De NodeJS installé en version 6.14.2 et plus. Dans un soucis de compatibilité, l'implémentation back-end fonctionne avec la version 6.* de Node, version la plus vielle actuellement encore maintenue. Si vous utilisez nvm, vous pouvez faire un
nvm use
à la racine du projet pour passer directement dans la bonne version de NodeJS. - D'un éditeur de code. Visual Studio Code fait désormais référence.
Une fois n'est pas coutume, nous récupérons ce projet depuis Github et installerons ses dépendances :
git clone https://github.com/js-republic/graphql-workshop.git
cd graphql-workshop
npm install
Il ne reste plus qu'à le démarrer :
npm start
Votre navigateur s'ouvre à l'adresse http://localhost:3000, vous devriez découvrir cette interface :
Prenez quelques instants pour vous familiariser avec le blog en l'état. Vous remarquerez notamment que, pour l'instant, il communique avec le back-end via l'API REST.
Le projet est organisé comme suit :
.
├── ...
├── blog.sqlite <-- Fichier de base de données SQlite du blog
├── migrations <-- Dossier contenant les scripts d'initialisation SQL
├── public <-- Dossier public exposé sur localhost:3000
│ ├── index.html
│ └── ...
├── server <-- Sources du serveur en NodeJS exposant les données
│ ├── ...
│ ├── route <-- Dossier contenant les routes exposées par le serveur
│ │ ├── graphql.js <-- Script pour exposer les données en GraphQL (à modifier)
│ │ ├── rest.js <-- Script pour exposer les données en REST
│ │ └── ...
│ └── service
│ └── index.js <-- Service qui permet d'accéder et modifier les données en base
└── src <-- Sources du front en React (architecture create-react-app)
├── ...
├── clients <-- Dossier contenant les routes exposées par le serveur
│ ├── rest.js <-- Script permettant la récupération et manipulation des données via REST
│ └── graphq.js <-- Script permettant la récupération et manipulation des données via GraphQL (à modifier)
└── components
└── ...
Quand le projet est démarré (via npm start
) deux tâches sont lancées en parallèle :
- Un webpack-dev-server avec hot reload qui compile les sources du front en React (dans le dossier
/src
) et les expose sur l'adresse http://localhost:3000. Ce webpack-dev-server fait aussi proxy pour envoyer les requêtes XHR vers le serveur. - Un serveur NodeJS qui expose une api REST sur http://localhost:3001/rest et une api GraphQL sur http://localhost:3001/graphql
Si vous faites ce workshop hors de la session Best Of Web, nous vous invitons à d'abord prendre connaissance du début de cette présentation jusqu'à la diapositive Premier exercice : https://slides.com/mbreton/graphql-workshop
Les données sont enregistrées dans SQLite, sous la forme d'un fichier
blog.sqlite
à la racine du projet. Si vous souhaitez réinitialiser vos données, il vous suffit de supprimer ce fichier et redémarrer.
Dans cette première partie, vous allez vous familiariser avec le requêtage GraphQL et l'implémentation côté serveur pour lire des données.
Présentation des points abordés : https://slides.com/mbreton/graphql-workshop#/1
Énoncé :
-
Rendez-vous dans le ficher
server/route/graphql.js
pour y ajouter un type Queryposts
permettant de récupérer une liste dePost
.Découvrir la solution ici
// ... const typeDefs = ` type Post { id: ID! title: String! content: String! } type Query { posts: [Post] } `); // ...
-
Ajouter un resolver correspondant à la Query que venez d'ajouter. Vous pouvez vous appuyer sur la fonction
getPosts
du scriptserver/service/index.js
déjà importé dansserver/route/graphql.js
. Petite subtilité, cette fonction retourne une promesse. (plus d'information sur les resolvers asynchrones ici)Découvrir la solution ici
// ... const typeDefs = ` type Post { id: ID! title: String! content: String! } type Query { posts: [Post] } `); router.use( graphqlHTTP({ schema, rootValue: { posts() { return service.getPosts(); } }, graphiql: true }) ); // ...
Vous devez maintenant pouvoir requêter la liste des articles via GraphQL dans GraphiQL.
Comment requêter les données ?
{ posts { id title content } }
Nos données désormais disponibles via GraphQL sur notre serveur, il est temps de les afficher !
Présentation des points abordés : https://slides.com/mbreton/graphql-workshop#/2
Énoncé :
-
Aller dans le fichier
src/clients/graphql.js
. Ce fichier est responsable de toutes les requêtes GraphQL envoyées par le front vers le backend. Implémenter la fonctiongetPosts
, pour l'instant vide, afin de charger l'id
, letitle
et lecontent
des articles :export async function getPosts() {}
Vous pouvez utiliser la librairie axios pour faire vos appels HTTP. Vérifiez que vous avez bien implémenté la fonction en retournant sur le blog http://localhost:3000 voir que la liste des articles se charge bien avec l'interrupteur en mode GraphQL, sinon regardez la console :)
Découvrir la solution ici
export async function getPosts() { const { data: { data: { posts } } } = await axios.post("/graphql", { query: `{ posts { id title content } }` }); return posts; }
Vous ne l'aurez pas manqué, il n'y a plus les commentaires ! Et cela est normal, ils n'existent pas dans notre schéma.
Nous confirmons des points déjà vus juste avant dans cette partie.
Énoncé :
-
Retournez dans
server/route/graphql.js
pour y ajouter un typeComment
dans le schéma. Ce typeComment
contiendra une propriétéid
qui représente son identifiant unique et une propriétécontent
qui représente le texte qu'elle contient. Ces deux propriétés doivent être obligatoires. Une fois ce type créé, ajouter une propriétécomments
dansPost
qui contiendra la liste du type fraichement créé.Découvrir la solution ici
// ... const typeDefs = ` type Post { id: ID! title: String! content: String! comments: [Comment]! } type Comment { id: ID! content: String! } type Query { posts: [Post] } `); // ...
-
Dans le même fichier, ajouter un resolver pour la propriété
comments
du typePost
. Se revolver devra charger les commentaires à l'aide de la fonctiongetCommentFor
du service en lui passant en paramètre l'id du post parent.Découvrir la solution ici
const typeDefs = ` type Post { id: ID! title: String! content: String! comments: [Comment]! } type Comment { id: ID! content: String! } type Query { posts: [Post]! } `; const resolvers = { Query: { posts() { return service.getPosts(); } }, Post: { comments(post) { return service.getCommentsFor(post.id); } } };
-
Il ne reste plus qu'à modifier la requête de la fonction
getPosts
du fichiersrc/client/Graphql.js
pour y ajouter le chargement des commentaires (et de toute le ses propriétés) en même temps que celui des Posts.Découvrir la solution ici
export async function getPosts() { const { data: { data: { posts } } } = await axios.post("/graphql", { query: `{ posts { id title content comments { id content } } }` }); return posts; }
Comparez maintenant les appels réseaux du blog en mode REST et en mode GraphQL. Qu'observe-t'on ?
Nous venons de mettre le doigt sur une des grandes forces de GraphQL : le requêtage multiple
Là où REST impose que chaque ressource doit être derrière une URL, GraphQL permet de récupérer plusieurs entitées, liées ou non, en une seule requête.
Nous savons désormais exposer et lire de la donnée à travers GraphQL, voyons maintenant comment nous pouvons la modifier par ce biais. Pour l'instant, quand vous êtes en mode GraphQL, l'ajout d'article ne fonctionne pas.
Présentation des points abordés : https://slides.com/mbreton/graphql-workshop#/3
Énoncé :
-
Implémentons d'abord la partie serveur. Comme lors du premier exercice, rendez-vous dans
server/route/graphql.js
. Ajouter dans le schéma un nouveau typeMutation
dans lequel il y aura une opérationcreateComment
prenant en paramètre unpostId
, de typeID
,content
de type string et retournant le typeComment
.Découvrir la solution ici
const typeDefs = ` type Post { id: ID! title: String! content: String! comments: [Comment]! } type Comment { id: ID! content: String! } type Query { posts: [Post]! } type Mutation { createComment(postId:ID!, content: String!): Comment } `);
-
Complétons notre implémentation serveur, en ajoutant le resolver correspondant à la mutation précédement ajoutée. Cette opération pourra s'appuyer sur la fonction
addNewCommentFor
du serviceserver/service/index.js
.Découvrir la solution ici
const resolvers = { Query: { posts() { return service.getPosts(); } }, Mutation: { createComment(parentValue, args) { return service.addNewCommentFor(args.postId, args.content); } }, Post: { comments(post) { return service.getCommentsFor(post.id); } } };
-
Retourner dans GraphiQL, et formuler une requête graphql pour tester la mutation mise en place côté serveur. Vous devriez recevoir un résultat du type (en fonction des champs que vous choisissez de récupérer):
{ "data": { "createComment": { "id": "f1fbde00-696c-11e8-b3cf-bf211aef93e8", "content": "contentdzdzdz" } } }
Découvrir la solution ici
mutation addNewComment { createComment( postId: "f1fbde00-696c-11e8-b3cf-bf211aef93e8", content: "contentdzdzdz" ) { id content } }
-
La partie serveur maintenant prête, il est temps d'ajouter le code côté front capable d'envoyer une requête de mutation au serveur. Vous l'aurez surement deviné, rendons-nous dans le fichier
src/clients/graphql.js
pour implémenter la fonctionaddNewComment
:export async function addNewComment(newPost) {}
Découvrir la solution ici
export async function addNewComment(comment, postId) { const { data: { data: { createComment } } } = await axios.post("/graphql", { query: `mutation createComment($postId:ID!, $content: String!) { createComment(postId: $postId, content: $content) { id content } }`, variables: { postId, content: comment.content } }); return createComment; }
Allons plus loin de la mutation avec la création d'un Post.
Présentation des points abordés : https://slides.com/mbreton/graphql-workshop#/4
Énoncé :
-
Rendez-vous dans
server/route/graphql.js
. Ajouter une nouvelleMutation
dans laquelle il y aura une opérationcreatePost
prenant en paramètre unnewPost
de typePostInput
, input que vous aurez créé juste avant et qui contiendra le title et le content sous forme de text.Découvrir la solution ici
const typeDefs = ` type Post { id: ID! title: String! content: String! comments: [Comment]! } input PostInput { title: String!, content: String! } type Comment { id: ID! content: String! } type Query { posts: [Post]! } type Mutation { createComment(postId:ID!, content: String!): Comment createPost(newPost: PostInput): Post } `);
-
Comme pour l'ajout de commentaire, on ajoute le resolver correspondant. Cette opération pourra s'appuyer sur la fonction
addNewPost
du serviceserver/service/index.js
.Découvrir la solution ici
const resolvers = { Query: { posts() { return service.getPosts(); } }, Mutation: { createComment(parentValue, args) { return service.addNewCommentFor(args.postId, args.content); }, createPost(parentValue, args) { return service.addNewPost(args.newPost); } }, Post: { comments(post) { return service.getCommentsFor(post.id); } } };
-
Retournez dans GraphiQL, et formulez une requête graphql pour tester la mutation mise en place côté serveur. Vous devriez recevoir un résultat du type (en fonction des champs que choisissez de récupérer):
{ "data": { "createPost": { "id": "f1fbde00-696c-11e8-b3cf-bf211aef93e8", "title": "test", "content": "contentdzdzdz" } } }
-
Plus la peine de vous le dire, rendons-nous dans le fichier
src/clients/graphql.js
pour implémenter la fonctionaddNewPost
:export async function addNewPost(newPost) {}
Découvrir la solution ici
export async function addNewPost(newPost) { await axios.post("/graphql", { query: `mutation createPost($newPost: PostInput!) { createPost(newPost: $newPost) { id, title, content } }`, variables: { newPost } }); }
Le workshop est terminé, merci de votre participation.
Points à conclure https://slides.com/mbreton/graphql-workshop#/5