Crearea primului tău API GraphQL

Publicat: 2022-03-11

cuvânt înainte

În urmă cu câțiva ani, Facebook a introdus un nou mod de a construi API-uri back-end numit GraphQL, care este practic un limbaj specific domeniului pentru interogare și manipulare a datelor. La început, nu i-am acordat prea multă atenție, dar în cele din urmă, m-am trezit implicat într-un proiect la Toptal, în care a trebuit să implementez API-uri back-end bazate pe GraphQL. Atunci am mers înainte și am învățat cum să aplic cunoștințele pe care le-am învățat pentru REST la GraphQL.

A fost o experiență foarte interesantă și, în perioada de implementare, a trebuit să regândesc abordările și metodologiile standard utilizate în API-urile REST într-o manieră mai prietenoasă cu GraphQL. În acest articol, încerc să rezum problemele comune de luat în considerare atunci când implementez API-urile GraphQL pentru prima dată.

Biblioteci necesare

GraphQL a fost dezvoltat intern de Facebook și lansat public în 2015. Mai târziu, în 2018, proiectul GraphQL a fost mutat de la Facebook la nou-înființata Fundație GraphQL, găzduită de Fundația Linux nonprofit, care menține și dezvoltă specificația limbajului de interogare GraphQL și o referință. implementare pentru JavaScript.

Deoarece GraphQL este încă o tehnologie tânără și implementarea de referință inițială a fost disponibilă pentru JavaScript, majoritatea bibliotecilor mature pentru aceasta există în ecosistemul Node.js. Există, de asemenea, alte două companii, Apollo și Prisma, care furnizează instrumente și biblioteci open-source pentru GraphQL. Proiectul exemplu din acest articol se va baza pe o implementare de referință a GraphQL pentru JavaScript și biblioteci furnizate de aceste două companii:

  • Graphql-js – O implementare de referință a GraphQL pentru JavaScript
  • Apollo-server – Server GraphQL pentru Express, Connect, Hapi, Koa și multe altele
  • Apollo-graphql-tools - Construiți, bateți joc și îmbinați o schemă GraphQL folosind SDL
  • Prisma-graphql-middleware – Împărțiți rezolutoarele GraphQL în funcții middleware

În lumea GraphQL, vă descrieți API-urile folosind scheme GraphQL, iar pentru acestea, specificația își definește propriul limbaj numit Limbajul de definire a schemei GraphQL (SDL). SDL este foarte simplu și intuitiv de utilizat, fiind în același timp extrem de puternic și expresiv.

Există două moduri de a crea scheme GraphQL: abordarea cu codul întâi și abordarea pe primul plan.

  • În abordarea bazată pe cod, descrieți schemele GraphQL ca obiecte JavaScript bazate pe biblioteca graphql-js, iar SDL-ul este generat automat din codul sursă.
  • În abordarea cu schema-prima, descrieți schemele GraphQL în SDL și vă conectați logica de afaceri folosind biblioteca Apollo graphql-tools.

Personal, prefer abordarea schema-first și o voi folosi pentru proiectul exemplu din acest articol. Vom implementa un exemplu clasic de librărie și vom crea un back-end care va oferi API-uri CRUD pentru a crea autori și cărți plus API-uri pentru gestionarea și autentificarea utilizatorilor.

Crearea unui server GraphQL de bază

Pentru a rula un server GraphQL de bază, trebuie să creăm un nou proiect, să-l inițializam cu npm și să configuram Babel. Pentru a configura Babel, mai întâi instalați bibliotecile necesare cu următoarea comandă:

 npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node

După instalarea Babel, creați un fișier cu numele .babelrc în directorul rădăcină al proiectului nostru și copiați următoarea configurație acolo:

 { "presets": [ [ "@babel/env", { "targets": { "node": "current" } } ] ] }

Editați, de asemenea, fișierul package.json și adăugați următoarea comandă la secțiunea de scripts :

 { ... "scripts": { "serve": "babel-node index.js" }, ... }

După ce am configurat Babel, instalați bibliotecile GraphQL necesare cu următoarea comandă:

 npm install --save express apollo-server-express graphql graphql-tools graphql-tag

