APIs e Microsserviços

4 abr, 2017

GraphQL hoje usando Apollo em aplicações que utilizam APIs REST

Publicidade

Apesar do entusiamo das pessoas que já usam GraphQL, a popularidade da ferramenta está crescendo a passos curtos. Desenvolvedores que trabalham no client-side das aplicações são os que mais 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.

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

Obs: 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!'
  }
}

Obs: 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 artigo, 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 artigo. 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 })

Obs: 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! Agora, 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, 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.

Obs: 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:

Até mais!