Criando sua primeira API GraphQL

Publicados: 2022-03-11

Prefácio

Há alguns anos, o Facebook introduziu uma nova maneira de construir APIs de back-end chamada GraphQL, que basicamente é uma linguagem específica de domínio para consulta e manipulação de dados. No início, não prestei muita atenção a isso, mas eventualmente me vi envolvido com um projeto na Toptal, onde tive que implementar APIs de back-end baseadas em GraphQL. Foi quando fui em frente e aprendi como aplicar o conhecimento que aprendi para REST ao GraphQL.

Foi uma experiência muito interessante e, durante o período de implementação, tive que repensar as abordagens e metodologias padrão usadas nas APIs REST de maneira mais amigável ao GraphQL. Neste artigo, tento resumir problemas comuns a serem considerados ao implementar APIs GraphQL pela primeira vez.

Bibliotecas necessárias

O GraphQL foi desenvolvido internamente pelo Facebook e lançado publicamente em 2015. Mais tarde, em 2018, o projeto GraphQL foi movido do Facebook para a recém-criada GraphQL Foundation, hospedada pela Linux Foundation sem fins lucrativos, que mantém e desenvolve a especificação da linguagem de consulta GraphQL e uma referência implementação para JavaScript.

Como o GraphQL ainda é uma tecnologia jovem e a implementação de referência inicial estava disponível para JavaScript, a maioria das bibliotecas maduras para ele existem no ecossistema Node.js. Há também duas outras empresas, Apollo e Prisma, que fornecem ferramentas e bibliotecas de código aberto para GraphQL. O projeto de exemplo neste artigo será baseado em uma implementação de referência do GraphQL para JavaScript e bibliotecas fornecidas por essas duas empresas:

  • Graphql-js – Uma implementação de referência do GraphQL para JavaScript
  • Apollo-server – servidor GraphQL para Express, Connect, Hapi, Koa e muito mais
  • Apollo-graphql-tools – Construir, simular e costurar um esquema GraphQL usando o SDL
  • Prisma-graphql-middleware – Divida seus resolvedores GraphQL em funções de middleware

No mundo GraphQL, você descreve suas APIs usando esquemas GraphQL e, para eles, a especificação define sua própria linguagem chamada The GraphQL Schema Definition Language (SDL). O SDL é muito simples e intuitivo de usar, ao mesmo tempo em que é extremamente poderoso e expressivo.

Existem duas maneiras de criar esquemas GraphQL: a abordagem de primeiro código e a abordagem de primeiro esquema.

  • Na abordagem code-first, você descreve seus esquemas GraphQL como objetos JavaScript baseados na biblioteca graphql-js e o SDL é gerado automaticamente a partir do código-fonte.
  • Na abordagem schema-first, você descreve seus esquemas GraphQL em SDL e conecta sua lógica de negócios usando a biblioteca Apollo graphql-tools.

Pessoalmente, prefiro a abordagem de esquema em primeiro lugar e a usarei para o projeto de exemplo neste artigo. Implementaremos um exemplo clássico de livraria e criaremos um back-end que fornecerá APIs CRUD para criar autores e livros, além de APIs para gerenciamento e autenticação de usuários.

Criando um servidor GraphQL básico

Para executar um servidor GraphQL básico, devemos criar um novo projeto, inicializá-lo com npm e configurar o Babel. Para configurar o Babel, primeiro instale as bibliotecas necessárias com o seguinte comando:

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

Após instalar o Babel, crie um arquivo com o nome .babelrc no diretório raiz do nosso projeto e copie a seguinte configuração para lá:

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

Edite também o arquivo package.json e adicione o seguinte comando à seção de scripts :

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

Depois de configurar o Babel, instale as bibliotecas GraphQL necessárias com o seguinte comando:

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