După instalarea bibliotecilor necesare, pentru a rula un server GraphQL cu o configurare minimă, copiați acest fragment de cod în fișierul nostru 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 }`); });

După aceasta, putem rula serverul nostru folosind comanda npm run serve , iar dacă navigăm într-un browser web la adresa URL http://localhost:8080/graphql , se va deschide shell-ul vizual interactiv al GraphQL, numit Playground, unde putem executați interogări și mutații GraphQL și vedeți datele rezultate.

În lumea GraphQL, funcțiile API sunt împărțite în trei seturi, numite interogări, mutații și abonamente:

  • Interogările sunt folosite de client pentru a solicita de la server datele de care are nevoie.
  • Mutațiile sunt folosite de client pentru a crea/actualiza/șterge date de pe server.
  • Abonamentele sunt folosite de client pentru a crea și menține conexiunea în timp real la server. Acest lucru permite clientului să primească evenimente de la server și să acționeze în consecință.

În articolul nostru, vom discuta doar interogări și mutații. Abonamentele sunt un subiect uriaș - își merită propriul articol și nu sunt necesare în fiecare implementare API.

Tipuri de date scalare avansate

Foarte curând după ce ați jucat cu GraphQL, veți descoperi că SDL oferă doar tipuri de date primitive, iar tipurile de date scalare avansate, cum ar fi Date, Time și DateTime, care sunt o parte importantă a fiecărui API, lipsesc. Din fericire, avem o bibliotecă care ne ajută să rezolvăm această problemă și se numește graphql-iso-date. După instalarea acestuia, va trebui să definim noi tipuri de date scalare avansate în schema noastră și să le conectăm la implementările furnizate de bibliotecă:

 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 }!`; } } };

Pe lângă data și ora, există și alte implementări interesante de tip de date scalare, care vă pot fi utile în funcție de cazul dvs. de utilizare. De exemplu, unul dintre ele este graphql-type-json, care ne oferă posibilitatea de a folosi tastarea dinamică în schema noastră GraphQL și de a transmite sau returna obiecte JSON netipizate folosind API-ul nostru. Există, de asemenea, biblioteca graphql-scalar, care ne oferă posibilitatea de a defini scalari GraphQL personalizați cu igienizare/validare/transformare avansată.

Dacă este necesar, puteți defini și tipul dvs. de date scalare personalizate și să-l utilizați în schema dvs., așa cum se arată mai sus. Acest lucru nu este dificil, dar discuția despre aceasta este în afara domeniului de aplicare al acestui articol - dacă sunteți interesat, puteți găsi informații mai avansate în documentația Apollo.

Schema de împărțire

După ce adăugați mai multe funcționalități la schema dvs., aceasta va începe să crească și vom înțelege că este imposibil să păstrăm întregul set de definiții într-un singur fișier și trebuie să-l împărțim în bucăți mici pentru a organiza codul și a-l face mai scalabil pentru o dimensiune mai mare. Din fericire, funcția de generare a schemelor makeExecutableSchema , furnizată de Apollo, acceptă, de asemenea, definiții de schemă și hărți de rezoluție sub forma unui tablou. Acest lucru ne oferă posibilitatea de a împărți schema și harta soluțiilor noastre în părți mai mici. Este exact ceea ce am făcut în proiectul meu exemplu; Am împărțit API-ul în următoarele părți:

  • auth.api.graphql – API pentru autentificarea și înregistrarea utilizatorilor
  • author.api.graphql – API-ul CRUD pentru intrările de autor
  • book.api.graphql – API-ul CRUD pentru intrări în carte
  • root.api.graphql – Rădăcina schemei și definițiile comune (cum ar fi tipurile scalare avansate)
  • user.api.graphql – CRUD API pentru gestionarea utilizatorilor

În timpul schemei de împărțire, există singurul lucru pe care trebuie să îl luăm în considerare. Una dintre părți trebuie să fie schema rădăcină, iar celelalte trebuie să extindă schema rădăcină. Acest lucru sună complex, dar în realitate este destul de simplu. În schema rădăcină, interogările și mutațiile sunt definite astfel:

 type Query { ... } type Mutation { ... }

Și în celelalte, acestea sunt definite astfel:

 extend type Query { ... } extend type Mutation { ... }

Și asta e tot.

Autentificare și autorizare

În majoritatea implementărilor API, există o cerință de a restricționa accesul global și de a oferi un fel de politici de acces bazate pe reguli. Pentru aceasta, trebuie să introducem în codul nostru: Autentificare — pentru a confirma identitatea utilizatorului — și Autorizare , pentru a aplica politicile de acces bazate pe reguli.

