Creazione della tua prima API GraphQL

Pubblicato: 2022-03-11

Prefazione

Alcuni anni fa, Facebook ha introdotto un nuovo modo di creare API di back-end chiamato GraphQL, che fondamentalmente è un linguaggio specifico del dominio per la query e la manipolazione dei dati. All'inizio non ci prestavo molta attenzione, ma alla fine mi sono ritrovato impegnato in un progetto in Toptal, in cui dovevo implementare API di back-end basate su GraphQL. È allora che sono andato avanti e ho imparato come applicare le conoscenze che ho appreso per REST a GraphQL.

È stata un'esperienza molto interessante e durante il periodo di implementazione ho dovuto ripensare agli approcci e alle metodologie standard utilizzati nelle API REST in un modo più compatibile con GraphQL. In questo articolo, provo a riassumere i problemi comuni da considerare quando si implementano le API GraphQL per la prima volta.

Biblioteche richieste

GraphQL è stato sviluppato internamente da Facebook e rilasciato pubblicamente nel 2015. Successivamente, nel 2018, il progetto GraphQL è stato spostato da Facebook alla neonata GraphQL Foundation, ospitata dalla Linux Foundation senza scopo di lucro, che mantiene e sviluppa la specifica del linguaggio di query GraphQL e un riferimento implementazione per JavaScript.

Poiché GraphQL è ancora una tecnologia giovane e l'implementazione di riferimento iniziale era disponibile per JavaScript, la maggior parte delle librerie mature esistono nell'ecosistema Node.js. Ci sono anche altre due società, Apollo e Prisma, che forniscono strumenti e librerie open source per GraphQL. Il progetto di esempio in questo articolo sarà basato su un'implementazione di riferimento di GraphQL per JavaScript e sulle librerie fornite da queste due società:

  • Graphql-js – Un'implementazione di riferimento di GraphQL per JavaScript
  • Apollo-server: server GraphQL per Express, Connect, Hapi, Koa e altro
  • Apollo-graphql-tools: crea, simula e cuci uno schema GraphQL utilizzando l'SDL
  • Prisma-graphql-middleware – Suddividi i tuoi resolver GraphQL in funzioni middleware

Nel mondo GraphQL, descrivi le tue API usando gli schemi GraphQL e, per questi, la specifica definisce il proprio linguaggio chiamato The GraphQL Schema Definition Language (SDL). SDL è molto semplice e intuitivo da usare e allo stesso tempo estremamente potente ed espressivo.

Esistono due modi per creare schemi GraphQL: l'approccio code-first e l'approccio schema-first.

  • Nell'approccio code-first, descrivi i tuoi schemi GraphQL come oggetti JavaScript basati sulla libreria graphql-js e l'SDL viene generato automaticamente dal codice sorgente.
  • Nell'approccio schema-first, descrivi i tuoi schemi GraphQL in SDL e colleghi la tua logica aziendale utilizzando la libreria graphql-tools di Apollo.

Personalmente, preferisco l'approccio schema-first e lo userò per il progetto di esempio in questo articolo. Implementeremo un classico esempio di libreria e creeremo un back-end che fornirà API CRUD per creare autori e libri più API per la gestione e l'autenticazione degli utenti.

Creazione di un server GraphQL di base

Per eseguire un server GraphQL di base, dobbiamo creare un nuovo progetto, inizializzarlo con npm e configurare Babel. Per configurare Babel, installa prima le librerie richieste con il seguente comando:

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

Dopo aver installato Babel, crea un file con il nome .babelrc nella directory principale del nostro progetto e copia lì la seguente configurazione:

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

Modifica anche il file package.json e aggiungi il seguente comando alla sezione degli scripts :

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

Dopo aver configurato Babel, installa le librerie GraphQL richieste con il seguente comando:

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

