Création de votre première API GraphQL
Publié: 2022-03-11Avant-propos
Il y a quelques années, Facebook a introduit une nouvelle façon de créer des API back-end appelées GraphQL, qui est essentiellement un langage spécifique à un domaine pour la requête et la manipulation de données. Au début, je n'y prêtais pas beaucoup d'attention, mais finalement, je me suis retrouvé engagé dans un projet chez Toptal, où je devais implémenter des API back-end basées sur GraphQL. C'est à ce moment-là que je suis allé de l'avant et que j'ai appris à appliquer les connaissances que j'ai acquises pour REST à GraphQL.
Ce fut une expérience très intéressante, et pendant la période de mise en œuvre, j'ai dû repenser les approches et méthodologies standard utilisées dans les API REST d'une manière plus conviviale pour GraphQL. Dans cet article, j'essaie de résumer les problèmes courants à prendre en compte lors de la première implémentation des API GraphQL.
Bibliothèques requises
GraphQL a été développé en interne par Facebook et rendu public en 2015. Plus tard en 2018, le projet GraphQL a été déplacé de Facebook vers la nouvelle Fondation GraphQL, hébergée par la Fondation Linux à but non lucratif, qui maintient et développe la spécification du langage de requête GraphQL et une référence. implémentation pour JavaScript.
Étant donné que GraphQL est encore une technologie jeune et que l'implémentation de référence initiale était disponible pour JavaScript, la plupart des bibliothèques matures existent dans l'écosystème Node.js. Il existe également deux autres sociétés, Apollo et Prisma, qui fournissent des outils et des bibliothèques open source pour GraphQL. L'exemple de projet de cet article sera basé sur une implémentation de référence de GraphQL pour JavaScript et des bibliothèques fournies par ces deux sociétés :
- Graphql-js - Une implémentation de référence de GraphQL pour JavaScript
- Serveur Apollo - Serveur GraphQL pour Express, Connect, Hapi, Koa, etc.
- Apollo-graphql-tools - Construire, simuler et assembler un schéma GraphQL à l'aide du SDL
- Prisma-graphql-middleware – Divisez vos résolveurs GraphQL en fonctions middleware
Dans le monde GraphQL, vous décrivez vos API à l'aide de schémas GraphQL, et pour ceux-ci, la spécification définit son propre langage appelé The GraphQL Schema Definition Language (SDL). SDL est très simple et intuitif à utiliser tout en étant extrêmement puissant et expressif.
Il existe deux manières de créer des schémas GraphQL : l'approche code-first et l'approche schema-first.
- Dans l'approche code-first, vous décrivez vos schémas GraphQL en tant qu'objets JavaScript basés sur la bibliothèque graphql-js, et le SDL est généré automatiquement à partir du code source.
- Dans l'approche schéma d'abord, vous décrivez vos schémas GraphQL dans SDL et reliez votre logique métier à l'aide de la bibliothèque Apollo graphql-tools.
Personnellement, je préfère l'approche du schéma d'abord et je l'utiliserai pour l'exemple de projet dans cet article. Nous allons implémenter un exemple de librairie classique et créer un back-end qui fournira des API CRUD pour créer des auteurs et des livres ainsi que des API pour la gestion et l'authentification des utilisateurs.
Création d'un serveur GraphQL de base
Pour exécuter un serveur GraphQL de base, nous devons créer un nouveau projet, l'initialiser avec npm et configurer Babel. Pour configurer Babel, installez d'abord les bibliothèques requises avec la commande suivante :
npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node
Après avoir installé Babel, créez un fichier avec le nom .babelrc
dans le répertoire racine de notre projet et copiez-y la configuration suivante :
{ "presets": [ [ "@babel/env", { "targets": { "node": "current" } } ] ] }
Modifiez également le fichier package.json
et ajoutez la commande suivante à la section scripts
:
{ ... "scripts": { "serve": "babel-node index.js" }, ... }
Une fois que nous avons configuré Babel, installez les bibliothèques GraphQL requises avec la commande suivante :
npm install --save express apollo-server-express graphql graphql-tools graphql-tag
Après avoir installé les bibliothèques requises, pour exécuter un serveur GraphQL avec une configuration minimale, copiez cet extrait de code dans notre fichier index.js
:
import gql from 'graphql-tag'; import express from 'express'; import { ApolloServer, makeExecutableSchema } from 'apollo-server-express'; const port = process.env.PORT || 8080; // Define APIs using GraphQL SDL const typeDefs = gql` type Query { sayHello(name: String!): String! } type Mutation { sayHello(name: String!): String! } `; // Define resolvers map for API definitions in SDL const resolvers = { Query: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } }, Mutation: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } } }; // Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolvers maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); });
Après cela, nous pouvons exécuter notre serveur à l'aide de la commande npm run serve
, et si nous naviguons dans un navigateur Web vers l'URL http://localhost:8080/graphql
, le shell visuel interactif de GraphQL, appelé Playground, s'ouvrira, où nous pourrons exécuter des requêtes et des mutations GraphQL et voir les données de résultat.
Dans le monde GraphQL, les fonctions API sont divisées en trois ensembles, appelés requêtes, mutations et abonnements :
- Les requêtes sont utilisées par le client pour demander au serveur les données dont il a besoin.
- Les mutations sont utilisées par le client pour créer/mettre à jour/supprimer des données sur le serveur.
- Les abonnements sont utilisés par le client pour créer et maintenir une connexion en temps réel au serveur. Cela permet au client d'obtenir des événements du serveur et d'agir en conséquence.
Dans notre article, nous ne traiterons que des requêtes et des mutations. Les abonnements sont un vaste sujet - ils méritent leur propre article et ne sont pas requis dans toutes les implémentations d'API.
Types de données scalaires avancés
Très peu de temps après avoir joué avec GraphQL, vous découvrirez que SDL ne fournit que des types de données primitifs et que des types de données scalaires avancés tels que Date, Time et DateTime, qui constituent une partie importante de chaque API, sont manquants. Heureusement, nous avons une bibliothèque qui nous aide à résoudre ce problème, et elle s'appelle graphql-iso-date. Après l'avoir installé, nous devrons définir de nouveaux types de données scalaires avancés dans notre schéma et les connecter aux implémentations fournies par la bibliothèque :
import { GraphQLDate, GraphQLDateTime, GraphQLTime } from 'graphql-iso-date'; // Define APIs using GraphQL SDL const typeDefs = gql` scalar Date scalar Time scalar DateTime type Query { sayHello(name: String!): String! } type Mutation { sayHello(name: String!): String! } `; // Define resolvers map for API definitions in SDL const resolvers = { Date: GraphQLDate, Time: GraphQLTime, DateTime: GraphQLDateTime, Query: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } }, Mutation: { sayHello: (obj, args, context, info) => { return `Hello ${ args.name }!`; } } };
Outre la date et l'heure, il existe également d'autres implémentations intéressantes de types de données scalaires, qui peuvent vous être utiles en fonction de votre cas d'utilisation. Par exemple, l'un d'eux est graphql-type-json, qui nous donne la possibilité d'utiliser le typage dynamique dans notre schéma GraphQL et de transmettre ou de renvoyer des objets JSON non typés à l'aide de notre API. Il existe également la bibliothèque graphql-scalar, qui nous donne la possibilité de définir des scalaires GraphQL personnalisés avec une désinfection/validation/transformation avancée.
Si nécessaire, vous pouvez également définir votre type de données scalaire personnalisé et l'utiliser dans votre schéma, comme indiqué ci-dessus. Ce n'est pas difficile, mais la discussion à ce sujet sort du cadre de cet article. Si vous êtes intéressé, vous pouvez trouver des informations plus avancées dans la documentation d'Apollo.
Schéma de fractionnement
Après avoir ajouté plus de fonctionnalités à votre schéma, il commencera à grandir et nous comprendrons qu'il est impossible de conserver l'ensemble des définitions dans un seul fichier, et nous devons le diviser en petits morceaux pour organiser le code et le rendre plus évolutif pour une taille plus grande. Heureusement, la fonction de création de schéma makeExecutableSchema
, fournie par Apollo, accepte également les définitions de schéma et les cartes de résolution sous la forme d'un tableau. Cela nous donne la possibilité de diviser notre schéma et notre carte de résolveurs en parties plus petites. C'est exactement ce que j'ai fait dans mon exemple de projet; J'ai divisé l'API en les parties suivantes :
-
auth.api.graphql
- API pour l'authentification et l'enregistrement des utilisateurs -
author.api.graphql
- API CRUD pour les entrées d'auteur -
book.api.graphql
- API CRUD pour les entrées de livre -
root.api.graphql
- Racine du schéma et des définitions communes (comme les types scalaires avancés) -
user.api.graphql
– API CRUD pour la gestion des utilisateurs
Pendant le schéma de clivage, il y a une chose que nous devons considérer. L'une des parties doit être le schéma racine et les autres doivent étendre le schéma racine. Cela semble complexe, mais en réalité c'est assez simple. Dans le schéma racine, les requêtes et les mutations sont définies comme ceci :
type Query { ... } type Mutation { ... }
Et dans les autres, ils sont définis comme ceci :
extend type Query { ... } extend type Mutation { ... }
Et c'est tout.
Authentification et autorisation
Dans la majorité des implémentations d'API, il est nécessaire de restreindre l'accès global et de fournir une sorte de règles d'accès basées sur des règles. Pour cela, nous devons introduire dans notre code : Authentification - pour confirmer l'identité de l'utilisateur - et Autorisation , pour appliquer des politiques d'accès basées sur des règles.
Dans le monde GraphQL, comme le monde REST, généralement pour l'authentification, nous utilisons JSON Web Token. Pour valider le jeton JWT passé, nous devons intercepter toutes les demandes entrantes et vérifier l'en-tête d'autorisation sur celles-ci. Pour cela, lors de la création du serveur Apollo, nous pouvons enregistrer une fonction en tant que crochet de contexte, qui sera appelée avec la requête en cours qui crée le contexte partagé entre tous les résolveurs. Cela peut être fait comme ceci :
// Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, context: ({ req, res }) => { const context = {}; // Verify jwt token const parts = req.headers.authorization ? req.headers.authorization.split(' ') : ['']; const token = parts.length === 2 && parts[0].toLowerCase() === 'bearer' ? parts[1] : undefined; context.authUser = token ? verify(token) : undefined; return context; } }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); });
Ici, si l'utilisateur transmet un jeton JWT correct, nous le vérifions et stockons l'objet utilisateur dans le contexte, qui sera accessible à tous les résolveurs lors de l'exécution de la requête.
Nous avons vérifié l'identité des utilisateurs, mais notre API est toujours accessible dans le monde entier et rien n'empêche nos utilisateurs de l'appeler sans autorisation. Une façon d'éviter cela consiste à vérifier l'objet utilisateur en contexte directement dans chaque résolveur, mais il s'agit d'une approche très sujette aux erreurs car nous devons écrire beaucoup de code passe-partout et nous pouvons oublier d'ajouter la vérification lors de l'ajout d'un nouveau résolveur. . Si nous examinons les frameworks API REST, généralement ces types de problèmes sont résolus à l'aide d'intercepteurs de requêtes HTTP, mais dans le cas de GraphQL, cela n'a pas de sens car une requête HTTP peut contenir plusieurs requêtes GraphQL, et si nous ajoutons toujours cela, nous n'avons accès qu'à la représentation brute de la chaîne de la requête et devons l'analyser manuellement, ce qui n'est certainement pas une bonne approche. Ce concept ne se traduit pas bien de REST à GraphQL.
Nous avons donc besoin d'un moyen d'intercepter les requêtes GraphQL, et ce moyen s'appelle prisma-graphql-middleware. Cette bibliothèque nous permet d'exécuter du code arbitraire avant ou après l'invocation d'un résolveur. Il améliore notre structure de code en permettant la réutilisation du code et une séparation claire des préoccupations.
La communauté GraphQL a déjà créé un tas de middlewares géniaux basés sur la bibliothèque middleware Prisma, qui résout certains cas d'utilisation spécifiques, et pour l'autorisation des utilisateurs, il existe une bibliothèque appelée graphql-shield, qui nous aide à créer une couche d'autorisation pour notre API.
Après avoir installé graphql-shield, nous pouvons introduire une couche d'autorisation pour notre API comme celle-ci :
import { allow } from 'graphql-shield'; const isAuthorized = rule()( (obj, args, { authUser }, info) => authUser && true ); export const permissions = { Query: { '*': isAuthorized, sayHello: allow }, Mutation: { '*': isAuthorized, sayHello: allow } }
Et nous pouvons appliquer cette couche en tant que middleware à notre schéma comme ceci :
// Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithMiddleware = applyMiddleware(schema, shield(permissions, { allowExternalErrors: true })); // Build Apollo server const apolloServer = new ApolloServer({ schemaWithMiddleware }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })
Ici, lors de la création d'un objet bouclier, nous définissons allowExternalErrors
sur true, car par défaut, le comportement du bouclier consiste à intercepter et gérer les erreurs qui se produisent à l'intérieur des résolveurs, et ce n'était pas un comportement acceptable pour mon exemple d'application.