În lumea GraphQL, ca și în lumea REST, în general, pentru autentificare, folosim JSON Web Token. Pentru a valida jetonul JWT transmis, trebuie să interceptăm toate cererile primite și să verificăm antetul de autorizare de pe ele. Pentru aceasta, în timpul creării serverului Apollo, putem înregistra o funcție ca un cârlig de context, care va fi apelată cu cererea curentă care creează contextul partajat între toate rezolutoarele. Acest lucru se poate face astfel:

 // 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 }`); });

Aici, dacă utilizatorul va trece un token JWT corect, îl verificăm și stocăm obiectul utilizator în context, care va fi accesibil pentru toți rezolutorii în timpul executării cererii.

Am verificat identitatea utilizatorului, dar API-ul nostru este încă accesibil la nivel global și nimic nu îi împiedică pe utilizatorii noștri să îl apeleze fără autorizație. O modalitate de a preveni acest lucru este să verificăm obiectul utilizator în context direct în fiecare rezolutor, dar aceasta este o abordare foarte predispusă la erori, deoarece trebuie să scriem mult cod standard și putem uita să adăugăm verificarea atunci când adăugăm un nou resolver. . Dacă ne uităm la cadrele API REST, în general astfel de probleme sunt rezolvate folosind interceptori de solicitări HTTP, dar în cazul GraphQL, nu are sens, deoarece o solicitare HTTP poate conține mai multe interogări GraphQL și dacă totuși adăugăm acesta, avem acces doar la reprezentarea brută a șirului de interogare și trebuie să o analizăm manual, ceea ce cu siguranță nu este o abordare bună. Acest concept nu se traduce bine de la REST la GraphQL.

Deci avem nevoie de un fel de modalitate de a intercepta interogările GraphQL, iar acest mod se numește prisma-graphql-middleware. Această bibliotecă ne permite să rulăm cod arbitrar înainte sau după invocarea unui resolver. Îmbunătățește structura codului nostru, permițând reutilizarea codului și o separare clară a preocupărilor.

Comunitatea GraphQL a creat deja o mulțime de middleware minunat bazate pe biblioteca de middleware Prisma, care rezolvă unele cazuri de utilizare specifice, iar pentru autorizarea utilizatorului, există o bibliotecă numită graphql-shield, care ne ajută să creăm un strat de permisiuni pentru API-ul nostru.

După instalarea graphql-shield, putem introduce un strat de permisiuni pentru API-ul nostru, astfel:

 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 } }

Și putem aplica acest strat ca middleware la schema noastră astfel:

 // 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 }`); })

Aici, când creăm un obiect scut, setăm allowExternalErrors la true, deoarece în mod implicit, comportamentul scutului este de a capta și gestiona erorile care apar în interiorul rezolutoarelor, iar acesta nu a fost un comportament acceptabil pentru aplicația mea eșantion.

În exemplul de mai sus, am restricționat accesul la API-ul nostru doar pentru utilizatorii autentificați, dar scutul este foarte flexibil și, folosindu-l, putem implementa o schemă de autorizare foarte bogată pentru utilizatorii noștri. De exemplu, în aplicația noastră exemplu, avem două roluri: USER și USER_MANAGER și numai utilizatorii cu rolul USER_MANAGER pot apela funcționalitatea de administrare a utilizatorilor. Acesta este implementat astfel:

 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 } }

Încă un lucru pe care vreau să-l menționez este cum să organizăm funcțiile middleware în proiectul nostru. Ca și în cazul definițiilor de schemă și al hărților rezolutoare, este mai bine să le împărțim pe schemă și să le păstrăm în fișiere separate, dar spre deosebire de serverul Apollo, care acceptă matrice de definiții de schemă și hărți de rezoluție și le împletește pentru noi, biblioteca middleware Prisma nu face acest lucru și acceptă un singur obiect hartă middleware, așa că dacă le împărțim, trebuie să le împletim manual. Pentru a vedea soluția mea pentru această problemă, vă rugăm să vedeți clasa ApiExplorer din proiectul exemplu.

Validare