Dopo aver installato le librerie richieste, per eseguire un server GraphQL con una configurazione minima, copia questo frammento di codice nel nostro file 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 }`); });

Successivamente, possiamo eseguire il nostro server utilizzando il comando npm run serve , e se navighiamo in un browser Web all'URL http://localhost:8080/graphql , si aprirà la shell visiva interattiva di GraphQL, chiamata Playground, dove possiamo eseguire query e mutazioni GraphQL e visualizzare i dati dei risultati.

Nel mondo GraphQL, le funzioni API sono divise in tre insiemi, chiamati query, mutazioni e abbonamenti:

  • Le query vengono utilizzate dal client per richiedere i dati di cui ha bisogno dal server.
  • Le mutazioni vengono utilizzate dal client per creare/aggiornare/eliminare dati sul server.
  • Le sottoscrizioni vengono utilizzate dal client per creare e mantenere una connessione in tempo reale al server. Ciò consente al client di ottenere eventi dal server e agire di conseguenza.

Nel nostro articolo discuteremo solo di domande e mutazioni. Le sottoscrizioni sono un argomento importante: meritano un articolo a parte e non sono richieste in ogni implementazione API.

Tipi di dati scalari avanzati

Subito dopo aver giocato con GraphQL, scoprirai che SDL fornisce solo tipi di dati primitivi e mancano tipi di dati scalari avanzati come Date, Time e DateTime, che sono una parte importante di ogni API. Fortunatamente, abbiamo una libreria che ci aiuta a risolvere questo problema e si chiama graphql-iso-date. Dopo averlo installato, dovremo definire nuovi tipi di dati scalari avanzati nel nostro schema e collegarli alle implementazioni fornite dalla libreria:

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

Insieme a data e ora, esistono anche altre interessanti implementazioni di tipi di dati scalari, che possono essere utili per te a seconda del tuo caso d'uso. Ad esempio, uno di questi è graphql-type-json, che ci dà la possibilità di utilizzare la tipizzazione dinamica nel nostro schema GraphQL e di passare o restituire oggetti JSON non tipizzati utilizzando la nostra API. Esiste anche la libreria graphql-scalar, che ci dà la possibilità di definire scalari GraphQL personalizzati con sanificazione/convalida/trasformazione avanzate.

Se necessario, puoi anche definire il tipo di dati scalare personalizzato e utilizzarlo nel tuo schema, come mostrato sopra. Non è difficile, ma la discussione al riguardo esula dallo scopo di questo articolo: se interessati, è possibile trovare informazioni più avanzate nella documentazione di Apollo.

Schema di divisione

Dopo aver aggiunto più funzionalità al tuo schema, inizierà a crescere e capiremo che è impossibile mantenere l'intero set di definizioni in un file e dobbiamo dividerlo in piccoli pezzi per organizzare il codice e renderlo più scalabile per una taglia più grande. Fortunatamente la funzione di creazione di schemi makeExecutableSchema , fornita da Apollo, accetta anche definizioni di schemi e mappe di risolutori sotto forma di array. Questo ci dà la possibilità di dividere il nostro schema e la mappa dei risolutori in parti più piccole. Questo è esattamente ciò che ho fatto nel mio progetto di esempio; Ho diviso l'API nelle seguenti parti:

  • auth.api.graphql – API per l'autenticazione e la registrazione dell'utente
  • author.api.graphql – API CRUD per le voci degli autori
  • book.api.graphql – API CRUD per voci di libri
  • root.api.graphql – Radice dello schema e definizioni comuni (come i tipi scalari avanzati)
  • user.api.graphql – API CRUD per la gestione degli utenti

Durante lo schema di divisione, c'è una cosa che dobbiamo considerare. Una delle parti deve essere lo schema radice e le altre devono estendere lo schema radice. Sembra complesso, ma in realtà è abbastanza semplice. Nello schema radice, le query e le mutazioni sono definite in questo modo:

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

E negli altri sono così definiti:

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

E questo è tutto.

Autenticazione e autorizzazione

Nella maggior parte delle implementazioni API, è necessario limitare l'accesso globale e fornire una sorta di criteri di accesso basati su regole. Per questo dobbiamo introdurre nel nostro codice: Autenticazione —per confermare l'identità dell'utente—e Autorizzazione , per applicare criteri di accesso basati su regole.

Nel mondo GraphQL, come nel mondo REST, generalmente per l'autenticazione utilizziamo JSON Web Token. Per convalidare il token JWT passato, dobbiamo intercettare tutte le richieste in arrivo e controllare l'intestazione di autorizzazione su di esse. Per questo, durante la creazione del server Apollo, possiamo registrare una funzione come hook di contesto, che verrà chiamata con la richiesta corrente che crea il contesto condiviso tra tutti i resolver. Questo può essere fatto in questo modo:

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

Qui, se l'utente passerà un token JWT corretto, lo verifichiamo e memorizziamo l'oggetto utente nel contesto, che sarà accessibile a tutti i resolver durante l'esecuzione della richiesta.

Abbiamo verificato l'identità dell'utente, ma la nostra API è ancora accessibile a livello globale e nulla impedisce ai nostri utenti di chiamarla senza autorizzazione. Un modo per impedirlo è controllare l'oggetto utente nel contesto direttamente in ogni risolutore, ma questo è un approccio molto soggetto a errori perché dobbiamo scrivere molto codice standard e possiamo dimenticare di aggiungere il controllo quando aggiungiamo un nuovo risolutore . Se diamo un'occhiata ai framework API REST, generalmente questo tipo di problemi viene risolto utilizzando gli intercettori di richieste HTTP, ma nel caso di GraphQL non ha senso perché una richiesta HTTP può contenere più query GraphQL e se aggiungiamo ancora it, otteniamo accesso solo alla rappresentazione della stringa grezza della query e dobbiamo analizzarla manualmente, il che sicuramente non è un buon approccio. Questo concetto non si traduce bene da REST a GraphQL.

Quindi abbiamo bisogno di un modo per intercettare le query GraphQL, e questo modo è chiamato prisma-graphql-middleware. Questa libreria ci consente di eseguire codice arbitrario prima o dopo l'invocazione di un risolutore. Migliora la nostra struttura del codice consentendo il riutilizzo del codice e una chiara separazione delle preoccupazioni.

La comunità GraphQL ha già creato un sacco di fantastici middleware basati sulla libreria middleware Prisma, che risolve alcuni casi d'uso specifici e, per l'autorizzazione dell'utente, esiste una libreria chiamata graphql-shield, che ci aiuta a creare un livello di autorizzazione per la nostra API.

Dopo aver installato graphql-shield, possiamo introdurre un livello di autorizzazione per la nostra API come questo:

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

E possiamo applicare questo livello come middleware al nostro schema in questo modo:

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

Qui, durante la creazione di un oggetto shield, impostiamo allowExternalErrors su true, perché per impostazione predefinita, il comportamento dello shield è quello di rilevare e gestire gli errori che si verificano all'interno dei resolver e questo non era un comportamento accettabile per la mia applicazione di esempio.

Nell'esempio sopra, abbiamo limitato l'accesso alla nostra API solo per gli utenti autenticati, ma lo scudo è molto flessibile e, utilizzandolo, possiamo implementare uno schema di autorizzazione molto ricco per i nostri utenti. Ad esempio, nella nostra applicazione di esempio, abbiamo due ruoli: USER e USER_MANAGER e solo gli utenti con il ruolo USER_MANAGER possono chiamare la funzionalità di amministrazione degli utenti. Questo è implementato in questo modo:

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

Un'altra cosa che voglio menzionare è come organizzare le funzioni del middleware nel nostro progetto. Come con le definizioni degli schemi e le mappe dei risolutori, è meglio dividerli per schema e conservarli in file separati, ma a differenza del server Apollo, che accetta array di definizioni di schema e mappe dei risolutori e le unisce per noi, la libreria middleware Prisma non lo fa e accetta solo un oggetto mappa middleware, quindi se li dividiamo dobbiamo ricucirli manualmente. Per vedere la mia soluzione per questo problema, vedere la classe ApiExplorer nel progetto di esempio.

Convalida

GraphQL SDL fornisce funzionalità molto limitate per convalidare l'input dell'utente; possiamo solo definire quale campo è obbligatorio e quale è facoltativo. Eventuali ulteriori requisiti di convalida, devono essere implementati manualmente. Possiamo applicare le regole di convalida direttamente nelle funzioni del risolutore, ma questa funzionalità in realtà non appartiene a qui, e questo è un altro ottimo caso d'uso per i middleware degli utenti GraphQL. Ad esempio, utilizziamo i dati di input della richiesta di registrazione dell'utente, in cui dobbiamo convalidare se il nome utente è un indirizzo e-mail corretto, se gli input della password corrispondono e la password è sufficientemente forte. Questo può essere implementato in questo modo:

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

E possiamo applicare il livello dei validatori come middleware al nostro schema, insieme a un livello di autorizzazioni come questo:

 // 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 query

Un altro problema da considerare, che si verifica con le API GraphQL ed è spesso trascurato, sono le query N + 1. Questo problema si verifica quando abbiamo una relazione uno-a-molti tra i tipi definiti nel nostro schema. Per dimostrarlo, ad esempio, utilizziamo l'API del libro del nostro progetto di esempio:

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

Qui vediamo che il tipo User ha una relazione uno-a-molti con il tipo Book e questa relazione è rappresentata come campo creatore in Book . La mappa dei risolutori per questo schema è definita in questo modo:

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

Se eseguiamo una query sui libri utilizzando questa API e osserviamo il registro delle istruzioni SQL, vedremo qualcosa del genere:

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

È facile intuire: durante l'esecuzione, il risolutore è stato prima chiamato per la query libri, che ha restituito un elenco di libri e quindi ogni oggetto libro è stato chiamato risolutore campo creatore e questo comportamento ha causato N + 1 query di database. Se non vogliamo far esplodere il nostro database, questo tipo di comportamento non è davvero eccezionale.

Per risolvere il problema delle query N+1, gli sviluppatori di Facebook hanno creato una soluzione molto interessante chiamata DataLoader, che viene descritta nella sua pagina README in questo modo:

"DataLoader è un'utilità generica da utilizzare come parte del livello di recupero dei dati dell'applicazione per fornire un'API semplificata e coerente su varie origini dati remote come database o servizi Web tramite batch e memorizzazione nella cache"

Non è molto semplice capire come funziona DataLoader, quindi vediamo prima l'esempio che risolve il problema illustrato sopra e poi spieghiamo la logica alla base.

Nel nostro progetto di esempio, DataLoader è definito in questo modo per il campo creatore:

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

Una volta definito UserDataLoader, possiamo cambiare il risolutore del campo creatore in questo modo:

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

Dopo le modifiche applicate, se eseguiamo nuovamente la query dei libri e guardiamo il registro delle istruzioni SQL, vedremo qualcosa del genere:

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

Qui, possiamo vedere che le query del database N + 1 sono state ridotte a due query, in cui la prima seleziona l'elenco dei libri e la seconda seleziona l'elenco degli utenti presentati come creatori nell'elenco dei libri. Ora spieghiamo come DataLoader ottiene questo risultato.

La caratteristica principale di DataLoader è il batch. Durante la singola fase di esecuzione, DataLoader raccoglierà tutti gli ID distinti di tutte le singole chiamate alla funzione di caricamento e quindi chiamerà la funzione batch con tutti gli ID richiesti. Una cosa importante da ricordare è che le istanze di DataLoaders non possono essere riutilizzate, una volta chiamata la funzione batch, i valori restituiti verranno memorizzati nella cache per sempre. A causa di questo comportamento, dobbiamo creare una nuova istanza di DataLoader per ogni fase di esecuzione. Per ottenere ciò abbiamo creato una funzione statica getInstance , che controlla se l'istanza di DataLoader è presentata in un oggetto di contesto e, se non trova, ne crea uno. Ricorda che un nuovo oggetto di contesto viene creato per ogni fase di esecuzione ed è condiviso tra tutti i risolutori.

Una funzione di caricamento batch di DataLoader accetta una matrice di ID richiesti distinti e restituisce una promessa che si risolve in una matrice di oggetti corrispondenti. Quando scriviamo una funzione di caricamento batch, dobbiamo ricordare due cose importanti:

  1. L'array di risultati deve avere la stessa lunghezza dell'array di ID richiesti. Ad esempio, se abbiamo richiesto gli ID [1, 2, 3] , l'array di risultati restituito deve contenere esattamente tre oggetti: [{ "id": 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }, { “id”: 3, “fullName”: “user3” }]
  2. Ciascun indice nell'array di risultati deve corrispondere allo stesso indice nell'array di ID richiesti. Ad esempio, se l'array di ID richiesti ha il seguente ordine: [3, 1, 2] , l'array di risultati restituito deve contenere oggetti esattamente nello stesso ordine: [{ "id": 3, “fullName”: “user3” }, { “id”: 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }]

Nel nostro esempio, assicuriamo che l'ordine dei risultati corrisponda all'ordine degli ID richiesti con il codice seguente:

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

Sicurezza

E, ultimo ma non meno importante, voglio citare la sicurezza. Con GraphQL, possiamo creare API molto flessibili e fornire all'utente funzionalità avanzate su come interrogare i dati. Ciò garantisce molto potere al lato client dell'applicazione e, come ha detto lo zio Ben, "Da un grande potere derivano grandi responsabilità". Senza un'adeguata sicurezza, un utente malintenzionato può inviare una query costosa e causare un attacco DoS (Denial of Service) al nostro server.

La prima cosa che possiamo fare per proteggere la nostra API è disabilitare l'introspezione dello schema GraphQL. Per impostazione predefinita, un server API GraphQL espone la capacità di introspezione del suo intero schema, che è generalmente utilizzato da shell visive interattive come GraphiQL e Apollo Playground, ma può anche essere molto utile per un utente malintenzionato costruire una query complessa basata sulla nostra API . Possiamo disabilitarlo impostando il parametro di introspection su false durante la creazione dell'Apollo Server:

 // 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 prossima cosa che possiamo fare per proteggere la nostra API è limitare la profondità della query. Ciò è particolarmente importante se abbiamo una relazione ciclica tra i nostri tipi di dati. Ad esempio, nel nostro esempio, il tipo di progetto Author ha libri di campo e il tipo Book ha autori di campo. Questa è chiaramente una relazione ciclica e nulla impedisce a un utente malintenzionato di scrivere una query come questa:

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

È chiaro che con una nidificazione sufficiente, una query del genere può facilmente far esplodere il nostro server. Per limitare la profondità delle query, possiamo usare una libreria chiamata graphql-depth-limit. Una volta installato, possiamo applicare una restrizione di profondità durante la creazione di Apollo Server, in questo modo:

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

Qui, abbiamo limitato a cinque la profondità massima delle query.

Post Scriptum: il passaggio da REST a GraphQL è interessante

In questo tutorial, ho cercato di dimostrare i problemi comuni che incontrerai quando inizi a implementare le API GraphQL. Tuttavia, alcune parti forniscono esempi di codice molto superficiali e graffiano solo la superficie del problema discusso, a causa delle sue dimensioni. Per questo motivo, per vedere esempi di codice più completi, fare riferimento al repository Git del mio progetto API GraphQL di esempio: graphql-example.

Alla fine, voglio dire che GraphQL è una tecnologia davvero interessante. Sostituirà REST? Nessuno lo sa, forse domani nel mondo in rapida evoluzione dell'IT apparirà un approccio migliore per sviluppare le API, ma GraphQL rientra davvero nella categoria delle tecnologie interessanti che vale sicuramente la pena imparare.