GraphQL vs REST - Un tutoriel GraphQL
Publié: 2022-03-11Vous avez peut-être entendu parler du petit nouveau du quartier : GraphQL. Sinon, GraphQL est, en un mot, une nouvelle façon de récupérer les API, une alternative à REST. Cela a commencé comme un projet interne chez Facebook, et depuis qu'il était open source, il a gagné beaucoup de terrain.
Le but de cet article est de vous aider à effectuer une transition facile de REST vers GraphQL, que vous ayez déjà pensé à GraphQL ou que vous souhaitiez simplement l'essayer. Aucune connaissance préalable de GraphQL n'est nécessaire, mais une certaine familiarité avec les API REST est nécessaire pour comprendre l'article.
La première partie de l'article commencera par donner trois raisons pour lesquelles je pense personnellement que GraphQL est supérieur à REST. La deuxième partie est un tutoriel sur la façon d'ajouter un point de terminaison GraphQL sur votre back-end.
Graphql vs REST : pourquoi abandonner REST ?
Si vous hésitez encore à savoir si GraphQL est adapté ou non à vos besoins, un aperçu assez complet et objectif de "REST vs. GraphQL" est donné ici. Cependant, pour mes trois principales raisons d'utiliser GraphQL, lisez la suite.
Raison 1 : performances du réseau
Supposons que vous ayez une ressource utilisateur sur le back-end avec le prénom, le nom, l'e-mail et 10 autres champs. Sur le client, vous n'en avez généralement besoin que de quelques-uns.
Faire un appel REST sur le point de terminaison /users
vous restitue tous les champs de l'utilisateur, et le client n'utilise que ceux dont il a besoin. Il y a clairement un gaspillage de transfert de données, ce qui pourrait être une considération pour les clients mobiles.
GraphQL récupère par défaut les plus petites données possibles. Si vous n'avez besoin que des noms et prénoms de vos utilisateurs, vous le spécifiez dans votre requête.
L'interface ci-dessous s'appelle GraphiQL, qui ressemble à un explorateur d'API pour GraphQL. J'ai créé un petit projet pour les besoins de cet article. Le code est hébergé sur GitHub, et nous y plongerons dans la deuxième partie.
Dans le volet gauche de l'interface se trouve la requête. Ici, nous récupérons tous les utilisateurs - nous ferions GET /users
avec REST - et n'obtenons que leurs prénom et nom.
Mettre en doute
query { users { firstname lastname } }
Résultat
{ "data": { "users": [ { "firstname": "John", "lastname": "Doe" }, { "firstname": "Alicia", "lastname": "Smith" } ] } }
Si nous voulions également obtenir les e-mails, ajouter une ligne "email" sous "nom de famille" ferait l'affaire.
Certains back-ends REST offrent des options telles que /users?fields=firstname,lastname
pour renvoyer des ressources partielles. Pour ce que ça vaut, Google le recommande. Cependant, il n'est pas implémenté par défaut et rend la requête à peine lisible, en particulier lorsque vous ajoutez d'autres paramètres de requête :
-
&status=active
pour filtrer les utilisateurs actifs -
&sort=createdAat
pour trier les utilisateurs par date de création -
&sortDirection=desc
car vous en avez évidemment besoin -
&include=projects
pour inclure les projets des utilisateurs
Ces paramètres de requête sont des correctifs ajoutés à l'API REST pour imiter un langage de requête. GraphQL est avant tout un langage de requête, qui rend les requêtes concises et précises dès le début.
Raison 2 : Le choix de conception « Inclure vs Endpoint »
Imaginons que nous voulions construire un outil de gestion de projet simple. Nous avons trois ressources : les utilisateurs, les projets et les tâches. Nous définissons également les relations suivantes entre les ressources :
Voici quelques-uns des endpoints que nous exposons au monde :
Point final | La description |
---|---|
GET /users | Lister tous les utilisateurs |
GET /users/:id | Obtenir l'utilisateur unique avec id :id |
GET /users/:id/projects | Obtenir tous les projets d'un utilisateur |
Les points de terminaison sont simples, facilement lisibles et bien organisés.
Les choses se compliquent lorsque nos demandes deviennent plus complexes. Prenons le point de terminaison GET /users/:id/projects
: supposons que je souhaite afficher uniquement les titres des projets sur la page d'accueil, mais les projets + tâches sur le tableau de bord, sans effectuer plusieurs appels REST. J'appellerais:
-
GET /users/:id/projects
pour la page d'accueil. -
GET /users/:id/projects?include=tasks
(par exemple) sur la page du tableau de bord afin que le back-end ajoute toutes les tâches associées.
Il est courant d'ajouter des paramètres de requête ?include=...
pour que cela fonctionne, et c'est même recommandé par la spécification de l'API JSON. Les paramètres de requête tels que ?include=tasks
sont toujours lisibles, mais d'ici peu, nous nous retrouverons avec ?include=tasks,tasks.owner,tasks.comments,tasks.comments.author
.
Dans ce cas, serait-il plus judicieux de créer un point de terminaison /projects
pour ce faire ? Quelque chose comme /projects?userId=:id&include=tasks
, car nous aurions un niveau de relation de moins à inclure ? Ou, en fait, un point de terminaison /tasks?userId=:id
peut également fonctionner. Cela peut être un choix de conception difficile, encore plus compliqué si nous avons une relation plusieurs à plusieurs.
GraphQL utilise l'approche include
partout. Cela rend la syntaxe pour extraire les relations puissante et cohérente.
Voici un exemple de récupération de tous les projets et tâches de l'utilisateur avec l'ID 1.
Mettre en doute
{ user(id: 1) { projects { name tasks { description } } } }
Résultat
{ "data": { "user": { "projects": [ { "name": "Migrate from REST to GraphQL", "tasks": [ { "description": "Read tutorial" }, { "description": "Start coding" } ] }, { "name": "Create a blog", "tasks": [ { "description": "Write draft of article" }, { "description": "Set up blog platform" } ] } ] } } }
Comme vous pouvez le constater, la syntaxe de la requête est facilement lisible. Si nous voulions aller plus loin et inclure des tâches, des commentaires, des images et des auteurs, nous n'hésiterions pas à organiser notre API. GraphQL facilite la récupération d'objets complexes.
Raison 3 : Gérer différents types de clients
Lors de la construction d'un back-end, nous commençons toujours par essayer de rendre l'API aussi largement utilisable par tous les clients que possible. Pourtant, les clients veulent toujours appeler moins et aller chercher plus. Avec des inclusions profondes, des ressources partielles et un filtrage, les requêtes effectuées par les clients Web et mobiles peuvent être très différentes les unes des autres.
Avec REST, il existe plusieurs solutions. Nous pouvons créer un point de terminaison personnalisé (c'est-à-dire un point de terminaison alias, par exemple, /mobile_user
), une représentation personnalisée ( Content-Type: application/vnd.rest-app-example.com+v1+mobile+json
), ou même un client - API spécifique (comme Netflix l'a fait autrefois). Tous les trois nécessitent des efforts supplémentaires de la part de l'équipe de développement back-end.
GraphQL donne plus de pouvoir au client. Si le client a besoin de requêtes complexes, il construira lui-même les requêtes correspondantes. Chaque client peut consommer la même API différemment.
Comment démarrer avec GraphQL
Dans la plupart des débats sur "GraphQL vs. REST" aujourd'hui, les gens pensent qu'ils doivent choisir l'un des deux. Ce n'est tout simplement pas vrai.
Les applications modernes utilisent généralement plusieurs services différents, qui exposent plusieurs API. Nous pourrions en fait considérer GraphQL comme une passerelle ou un wrapper vers tous ces services. Tous les clients atteindraient le point de terminaison GraphQL, et ce point de terminaison toucherait la couche de base de données, un service externe comme ElasticSearch ou Sendgrid, ou d'autres points de terminaison REST.
Une deuxième façon d'utiliser les deux consiste à avoir un point de terminaison /graphql
séparé sur votre API REST. Ceci est particulièrement utile si vous avez déjà de nombreux clients qui accèdent à votre API REST, mais que vous souhaitez essayer GraphQL sans compromettre l'infrastructure existante. Et c'est la solution que nous explorons aujourd'hui.
Comme dit précédemment, je vais illustrer ce tutoriel avec un petit exemple de projet, disponible sur GitHub. C'est un outil de gestion de projet simplifié, avec des utilisateurs, des projets et des tâches.
Les technologies utilisées pour ce projet sont Node.js et Express pour le serveur web, SQLite comme base de données relationnelle et Sequelize comme ORM. Les trois modèles (utilisateur, projet et tâche) sont définis dans le dossier des models
. Les points de terminaison REST /api/users
, /api/projects
et /api/tasks
sont exposés au monde et sont définis dans le dossier rest
.

Notez que GraphQL peut être installé sur n'importe quel type de back-end et de base de données, en utilisant n'importe quel langage de programmation. Les technologies utilisées ici sont choisies dans un souci de simplicité et de lisibilité.
Notre objectif est de créer un point de terminaison /graphql
sans supprimer les points de terminaison REST. Le point de terminaison GraphQL frappera directement l'ORM de la base de données pour récupérer les données, de sorte qu'il soit totalement indépendant de la logique REST.
Les types
Le modèle de données est représenté dans GraphQL par des types , qui sont fortement typés. Il devrait y avoir un mappage 1 à 1 entre vos modèles et les types GraphQL. Notre type User
serait :
type User { id: ID! # The "!" means required firstname: String lastname: String email: String projects: [Project] # Project is another GraphQL type }
Requêtes
Les requêtes définissent les requêtes que vous pouvez exécuter sur votre API GraphQL. Par convention, il devrait y avoir un RootQuery
, qui contient toutes les requêtes existantes. J'ai également souligné l'équivalent REST de chaque requête :
type RootQuery { user(id: ID): User # Corresponds to GET /api/users/:id users: [User] # Corresponds to GET /api/users project(id: ID!): Project # Corresponds to GET /api/projects/:id projects: [Project] # Corresponds to GET /api/projects task(id: ID!): Task # Corresponds to GET /api/tasks/:id tasks: [Task] # Corresponds to GET /api/tasks }
mutation
Si les requêtes sont des requêtes GET
, les mutations peuvent être vues comme des requêtes POST
/ PATCH
/ PUT
/ DELETE
(bien qu'il s'agisse en réalité de versions synchronisées des requêtes).
Par convention, nous mettons toutes nos mutations dans une RootMutation
:
type RootMutation { createUser(input: UserInput!): User # Corresponds to POST /api/users updateUser(id: ID!, input: UserInput!): User # Corresponds to PATCH /api/users removeUser(id: ID!): User # Corresponds to DELETE /api/users createProject(input: ProjectInput!): Project updateProject(id: ID!, input: ProjectInput!): Project removeProject(id: ID!): Project createTask(input: TaskInput!): Task updateTask(id: ID!, input: TaskInput!): Task removeTask(id: ID!): Task }
Notez que nous avons introduit de nouveaux types ici, appelés UserInput
, ProjectInput
et TaskInput
. Il s'agit également d'une pratique courante avec REST pour créer un modèle de données d'entrée pour créer et mettre à jour des ressources. Ici, notre type UserInput
est notre type User
sans les champs id
et projects
, et notez le mot clé input
au lieu de type
:
input UserInput { firstname: String lastname: String email: String }
Schéma
Avec les types, les requêtes et les mutations, nous définissons le schéma GraphQL , qui est ce que le point de terminaison GraphQL expose au monde :
schema { query: RootQuery mutation: RootMutation }
Ce schéma est fortement typé et c'est ce qui nous a permis d'avoir ces autocomplétions pratiques dans GraphiQL.
Résolveurs
Maintenant que nous avons le schéma public, il est temps de dire à GraphQL quoi faire lorsque chacune de ces requêtes/mutations est demandée. Les résolveurs font le gros du travail ; ils peuvent, par exemple :
- Atteindre un point de terminaison REST interne
- Appeler un microservice
- Appuyez sur la couche de base de données pour effectuer des opérations CRUD
Nous choisissons la troisième option dans notre exemple d'application. Jetons un coup d'œil à notre fichier de résolveurs :
const models = sequelize.models; RootQuery: { user (root, { id }, context) { return models.User.findById(id, context); }, users (root, args, context) { return models.User.findAll({}, context); }, // Resolvers for Project and Task go here }, /* For reminder, our RootQuery type was: type RootQuery { user(id: ID): User users: [User] # Other queries }
Cela signifie que si la requête user(id: ID!)
est demandée sur GraphQL, nous User.findById()
, qui est une fonction Sequelize ORM, à partir de la base de données.
Qu'en est-il de joindre d'autres modèles dans la demande ? Eh bien, nous devons définir plus de résolveurs :
User: { projects (user) { return user.getProjects(); // getProjects is a function managed by Sequelize ORM } }, /* For reminder, our User type was: type User { projects: [Project] # We defined a resolver above for this field # ...other fields } */
Ainsi, lorsque nous demandons le champ projects
dans un type d' User
dans GraphQL, cette jointure sera ajoutée à la requête de la base de données.
Et enfin, les résolveurs pour les mutations :
RootMutation: { createUser (root, { input }, context) { return models.User.create(input, context); }, updateUser (root, { id, input }, context) { return models.User.update(input, { ...context, where: { id } }); }, removeUser (root, { id }, context) { return models.User.destroy(input, { ...context, where: { id } }); }, // ... Resolvers for Project and Task go here }
Vous pouvez jouer avec ça ici. Dans un souci de propreté des données sur le serveur, j'ai désactivé les résolveurs pour les mutations, ce qui signifie que les mutations n'effectueront aucune opération de création, de mise à jour ou de suppression dans la base de données (et donc renverront null
sur l'interface).
Mettre en doute
query getUserWithProjects { user(id: 2) { firstname lastname projects { name tasks { description } } } } mutation createProject { createProject(input: {name: "New Project", UserId: 2}) { id name } }
Résultat
{ "data": { "user": { "firstname": "Alicia", "lastname": "Smith", "projects": [ { "name": "Email Marketing Campaign", "tasks": [ { "description": "Get list of users" }, { "description": "Write email template" } ] }, { "name": "Hire new developer", "tasks": [ { "description": "Find candidates" }, { "description": "Prepare interview" } ] } ] } } }
La réécriture de tous les types, requêtes et résolveurs pour votre application existante peut prendre un certain temps. Cependant, de nombreux outils existent pour vous aider. Par exemple, il existe des outils qui traduisent un schéma SQL en schéma GraphQL, y compris des résolveurs !
Tout mettre ensemble
Avec un schéma bien défini et des résolveurs sur ce qu'il faut faire sur chaque requête du schéma, nous pouvons monter un point de terminaison /graphql
sur notre back-end :
// Mount GraphQL on /graphql const schema = makeExecutableSchema({ typeDefs, // Our RootQuery and RootMutation schema resolvers: resolvers() // Our resolvers }); app.use('/graphql', graphqlExpress({ schema }));
Et nous pouvons avoir une belle interface GraphiQL sur notre back-end. Pour faire une requête sans GraphiQL, copiez simplement l'URL de la requête et exécutez-la avec cURL, AJAX ou directement dans le navigateur. Bien sûr, il existe des clients GraphQL pour vous aider à créer ces requêtes. Voir ci-dessous pour quelques exemples.
Et après?
Le but de cet article est de vous donner un avant-goût de ce à quoi ressemble GraphQL et de vous montrer qu'il est tout à fait possible d'essayer GraphQL sans jeter votre infrastructure REST. La meilleure façon de savoir si GraphQL répond à vos besoins est de l'essayer vous-même. J'espère que cet article vous fera sauter le pas.
Il existe de nombreuses fonctionnalités dont nous n'avons pas parlé dans cet article, telles que les mises à jour en temps réel, le traitement par lots côté serveur, l'authentification, l'autorisation, la mise en cache côté client, le téléchargement de fichiers, etc. Une excellente ressource pour en savoir plus sur ces fonctionnalités. est Comment GraphQL.
Vous trouverez ci-dessous d'autres ressources utiles :
Outil côté serveur | La description |
---|---|
graphql-js | L'implémentation de référence de GraphQL. Vous pouvez l'utiliser avec express-graphql pour créer un serveur. |
graphql-server | Un serveur GraphQL tout-en-un créé par l'équipe Apollo. |
Implémentations pour d'autres plates-formes | Ruby, PHP, etc. |
Outil côté client | La description |
---|---|
Relais | Un framework pour connecter React avec GraphQL. |
apollo-client. | Un client GraphQL avec des liaisons pour React, Angular 2 et d'autres frameworks frontaux. |
En conclusion, je crois que GraphQL est plus qu'un battage publicitaire. Il ne remplacera pas encore REST demain, mais il offre une solution performante à un véritable problème. C'est relativement nouveau et les meilleures pratiques sont encore en développement, mais c'est certainement une technologie dont nous entendrons parler dans les deux prochaines années.