GraphQL SDL oferă o funcționalitate foarte limitată pentru a valida intrarea utilizatorului; putem defini doar ce câmp este obligatoriu și care este opțional. Orice cerințe suplimentare de validare, trebuie să implementăm manual. Putem aplica reguli de validare direct în funcțiile de rezolvare, dar această funcționalitate chiar nu aparține aici și acesta este un alt caz de utilizare grozav pentru middleware-urile GraphQL ale utilizatorului. De exemplu, să folosim datele de intrare ale cererii de înscriere a utilizatorului, unde trebuie să validăm dacă numele de utilizator este o adresă de e-mail corectă, dacă introducerea parolei se potrivește și parola este suficient de puternică. Acest lucru poate fi implementat astfel:

 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); } } }

Și putem aplica stratul validatorilor ca middleware la schema noastră, împreună cu un strat de permisiuni ca acesta:

 // 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 Interogări

O altă problemă de luat în considerare, care se întâmplă cu API-urile GraphQL și este adesea trecută cu vederea, este N + 1 interogări. Această problemă se întâmplă atunci când avem o relație unu-la-mai multe între tipurile definite în schema noastră. Pentru a-l demonstra, de exemplu, să folosim API-ul de carte a proiectului nostru exemplu:

 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! }

Aici, vedem că tipul de User are o relație unu-la-mulți cu tipul Book , iar această relație este reprezentată ca câmp de creator în Book . Harta rezolutorilor pentru această schemă este definită astfel:

 export const resolvers = { Query: { books: (obj, args, context, info) => { return bookService.findAll(); }, ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { return userService.findById(creatorId); }, ... } }

Dacă executăm o interogare de cărți folosind acest API și ne uităm la jurnalul de instrucțiuni SQL, vom vedea ceva de genul acesta:

 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` = ? ...

Este ușor de ghicit — în timpul execuției, soluția a fost chemată mai întâi pentru interogarea cărților, care a returnat o listă de cărți și apoi fiecare obiect de carte a fost numit solutor de câmp al creatorului, iar acest comportament a cauzat N + 1 interogări la baza de date. Dacă nu vrem să ne explodăm baza de date, un astfel de comportament nu este chiar grozav.

Pentru a rezolva problema interogărilor N + 1, dezvoltatorii Facebook au creat o soluție foarte interesantă numită DataLoader, care este descrisă pe pagina README astfel:

„DataLoader este un utilitar generic care poate fi utilizat ca parte a stratului de preluare a datelor al aplicației dvs. pentru a oferi un API simplificat și consistent pe diferite surse de date la distanță, cum ar fi baze de date sau servicii web prin loturi și stocare în cache”

Nu este foarte simplu să înțelegeți cum funcționează DataLoader, așa că să vedem mai întâi exemplul care rezolvă problema demonstrată mai sus și apoi să explicăm logica din spatele acesteia.

În proiectul nostru exemplu, DataLoader este definit astfel pentru câmpul 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; } }

Odată ce am definit UserDataLoader, putem schimba soluția câmpului creator astfel:

 export const resolvers = { Query: { ... }, Mutation: { ... }, Book: { creator: ({ creatorId }, args, context, info) => { const userDataLoader = UserDataLoader.getInstance(context); return userDataLoader.load(creatorId); }, ... } }

După modificările aplicate, dacă executăm din nou interogarea cărți și ne uităm la jurnalul de instrucțiuni SQL, vom vedea ceva de genul acesta:

 select `books`.* from `books` select `users`.* from `users` where `id` in (?)

Aici, putem observa că N + 1 interogări de baze de date s-au redus la două interogări, unde prima selectează lista de cărți, iar a doua selectează lista de utilizatori prezentați ca creatori în lista de cărți. Acum să explicăm cum DataLoader atinge acest rezultat.

Caracteristica principală a DataLoader este gruparea. În timpul unei singure etape de execuție, DataLoader va colecta toate ID-urile distincte ale tuturor apelurilor individuale ale funcției de încărcare și apoi va apela funcția batch cu toate ID-urile solicitate. Un lucru important de reținut este că instanțele DataLoaders nu pot fi reutilizate, odată ce funcția batch este apelată, valorile returnate vor fi stocate în cache pentru totdeauna. Datorită acestui comportament, trebuie să creăm o nouă instanță de DataLoader pentru fiecare fază de execuție. Pentru a realiza acest lucru, am creat o funcție statică getInstance , care verifică dacă instanța DataLoader este prezentată într-un obiect context și, dacă nu este găsită, creează unul. Amintiți-vă că un nou obiect de context este creat pentru fiecare fază de execuție și este partajat între toate rezolutoarele.

O funcție de încărcare lot a DataLoader acceptă o serie de ID-uri solicitate distincte și returnează o promisiune care se rezolvă într-o serie de obiecte corespunzătoare. Când scriem o funcție de încărcare în lot, trebuie să ne amintim două lucruri importante:

  1. Matricea de rezultate trebuie să aibă aceeași lungime ca și matricea de ID-uri solicitate. De exemplu, dacă am solicitat ID-urile [1, 2, 3] , tabloul de rezultate returnat trebuie să conțină exact trei obiecte: [{ "id": 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }, { “id”: 3, “fullName”: “user3” }]
  2. Fiecare index din matricea de rezultate trebuie să corespundă aceluiași index din matricea de ID-uri solicitate. De exemplu, dacă matricea de ID-uri solicitate are următoarea ordine: [3, 1, 2] , atunci matricea de rezultate returnată trebuie să conțină obiecte exact în aceeași ordine: [{ "id": 3, “fullName”: “user3” }, { “id”: 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }]

În exemplul nostru, ne asigurăm că ordinea rezultatelor se potrivește cu ordinea ID-urilor solicitate cu următorul cod:

 then( users => userIds.map( userId => users.filter(user => user.id === userId)[0] ) )

Securitate

Și nu în ultimul rând, vreau să menționez securitatea. Cu GraphQL, putem crea API-uri foarte flexibile și putem oferi utilizatorului capabilități bogate pentru cum să interogheze datele. Acest lucru acordă destul de multă putere părții client a aplicației și, așa cum a spus unchiul Ben, „Cu o mare putere vine o mare responsabilitate”. Fără o securitate adecvată, un utilizator rău intenționat poate trimite o interogare costisitoare și poate provoca un atac DoS (Denial of Service) pe serverul nostru.

Primul lucru pe care îl putem face pentru a ne proteja API-ul este să dezactivăm introspecția schemei GraphQL. În mod implicit, un server API GraphQL expune capacitatea de a introspecta întreaga sa schemă, care este în general utilizată de shell-uri vizuale interactive precum GraphiQL și Apollo Playground, dar poate fi și foarte util pentru un utilizator rău intenționat pentru a construi o interogare complexă bazată pe API-ul nostru. . Putem dezactiva acest lucru setând parametrul de introspection la false atunci când creăm serverul 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 }`); })

