Apesar do entusiamo das pessoas que já usam GraphQL, a popularidade da ferramenta está crescendo a passos curtos. Desenvolvedores trabalhando no client-side das aplicações são os que mais rapidamente têm a ganhar com o GraphQL, mas poucos ainda conseguem justificar o investimento financeiro na migração de um backend em pleno funcionamento servindo uma API REST. O que poucos percebem, porém, é que não é preciso fazer a migração simultaneamente no servidor antes de começar a usar a tecnologia no client-side. A implementação de referência para servidores GraphQL é escrita em JavaScript, roda muito bem em navegadores, e é ainda mais fácil de usar quando combinada com as ferramentas fornecidas pelo Apollo.
Se você prefere Relay, pode ler esse post no blog oficial do GraphQL.
O que é Apollo?
O GraphQL é, fundamentalmente, apenas um protocolo de comunicação, e portanto existem dezenas de projetos em várias linguagens, tanto pra client-side quanto pra server-side. Já o Apollo é um conjunto de ferramentas e produtos criados pelo time de desenvolvimento do Meteor para trabalhar com GraphQl.
Dentre esses projetos, há o graphql-tools, que visa facilitar a criação de schemas executáveis, e o apollo-client, que se auto-determina “O cliente GraphQL totalmente preparado para produção e para qualquer servidor ou framework UI“. Ousado, não?
Resolvendo GraphQL queries no navegador
O primeiro problema a ser resolvido é como executar GraphQL resolvers no client-side. Sinceramente, não é muito difícil. Como mencionei anteriormente, o graphql-js
funciona muito bem no ambiente de um navegador, e basta usá-la como faríamos num servidor Node.
Instalação
Vamos precisar inicialmente de duas ferramentas para construir nosso schema:
yarn add --save graphql graphql-tools
Sentindo falta do NPM no comando acima? Sugiro que você dê uma olhada no Yarn 😉
Construindo o GraphQL Schema
Vamos começar pelo início (!). Construir um schema é simples, usando o graphql-tools. Começamos por definir um schema usando a linguagem de schema do GraphQL, como segue:
const typeDefs = `
type Query {
helloWorld: String!
}
schema {
query: Query
}
`
O que estamos dizendo aqui é que nosso schema tem um único typo, chamado Query, e que esse tipo é o “tipo raiz”. Isso significa que os campos desse tipo são pesquisáveis no primeiro nível do schema – neste caso, o campo helloWorld
, que é resolvido a uma string.
Em seguida definimos os resolvers através de um objeto que serve de mapa de resolução (resolver map) para os campos de cada tipo declarado no schema:
const resolvers = {
Query: {
helloWorld: () => 'Hello!'
}
}
Veja mais informações sobre resolver maps neste guia.
Por fim, combinamos a definição do schema com os resolvers usando o método makeExecutableSchema
, criando assim um schema executável:
import { makeExecutableSchema } from 'graphql-tools'
const schema = makeExecutableSchema({ typeDefs, resolvers })
Para manter a simplicidade, por hora vamos manter todo o código num mesmo arquivo chamado schema.js
que, portanto, conterá o seguinte:
import { makeExecutableSchema } from 'graphql-tools'
const typeDefs = `
type Query {
helloWorld: String!
}
schema {
query: Query
}
`
const resolvers = {
Query: {
helloWorld: () => 'Hello!'
}
}
export const schema = makeExecutableSchema({
typeDefs,
resolvers
})
Há uma menção extensa sobre modularização do schema na documentação do Apollo. Eu mesmo tenho um projeto sobre este assunto, apesar de ele ser ainda bastante inicial: graphql-modules. Durante esse tutorial, porém, vamos manter apenas um arquivo para o schema a fim de simplificar as coisas.
Executing queries
Agora que temos um schema executável, podemos resolver queries usando o graphql-js da seguinte forma:
import { graphql } from 'graphql'
import { schema } from './schema'
const query = '{ helloWorld }'
graphql(schema, query).then(result => {
// Exibe no console:
// {
// data: { helloWorld: "Hello!" }
// }
console.log(result)
})
Perfeito! Conseguimos resolver queries de GraphQL. O código até aqui pode ser empacotado usando webpack ou qualquer outra ferramenta de empacotamento, e então executado no navegador, imprimindo o resultado no console.
Criei um repositório para servir de código de referência para este post. Ele está disponível no GitHub, e já conta com um sistema de empacotamento pré-configurado para facilitar seus testes. Baixe o projeto usando git e acesse a tag 1-hello-world para ver o código até este momento.
Usando REST nos resolvers
Agora que temos uma forma de executar queries de GraphQL no navegador, podemos seguir adiante e adicionar um schema mais realista, com resolvers que realizarão requisições REST.
Para fins de simplificar as coisas, vamos usar uma API REST para testes chamada JSONPlaceholder. Não é preciso instalá-la, está (quase) sempre disponível, e tem um schema básico de um blog, com posts, usuários, comentários, etc; exatamente o que precisamos pra fazer alguns testes com GraphQL.
Primeiro, vamos atualizar nosso schema pra adicionar os novos tipos:
const typeDefs = `
type Post {
id: Int!
title: String
body: String
}
type User {
id: Int!
username: String
email: String
}
type Query {
posts: [Post]
post (id: Int!): Post
users: [User]
user: User
}
schema {
query: Query
}
`
Agora, atualizaremos os resolvers da seguinte forma:
const endpoint = 'https://jsonplaceholder.typicode.com'
const toJSON = res => res.json()
const post = (root, { id }) => fetch(`${endpoint}/posts/${id}`).then(toJSON)
const posts = () => fetch(`${endpoint}/posts`).then(toJSON)
const user = (root, { id }) => fetch(`${endpoint}/users/${id}`).then(toJSON)
const users = () => fetch(`${endpoint}/users`).then(toJSON)
const resolvers = {
Query: {
post,
posts,
user,
users,
},
}
Note que utilizamos a Fetch API, já disponível nos principais navegadores. Se for preciso, você pode instalar o polyfill whatwg-fetch para navegadores antigos.
Agora podemos consultar posts:
import { graphql } from 'graphql'
import { schema } from './schema'
const query = '{ posts { id, title, body } }'
// Exibe no console:
// {
// data: { posts: [...] }
// }
graphql(schema, query).then(console.log)
Checkpoint: 2-rest-resolvers
Ok, isso parece legal. E se quiséssemos retornar apenas um post dessa API? Fácil. Segue uma query pelo post de id igual a 1:
const query = `
{
post (id: 1) {
id
title
body
}
}
`
Agora, analisando o endpoint de posts na API de testes vemos que ela retorna um quarto campo em cada post: o userId
. é chegada a hora para…
Resolvendo relacionamentos
Relacionamentos são a beleza do GraphQL mas, apesar da sua importância, fundamentalmente são apenas campos comuns. Vamos seguir adiante e adicionar o campo author no tipo Post e o campo posts no tipo User, junto dos seus resolvers:
import { makeExecutableSchema } from 'graphql-tools'
const typeDefs = `
type Post {
id: Int!
title: String
body: String
+ author: User
}
type User {
id: Int!
username: String
email: String
+ posts: [Post]
}
type Query {
posts: [Post]
post (id: Int!): Post
users: [User]
user: User
}
schema {
query: Query
}
`
const endpoint = 'https://jsonplaceholder.typicode.com'
const toJSON = res => res.json()
const post = (root, { id }) => fetch(`${endpoint}/posts/${id}`).then(toJSON)
const posts = () => fetch(`${endpoint}/posts`).then(toJSON)
const user = (root, { id }) => fetch(`${endpoint}/users/${id}`).then(toJSON)
const users = () => fetch(`${endpoint}/users`).then(toJSON)
+const author = ({ userId }) => fetch(`${endpoint}/users/${userId}`).then(toJSON)
+const userPosts = ({ id }) => fetch(`${endpoint}/users/${id}/posts`).then(toJSON)
+
const resolvers = {
Query: {
post,
posts,
user,
users,
},
+ Post: {
+ author,
+ },
+ User: {
+ posts: userPosts,
+ }
}
export const schema = makeExecutableSchema({ typeDefs, resolvers })
Refrescando a memória: de uma olhada na documentação das
resolver functions para entender os argumentos que estamos usando no código acima.
Agora as coisas estão ficando interessantes. Agora podemos deixar que o GraphQL faça sua mágica, fazendo coisas como “pegar todos os posts cujo autor é o autor do post 1”
const query = `
{
post (id: 1) {
id
author {
id
posts {
id
title
body
}
}
}
}
`
Ah, isso é fantástico! Uma pausa para o café…
Enquanto isso, outro checkpoint pra você testar: 3-relationship-resolvers.
E agora, mutações!
Mutações no GraphQL são apenas mais resolvers de campos, somente com alguns comportamentos divergentes, como o fato de serem resolvidos em série, e não em paralelo, como as queries. Criar uma mutação addPost
, por exemplo, será nada mais do que criar um resolver que realiza uma requisição POST
, como vemos a seguir:
import { makeExecutableSchema } from 'graphql-tools'
const typeDefs = `
type Post {
id: Int!
title: String
body: String
author: User
}
type User {
id: Int!
username: String
email: String
posts: [Post]
}
type Query {
posts: [Post]
post (id: Int!): Post
users: [User]
user: User
}
+ type Mutation {
+ addPost(title: String!, body: String!, userId: Int!): Post!
+ }
+
schema {
query: Query
+ mutation: Mutation
}
`
const endpoint = 'https://jsonplaceholder.typicode.com'
const toJSON = res => res.json()
const post = (root, { id }) => fetch(`${endpoint}/posts/${id}`).then(toJSON)
const posts = () => fetch(`${endpoint}/posts`).then(toJSON)
const user = (root, { id }) => fetch(`${endpoint}/users/${id}`).then(toJSON)
const users = () => fetch(`${endpoint}/users`).then(toJSON)
const author = ({ userId }) => fetch(`${endpoint}/users/${userId}`).then(toJSON)
const userPosts = ({ id }) => fetch(`${endpoint}/users/${id}/posts`).then(toJSON)
+const addPost = (root, post) => fetch(`${endpoint}/posts`, { method: 'POST', body: post })
+ .then(toJSON).then(({ id }) => ({ id, ...post }))
+
const resolvers = {
Query: {
post,
posts,
user,
users,
},
+ Mutation: {
+ addPost,
+ },
Post: {
author,
},
User: {
posts: userPosts,
}
}
export const schema = makeExecutableSchema({ typeDefs, resolvers })
Um parêntese sobre o código acima: a nossa API de testes até aceita requisições
POST
, mas retorna como resultado apenas o id supostamente gerado. Na verdade, nenhum dado é persistido.
Uma query de mutação, então, deve ser identificada da seguinte forma:
const query = `
mutation {
addPost(userId: 1, title: "Meu post", body: "Meu texto!") {
id
body
title
}
}
`
Mais um checkpoint: 4-mutation-resolvers.
Apollo Client
Ok, entendo que executar queries estáticas se provou fácil, mas nossa aplicação precisará de mais. O próximo passo é integrar o que temos ao Apollo Client.
Instalação
yarn add apollo-client graphql-tag
Criando o client
Para criar um cliente Apollo, precisamos instanciar a classe ApolloClient
. Ela recebe como argumento um objeto que contenha, pelo menos, um network interface – interface de rede – que será utilizado pelo cliente para efetuar as requisições GraphQL. Normalmente, quando numa aplicação com GraphQL em ambos client-side e server-side, criamos um network interface usando o helper createNetworkInterface, que basicamente cria uma interface de rede para realizar requisições POST
contra um backend servido no mesmo domínio da aplicação em execução. Seria algo assim:
import ApolloClient, { createNetworkInterface } from 'apollo-client'
export const client = new ApolloClient({
networkInterface: createNetworkInterface({
uri: 'https://graphql.example.com',
}),
})
E, para executar uma query, faríamos:
const query = gql`
query {
helloWorld
}
`
client.query({ query }).then(console.log)
Se tiver interesse, leia mais sobre a camada de network do Apollo Client.
Aqui, porém, não temos GraphQL no backend, e portanto vamos criar uma interface de rede personalizada para resolver as queries diretamente no navegador, usando o schema e os resolvers criados anteriormente. Não é algo simples, veja só:
import ApolloClient, { printAST } from 'apollo-client'
import { graphql } from 'graphql'
import { schema } from './schema'
export const client = new ApolloClient({
networkInterface: {
query: req => {
const query = printAST(req.query)
const { operationName, variables = {} } = req
return graphql(schema, query, null, null, variables, operationName)
}
}
})
Céus, o que está acontecendo aqui?
Primeiro, instanciamos o ApolloClient passando nosso networkInterface personalizado. Ele consiste de um objeto com o método query disponível. Esse método será chamado toda vez que uma query for ser resolvida. O método recebe um único argumento: um objeto do tipo Request Interface.
Segundo, usamos um método auxiliar disponibilizado pelo próprio apollo-client para processar o objeto de requisição e criar uma query GraphQL válida, em forma de string, similar as que estávamos definindo antes estaticamente.
Terceiro, extraímos outras informações importantes da requisição: operationName
, que é o nome (opcionalmente) dado à operação; e possíveis variables
que seriam fornecidas junto da query.
Por último, executamos a query contra o schema, fornecendo também um root inicial e um contexto (ambos nulos aqui, já que não precisamos deles ainda), as variáveis, e o nome da operação. A maioria dos argumentos aqui é opcional.
Se tiver dúvidas sobre esse último passo, dê uma olhada na documentação oficial sobre execução de queries.
Agora podemos usar nosso client como normalmente faríamos:
import { client } from './client'
import gql from 'graphql-tag'
const query = gql`
query Post ($id: Int!) {
post (id: $id) {
id
title
body
author {
id
username
email
}
}
}
`
client.query({ query, variables: { id: 1 } }).then(console.log)
Último checkpoint: 5-apollo-client.
Conclusão
Isso é tudo. Espero que vocês tenham apreciado nosso devaneio no aprendizado de GraphQl e, sobretudo, espero que vocês agora sejam capazes de começar a usar GraphQL, sem mais desculpas envolvendo o pessoal do backend estar com preguiça de preparar um servidor pra você.
Cena após os créditos:
Se você está realmente só começando com GraphQL talvez você nem saiba como/onde usar esse cliente que acabamos de criar. Peço desculpas. Bom, eu imagino que se você está aqui é provável que já use React, Angular, ou mesmo Vue (se for um desenvolvedor hipster incompreendido). Se for esse o caso, tem algumas bibliotecas que vão te ajudar a seguir em frente, conectando o cliente Apollo ao seu framework favorito:
- https://github.com/apollographql/react-apollo
- https://github.com/apollographql/apollo-angular
- https://github.com/Akryum/vue-apollo
Até mais!