Dans l'exemple ci-dessus, nous avons limité l'accès à notre API uniquement aux utilisateurs authentifiés, mais le bouclier est très flexible et, en l'utilisant, nous pouvons implémenter un schéma d'autorisation très riche pour nos utilisateurs. Par exemple, dans notre exemple d'application, nous avons deux rôles : USER
et USER_MANAGER
, et seuls les utilisateurs ayant le rôle USER_MANAGER
peuvent appeler la fonctionnalité d'administration des utilisateurs. Ceci est implémenté comme ceci:
export const isUserManager = rule()( (obj, args, { authUser }, info) => authUser && authUser.role === 'USER_MANAGER' ); export const permissions = { Query: { userById: isUserManager, users: isUserManager }, Mutation: { editUser: isUserManager, deleteUser: isUserManager } }
Une autre chose que je veux mentionner est la façon d'organiser les fonctions du middleware dans notre projet. Comme pour les définitions de schéma et les cartes de résolveurs, il est préférable de les diviser par schéma et de les conserver dans des fichiers séparés, mais contrairement au serveur Apollo, qui accepte des tableaux de définitions de schéma et de cartes de résolveurs et les assemble pour nous, la bibliothèque middleware Prisma ne le fait pas et n'accepte qu'un seul objet de carte middleware, donc si nous les divisons, nous devons les recoudre manuellement. Pour voir ma solution à ce problème, veuillez consulter la classe ApiExplorer
dans l'exemple de projet.
Validation
GraphQL SDL fournit des fonctionnalités très limitées pour valider les entrées de l'utilisateur ; nous pouvons seulement définir quel champ est obligatoire et lequel est facultatif. Toute autre exigence de validation, nous devons la mettre en œuvre manuellement. Nous pouvons appliquer des règles de validation directement dans les fonctions de résolution, mais cette fonctionnalité n'a vraiment pas sa place ici, et c'est un autre excellent cas d'utilisation pour les middlewares GraphQL. Par exemple, utilisons les données d'entrée de la demande d'inscription de l'utilisateur, où nous devons valider si le nom d'utilisateur est une adresse e-mail correcte, si les entrées du mot de passe correspondent et si le mot de passe est suffisamment fort. Cela peut être implémenté comme ceci :
import { UserInputError } from 'apollo-server-express'; import passwordValidator from 'password-validator'; import { isEmail } from 'validator'; const passwordSchema = new passwordValidator() .is().min(8) .is().max(20) .has().letters() .has().digits() .has().symbols() .has().not().spaces(); export const validators = { Mutation: { signup: (resolve, parent, args, context) => { const { email, password, rePassword } = args.signupReq; if (!isEmail(email)) { throw new UserInputError('Invalid Email address!'); } if (password !== rePassword) { throw new UserInputError('Passwords don\'t match!'); } if (!passwordSchema.validate(password)) { throw new UserInputError('Password is not strong enough!'); } return resolve(parent, args, context); } } }
Et nous pouvons appliquer la couche des validateurs en tant que middleware à notre schéma, avec une couche d'autorisations comme celle-ci :
// Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); const schemaWithMiddleware = applyMiddleware(schema, validators, shield(permissions, { allowExternalErrors: true })); // Build Apollo server const apolloServer = new ApolloServer({ schemaWithMiddleware }); apolloServer.applyMiddleware({ app })
N + 1 requêtes
Un autre problème à prendre en compte, qui se produit avec les API GraphQL et est souvent négligé, est les requêtes N + 1. Ce problème se produit lorsque nous avons une relation un-à-plusieurs entre les types définis dans notre schéma. Pour le démontrer, par exemple, utilisons l'API book de notre exemple de projet :
extend type Query { books: [Book!]! ... } extend type Mutation { ... } type Book { id: ID! creator: User! createdAt: DateTime! updatedAt: DateTime! authors: [Author!]! title: String! about: String language: String genre: String isbn13: String isbn10: String publisher: String publishDate: Date hardcover: Int } type User { id: ID! createdAt: DateTime! updatedAt: DateTime! fullName: String! email: String! }
Ici, nous voyons que le type User
a une relation un-à-plusieurs avec le type de Book
, et cette relation est représentée en tant que champ créateur dans Book
. La carte des résolveurs pour ce schéma est définie comme suit :
export const resolvers = { Query: { books: (obj, args, context, info) => { return bookService.findAll(); }, ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { return userService.findById(creatorId); }, ... } }
Si nous exécutons une requête de livres à l'aide de cette API et regardons le journal des instructions SQL, nous verrons quelque chose comme ceci :
select `books`.* from `books` select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? select `users`.* from `users` where `users`.`id` = ? ...
C'est facile à deviner - lors de l'exécution, le résolveur a d'abord été appelé pour la requête livres, qui a renvoyé la liste des livres, puis chaque objet livre a été appelé résolveur de champ créateur, et ce comportement a provoqué N + 1 requêtes de base de données. Si nous ne voulons pas exploser notre base de données, ce genre de comportement n'est pas vraiment génial.
Pour résoudre le problème des requêtes N+1, les développeurs de Facebook ont créé une solution très intéressante appelée DataLoader, qui est décrite sur sa page README comme ceci :
"DataLoader est un utilitaire générique à utiliser dans le cadre de la couche de récupération de données de votre application pour fournir une API simplifiée et cohérente sur diverses sources de données distantes telles que des bases de données ou des services Web via le traitement par lots et la mise en cache"
Il n'est pas très simple de comprendre le fonctionnement de DataLoader. Voyons donc d'abord l'exemple qui résout le problème présenté ci-dessus, puis expliquons la logique qui le sous-tend.
Dans notre exemple de projet, DataLoader est défini comme ceci pour le champ Creator :
export class UserDataLoader extends DataLoader { constructor() { const batchLoader = userIds => { return userService .findByIds(userIds) .then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) ); }; super(batchLoader); } static getInstance(context) { if (!context.userDataLoader) { context.userDataLoader = new UserDataLoader(); } return context.userDataLoader; } }
Une fois que nous avons défini UserDataLoader, nous pouvons changer le résolveur du champ créateur comme ceci :
export const resolvers = { Query: { ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { const userDataLoader = UserDataLoader.getInstance(context); return userDataLoader.load(creatorId); }, ... } }
Après les modifications appliquées, si nous exécutons à nouveau la requête de livres et regardons le journal des instructions SQL, nous verrons quelque chose comme ceci :
select `books`.* from `books` select `users`.* from `users` where `id` in (?)
Ici, nous pouvons voir que les requêtes de base de données N + 1 ont été réduites à deux requêtes, où la première sélectionne la liste des livres et la seconde sélectionne la liste des utilisateurs présentés comme créateurs dans la liste des livres. Expliquons maintenant comment DataLoader atteint ce résultat.
La fonctionnalité principale de DataLoader est le traitement par lots. Au cours de la phase d'exécution unique, DataLoader collectera tous les identifiants distincts de tous les appels de fonction de chargement individuels, puis appellera la fonction batch avec tous les identifiants demandés. Une chose importante à retenir est que les instances de DataLoaders ne peuvent pas être réutilisées, une fois la fonction batch appelée, les valeurs renvoyées seront mises en cache dans l'instance pour toujours. En raison de ce comportement, nous devons créer une nouvelle instance de DataLoader pour chaque phase d'exécution. Pour ce faire, nous avons créé une fonction statique getInstance
, qui vérifie si l'instance de DataLoader est présentée dans un objet de contexte et, si elle n'est pas trouvée, en crée un. N'oubliez pas qu'un nouvel objet de contexte est créé pour chaque phase d'exécution et est partagé par tous les résolveurs.
Une fonction de chargement par lots de DataLoader accepte un tableau d'ID demandés distincts et renvoie une promesse qui se résout en un tableau d'objets correspondants. Lors de l'écriture d'une fonction de chargement par lots, nous devons nous souvenir de deux choses importantes :
- Le tableau des résultats doit être de la même longueur que le tableau des ID demandés. Par exemple, si nous avons demandé les identifiants
[1, 2, 3]
, le tableau de résultats renvoyé doit contenir exactement trois objets :[{ "id": 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }, { “id”: 3, “fullName”: “user3” }]
- Chaque index dans le tableau des résultats doit correspondre au même index dans le tableau des ID demandés. Par exemple, si le tableau des ID demandés a l'ordre suivant :
[3, 1, 2]
, alors le tableau de résultats renvoyé doit contenir des objets exactement dans le même ordre :[{ "id": 3, “fullName”: “user3” }, { “id”: 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }]
Dans notre exemple, nous nous assurons que l'ordre des résultats correspond à l'ordre des identifiants demandés avec le code suivant :
then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) )
Sécurité
Et le dernier mais non le moindre, je veux mentionner la sécurité. Avec GraphQL, nous pouvons créer des API très flexibles et donner à l'utilisateur de riches capacités pour interroger les données. Cela accorde beaucoup de pouvoir au côté client de l'application et, comme l'a dit Uncle Ben, "Avec un grand pouvoir vient une grande responsabilité." Sans une sécurité adéquate, un utilisateur malveillant peut soumettre une requête coûteuse et provoquer une attaque DoS (Denial of Service) sur notre serveur.
La première chose que nous pouvons faire pour protéger notre API est de désactiver l'introspection du schéma GraphQL. Par défaut, un serveur d'API GraphQL expose la capacité d'introspecter l'intégralité de son schéma, qui est généralement utilisé par des shells visuels interactifs comme GraphiQL et Apollo Playground, mais il peut également être très utile pour un utilisateur malveillant de construire une requête complexe basée sur notre API. . Nous pouvons désactiver cela en définissant le paramètre d' introspection
sur false lors de la création du serveur Apollo :
// Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, introspection: false }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })
La prochaine chose que nous pouvons faire pour protéger notre API est de limiter la profondeur de la requête. Ceci est particulièrement important si nous avons une relation cyclique entre nos types de données. Par exemple, dans notre exemple, le type de projet Author
a des carnets de terrain et le type Book
a des auteurs de terrain. Il s'agit clairement d'une relation cyclique, et rien n'empêche un utilisateur malveillant d'écrire une requête comme celle-ci :
query { authors { id, fullName books { id, title authors { id, fullName books { id, title, authors { id, fullName books { id, title authors { ... } } } } } } } }
Il est clair qu'avec suffisamment d'imbrication, une telle requête peut facilement faire exploser notre serveur. Pour limiter la profondeur des requêtes, nous pouvons utiliser une bibliothèque appelée graphql-depth-limit. Une fois que nous l'avons installé, nous pouvons appliquer une restriction de profondeur lors de la création d'Apollo Server, comme ceci :
// Configure express const app = express(); // Build GraphQL schema based on SDL definitions and resolver maps const schema = makeExecutableSchema({ typeDefs, resolvers }); // Build Apollo server const apolloServer = new ApolloServer({ schema, introspection: false, validationRules: [ depthLimit(5) ] }); apolloServer.applyMiddleware({ app }); // Run server app.listen({ port }, () => { console.log(`Server ready at http://localhost:${ port }${ apolloServer.graphqlPath }`); })
Ici, nous avons limité la profondeur maximale des requêtes à cinq.
Post Scriptum : Passer de REST à GraphQL est intéressant
Dans ce didacticiel, j'ai essayé de démontrer les problèmes courants que vous rencontrerez lors du démarrage de la mise en œuvre des API GraphQL. Cependant, certaines parties de celui-ci fournissent des exemples de code très superficiels et n'effleurent que la surface du problème discuté, en raison de sa taille. Pour cette raison, pour voir des exemples de code plus complets, veuillez vous référer au référentiel Git de mon exemple de projet d'API GraphQL : graphql-example.
Au final, je tiens à dire que GraphQL est une technologie vraiment intéressante. Remplacera-t-il REST ? Personne ne le sait, peut-être que demain, dans le monde en évolution rapide de l'informatique, apparaîtra une meilleure approche pour développer des API, mais GraphQL entre vraiment dans la catégorie des technologies intéressantes qui valent vraiment la peine d'être apprises.