Următorul lucru pe care îl putem face pentru a ne proteja API-ul este să limităm profunzimea interogării. Acest lucru este deosebit de important dacă avem o relație ciclică între tipurile noastre de date. De exemplu, în exemplul nostru, tipul de proiect Author are cărți de câmp și tipul Book are autori de câmp. Aceasta este în mod clar o relație ciclică și nimic nu împiedică utilizatorul rău intenționat să scrie o interogare ca aceasta:

 query { authors { id, fullName books { id, title authors { id, fullName books { id, title, authors { id, fullName books { id, title authors { ... } } } } } } } }

Este clar că, cu suficient imbricare, o astfel de interogare poate exploda cu ușurință serverul nostru. Pentru a limita profunzimea interogărilor, putem folosi o bibliotecă numită graphql-depth-limit. Odată ce l-am instalat, putem aplica o restricție de adâncime atunci când creăm Apollo Server, astfel:

 // 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 }`); })

Aici, am limitat profunzimea maximă a interogărilor la cinci.

Post Scriptum: Trecerea de la REST la GraphQL este interesantă

În acest tutorial, am încercat să demonstrez problemele comune pe care le veți întâlni atunci când începeți implementarea API-urilor GraphQL. Cu toate acestea, unele părți ale acestuia oferă exemple de cod foarte superficiale și zgârie doar suprafața problemei discutate, datorită dimensiunii sale. Din acest motiv, pentru a vedea exemple de cod mai complete, vă rugăm să consultați depozitul Git al exemplului meu de proiect API GraphQL: graphql-example.

În cele din urmă, vreau să spun că GraphQL este o tehnologie cu adevărat interesantă. Va înlocui REST? Nimeni nu știe, poate mâine, în lumea IT în schimbare rapidă, va apărea o abordare mai bună pentru a dezvolta API-uri, dar GraphQL se încadrează într-adevăr în categoria tehnologiilor interesante care merită cu siguranță învățate.