Depois de instalar as bibliotecas necessárias, para executar um servidor GraphQL com configuração mínima, copie este snippet de código em nosso arquivo 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 }`); });

Depois disso, podemos executar nosso servidor usando o comando npm run serve , e se navegarmos em um navegador da Web para a URL http://localhost:8080/graphql , o shell visual interativo do GraphQL, chamado Playground, será aberto, onde podemos execute consultas e mutações do GraphQL e veja os dados do resultado.

No mundo do GraphQL, as funções da API são divididas em três conjuntos, chamados de consultas, mutações e assinaturas:

  • As consultas são usadas pelo cliente para solicitar os dados necessários do servidor.
  • As mutações são usadas pelo cliente para criar/atualizar/excluir dados no servidor.
  • As assinaturas são usadas pelo cliente para criar e manter uma conexão em tempo real com o servidor. Isso permite que o cliente obtenha eventos do servidor e aja de acordo.

Em nosso artigo, discutiremos apenas consultas e mutações. Assinaturas são um tópico enorme - elas merecem seu próprio artigo e não são necessárias em todas as implementações de API.

Tipos de dados escalares avançados

Logo depois de jogar com o GraphQL, você descobrirá que o SDL fornece apenas tipos de dados primitivos e tipos de dados escalares avançados, como Date, Time e DateTime, que são uma parte importante de todas as APIs, estão ausentes. Felizmente, temos uma biblioteca que nos ajuda a resolver esse problema, chamada graphql-iso-date. Após instalá-lo, precisaremos definir novos tipos de dados escalares avançados em nosso esquema e conectá-los às implementações fornecidas pela biblioteca:

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

Junto com data e hora, também existem outras implementações de tipos de dados escalares interessantes, que podem ser úteis para você dependendo do seu caso de uso. Por exemplo, um deles é graphql-type-json, que nos dá a capacidade de usar tipagem dinâmica em nosso esquema GraphQL e passar ou retornar objetos JSON não tipados usando nossa API. Também existe a biblioteca graphql-scalar, que nos dá a capacidade de definir escalares personalizados do GraphQL com sanitização/validação/transformação avançada.

Se necessário, você também pode definir seu tipo de dados escalar personalizado e usá-lo em seu esquema, conforme mostrado acima. Isso não é difícil, mas a discussão está fora do escopo deste artigo – se estiver interessado, você pode encontrar informações mais avançadas na documentação do Apollo.

Esquema de Divisão

Após adicionar mais funcionalidades ao seu esquema, ele começará a crescer e entenderemos que é impossível manter todo o conjunto de definições em um arquivo, e precisamos dividi-lo em pequenos pedaços para organizar o código e torná-lo mais escalável para um tamanho maior. Felizmente, a função construtora de esquema makeExecutableSchema , fornecida pela Apollo, também aceita definições de esquema e mapas de resolvedores na forma de uma matriz. Isso nos dá a capacidade de dividir nosso esquema e mapa de resolvedores em partes menores. Isso é exatamente o que fiz no meu projeto de amostra; Eu dividi a API nas seguintes partes:

  • auth.api.graphql – API para autenticação e registro de usuários
  • author.api.graphql – API CRUD para entradas de autor
  • book.api.graphql – API CRUD para entradas de livros
  • root.api.graphql – Raiz do esquema e definições comuns (como tipos escalares avançados)
  • user.api.graphql – API CRUD para gerenciamento de usuários

Durante o esquema de divisão, há uma coisa que devemos considerar. Uma das partes deve ser o esquema raiz e as outras devem estender o esquema raiz. Isso parece complexo, mas na realidade é bem simples. No esquema raiz, consultas e mutações são definidas assim:

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

E nos outros, eles são definidos assim:

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

E isso é tudo.

Autenticação e autorização

Na maioria das implementações de API, há um requisito para restringir o acesso global e fornecer algum tipo de política de acesso baseada em regras. Para isso, temos que introduzir em nosso código: Autenticação —para confirmar a identidade do usuário—e Autorização , para impor políticas de acesso baseadas em regras.

No mundo GraphQL, como no mundo REST, geralmente para autenticação usamos JSON Web Token. Para validar o token JWT passado, precisamos interceptar todas as solicitações recebidas e verificar o cabeçalho de autorização nelas. Para isso, durante a criação do servidor Apollo, podemos registrar uma função como um gancho de contexto, que será chamado com a solicitação atual que cria o contexto compartilhado entre todos os resolvedores. Isso pode ser feito assim:

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

Aqui, se o usuário passar um token JWT correto, nós o verificamos e armazenamos o objeto do usuário no contexto, que ficará acessível para todos os resolvedores durante a execução da solicitação.

Verificamos a identidade do usuário, mas nossa API ainda é acessível globalmente e nada impede que nossos usuários a chamem sem autorização. Uma maneira de evitar isso é verificar o objeto do usuário no contexto diretamente em cada resolvedor, mas essa é uma abordagem muito propensa a erros porque temos que escrever muito código clichê e podemos esquecer de adicionar a verificação ao adicionar um novo resolvedor . Se dermos uma olhada nas estruturas da API REST, geralmente esses tipos de problemas são resolvidos usando interceptores de solicitação HTTP, mas no caso do GraphQL, não faz sentido porque uma solicitação HTTP pode conter várias consultas GraphQL e, se ainda adicionarmos isso, temos acesso apenas à representação de string bruta da consulta e temos que analisá-la manualmente, o que definitivamente não é uma boa abordagem. Esse conceito não se traduz bem de REST para GraphQL.

Portanto, precisamos de algum tipo de maneira de interceptar consultas GraphQL, e essa maneira é chamada de prisma-graphql-middleware. Essa biblioteca nos permite executar código arbitrário antes ou depois de um resolvedor ser invocado. Ele melhora nossa estrutura de código, permitindo a reutilização de código e uma separação clara de interesses.

A comunidade GraphQL já criou vários middlewares incríveis baseados na biblioteca de middleware Prisma, que resolve alguns casos de uso específicos, e para autorização do usuário, existe uma biblioteca chamada graphql-shield, que nos ajuda a criar uma camada de permissão para nossa API.

Depois de instalar o graphql-shield, podemos introduzir uma camada de permissão para nossa API assim:

 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 podemos aplicar essa camada como middleware ao nosso esquema assim:

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

Aqui, ao criar um objeto de escudo, definimos allowExternalErrors como true, porque, por padrão, o comportamento do escudo é capturar e lidar com erros que ocorrem dentro dos resolvedores, e isso não era um comportamento aceitável para meu aplicativo de exemplo.

No exemplo acima, apenas restringimos o acesso à nossa API para usuários autenticados, mas o shield é bem flexível e, usando-o, podemos implementar um esquema de autorização muito rico para nossos usuários. Por exemplo, em nosso aplicativo de exemplo, temos duas funções: USER e USER_MANAGER e somente usuários com a função USER_MANAGER podem chamar a funcionalidade de administração de usuários. Isso é implementado assim:

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

Mais uma coisa que quero mencionar é como organizar funções de middleware em nosso projeto. Assim como nas definições de esquema e mapas de resolvedores, é melhor dividi-los por esquema e mantê-los em arquivos separados, mas ao contrário do servidor Apollo, que aceita matrizes de definições de esquema e mapas de resolvedores e os une para nós, a biblioteca de middleware Prisma não faz isso e aceita apenas um objeto de mapa de middleware, portanto, se os dividirmos, teremos que costurá-los manualmente. Para ver minha solução para esse problema, consulte a classe ApiExplorer no projeto de exemplo.

Validação

O GraphQL SDL fornece uma funcionalidade muito limitada para validar a entrada do usuário; só podemos definir qual campo é obrigatório e qual é opcional. Quaisquer outros requisitos de validação, devemos implementar manualmente. Podemos aplicar regras de validação diretamente nas funções do resolvedor, mas essa funcionalidade realmente não pertence aqui, e este é outro ótimo caso de uso para middlewares GraphQL do usuário. Por exemplo, vamos usar os dados de entrada de solicitação de inscrição do usuário, onde temos que validar se o nome de usuário é um endereço de e-mail correto, se as entradas de senha correspondem e a senha é forte o suficiente. Isso pode ser implementado assim:

 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 podemos aplicar a camada de validadores como middleware ao nosso esquema, junto com uma camada de permissões como esta:

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

Outro problema a ser considerado, que acontece com as APIs do GraphQL e muitas vezes é ignorado, são as consultas N + 1. Esse problema acontece quando temos um relacionamento um-para-muitos entre os tipos definidos em nosso esquema. Para demonstrá-lo, por exemplo, vamos usar a API do livro do nosso projeto de exemplo:

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

Aqui, vemos que o tipo de User tem um relacionamento um-para-muitos com o tipo de Book , e esse relacionamento é representado como campo criador em Book . O mapa de resolvedores para este esquema é definido assim:

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

Se executarmos uma consulta de livros usando esta API e observarmos o log de instruções SQL, veremos algo assim:

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

É fácil adivinhar - durante a execução, o resolvedor foi chamado primeiro para a consulta de livros, que retornou a lista de livros e, em seguida, cada objeto de livro foi chamado de resolvedor de campo do criador, e esse comportamento causou N + 1 consultas ao banco de dados. Se não quisermos explodir nosso banco de dados, esse tipo de comportamento não é muito bom.

Para resolver o problema das consultas N+1, os desenvolvedores do Facebook criaram uma solução muito interessante chamada DataLoader, que está descrita em sua página README assim:

“DataLoader é um utilitário genérico para ser usado como parte da camada de busca de dados do seu aplicativo para fornecer uma API simplificada e consistente em várias fontes de dados remotas, como bancos de dados ou serviços da web por meio de lote e armazenamento em cache”

Não é muito simples entender como o DataLoader funciona, então vamos primeiro ver o exemplo que resolve o problema demonstrado acima e depois explicar a lógica por trás dele.

Em nosso projeto de exemplo, DataLoader é definido assim para o campo criador:

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

Depois de definir UserDataLoader, podemos alterar o resolvedor do campo criador assim:

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

Após as alterações aplicadas, se executarmos a consulta de livros novamente e observarmos o log de instruções SQL, veremos algo assim:

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

Aqui, podemos ver que N+1 consultas de banco de dados foram reduzidas a duas consultas, onde a primeira seleciona a lista de livros e a segunda seleciona a lista de usuários apresentados como criadores na lista de livros. Agora vamos explicar como o DataLoader alcança esse resultado.

O principal recurso do DataLoader é o batching. Durante a fase de execução única, o DataLoader coletará todos os IDs distintos de todas as chamadas de função de carregamento individuais e, em seguida, chamará a função de lote com todos os IDs solicitados. Uma coisa importante a lembrar é que as instâncias do DataLoaders não podem ser reutilizadas, uma vez que a função batch é chamada, os valores retornados serão armazenados em cache na instância para sempre. Devido a esse comportamento, devemos criar uma nova instância do DataLoader a cada fase de execução. Para isso criamos uma função estática getInstance , que verifica se a instância do DataLoader é apresentada em um objeto de contexto e, caso não seja encontrada, cria um. Lembre-se de que um novo objeto de contexto é criado para cada fase de execução e compartilhado entre todos os resolvedores.

Uma função de carregamento em lote do DataLoader aceita uma matriz de IDs solicitados distintos e retorna uma promessa que resolve para uma matriz de objetos correspondentes. Ao escrever uma função de carregamento em lote, devemos lembrar duas coisas importantes:

  1. A matriz de resultados deve ter o mesmo comprimento que a matriz de IDs solicitados. Por exemplo, se solicitamos os IDs [1, 2, 3] , a matriz de resultados retornada deve conter exatamente três objetos: [{ "id": 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }, { “id”: 3, “fullName”: “user3” }]
  2. Cada índice na matriz de resultados deve corresponder ao mesmo índice na matriz de IDs solicitados. Por exemplo, se a matriz de IDs solicitados tiver a seguinte ordem: [3, 1, 2] , a matriz de resultados retornada deverá conter objetos exatamente na mesma ordem: [{ "id": 3, “fullName”: “user3” }, { “id”: 1, “fullName”: “user1” }, { “id”: 2, “fullName”: “user2” }]

Em nosso exemplo, garantimos que a ordem dos resultados corresponda à ordem dos IDs solicitados com o seguinte código:

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

Segurança

E por último, mas não menos importante, quero mencionar a segurança. Com o GraphQL, podemos criar APIs muito flexíveis e fornecer ao usuário recursos avançados sobre como consultar os dados. Isso concede bastante poder ao lado do cliente do aplicativo e, como disse o tio Ben, “com grandes poderes vêm grandes responsabilidades”. Sem a segurança adequada, um usuário mal-intencionado pode enviar uma consulta cara e causar um ataque DoS (Denial of Service) em nosso servidor.

A primeira coisa que podemos fazer para proteger nossa API é desabilitar a introspecção do esquema GraphQL. Por padrão, um servidor de API GraphQL expõe a capacidade de introspecção de todo o seu esquema, que geralmente é usado por shells visuais interativos como GraphiQL e Apollo Playground, mas também pode ser muito útil para um usuário mal-intencionado construir uma consulta complexa com base em nossa API . Podemos desabilitar isso definindo o parâmetro introspection como false ao criar o 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 }`); })

A próxima coisa que podemos fazer para proteger nossa API é limitar a profundidade da consulta. Isso é especialmente importante se tivermos um relacionamento cíclico entre nossos tipos de dados. Por exemplo, em nosso exemplo, o tipo de projeto Author tem livros de campo e o tipo Book tem autores de campo. Esta é claramente uma relação cíclica e nada impede que o usuário mal-intencionado escreva uma consulta como esta:

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

É claro que com aninhamento suficiente, essa consulta pode explodir facilmente nosso servidor. Para limitar a profundidade das consultas, podemos usar uma biblioteca chamada graphql-depth-limit. Uma vez instalado, podemos aplicar uma restrição de profundidade ao criar o Apollo Server, assim:

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

Aqui, limitamos a profundidade máxima de consultas a cinco.

Post Scriptum: Mudar de REST para GraphQL é interessante

Neste tutorial, tentei demonstrar problemas comuns que você encontrará ao começar a implementar as APIs do GraphQL. No entanto, algumas partes dele fornecem exemplos de código muito superficiais e arranham apenas a superfície do problema discutido, devido ao seu tamanho. Por isso, para ver exemplos de código mais completos, consulte o repositório Git do meu projeto de exemplo de API GraphQL: graphql-example.

No final, quero dizer que o GraphQL é uma tecnologia realmente interessante. Ele substituirá o REST? Ninguém sabe, talvez amanhã, no mundo de TI em rápida mudança, apareça uma abordagem melhor para desenvolver APIs, mas o GraphQL realmente se enquadra na categoria de tecnologias interessantes que definitivamente valem a pena aprender.