Além do REST com GraphQL

Construir APIs virou uma das coisas mais comuns no que tange o desenvolvimento web. Não obstante, escolher uma biblioteca ou framework com bom suporte a REST é parte fundamental da tomada de decisão ao iniciar um projeto.

Em tempos de API-First, debater arquitetura, design, e considerar opções deve fazer parte da concepção de um produto (por mais "padrãozinho" que ele possa parecer). E nessa etapa, questionar a adoção do REST pode resultar em impactos positivos, dependendo do contexto e público alvo.

Grafos, árvores e florestas
Grafos! Grafos everywhere! (sitepoint.com)

Uma das alternativas é o GraphQL. E agora, com o hype ao redor da tecnologia um pouco mais frio, dá para falar sobre ela com menos paixão e mais razão.

Antes de ir: O que é REST, mesmo?

Para a comparação ficar mais rica, é fundamental que haja um entendimento sobre o que é REST. Já exploramos o conceito nos dois artigos abaixo:

Ou se você preferir, o artigo na Wikipedia é uma referência bem completa.

O que é GraphQL?

Bom, definitivamente não é mais um framework Javascript. E dizer que é unicamente um query language é uma afirmação incompleta.

Segundo Adriano Lisboa:

Resumidamente, GraphQL é uma especificação open-source de uma linguagem de consulta criada pelo Facebook.

Hoje mantido pela GraphQL Foundation, sob a licença OWFa 1.0, o GraphQL não só é uma linguagem de consulta, como também de manipulação.

É possível fazer uma analogia ao SQL (Linguagem de Consulta Estruturada), que também é uma especificação que possui diferentes implementações, e que realiza consulta e manipulação de dados. Só que ao contrário (das implementações) do SQL, o GraphQL não é um banco de dados em si, como ilustra o How to GraphQL:

GraphQL is often confused with being a database technology. This is a misconception, GraphQL is a query language for APIs - not databases. In that sense it’s database agnostic and effectively can be used in any context where an API is used.

E por ser uma linguagem de consulta para APIs, que ela se encaixa como uma alternativa ao REST.

Qual a sua principal vantagem em relação ao REST?

Um dos problemas fundamentais do REST é a flexibilidade do dado sendo transmitido. Quando diferentes clientes, com diferentes necessidades, começam a consumir a sua API, é possível que você enfrente um fenômeno conhecido por over-fetching e under-fetching:

  • over-fetching: É quando você está recebendo dados demais, não necessários para o seu propósito, em sua requisição;
  • under-fetching: O oposto. Quando você não recebe dados o suficiente, e precisa realizar outras chamadas para cumprir o seu propósito.

Diagrama com três diferentes chamadas REST para os endpoints de perfil do usuário, artigos, e seguidores
Imagine uma aplicação com a necessidade de exibir o perfil do usuário, seus artigos e seguidores, em uma mesma visualização (howtographql.com)

Já com a flexibilidade do GraphQL, é possível obter toda a informação necessária através de uma consulta, como ilustrado no diagrama abaixo:

Diagrama com apenas uma requisição HTTP, utilizando GraphQL, e consultando por perfil do usuário, seus artigos e seguidores
Através de uma requisição HTTP, o cliente é capaz de obter os dados na medida certa (howtographql.com)

Vale lembrar que existe complementos ao REST que possibilitam uma maior flexibilidade do payload (como por exemplo, passar quais campos você quer coletar através de query string), ainda assim é necessário salientar a legibilidade e até mesmo elegância da consulta realizada no exemplo acima.

Leia sobre outras vantagens do GraphQL em comparação ao REST.

Qual a sua principal desvantagem em relação ao REST?

"Cada escolha uma renúncia", já dizia o poeta.

O GraphQL não é bala de prata, e possui desvantagens quando comparado ao REST. Essa thread no Stackoverflow lista algumas delas.

Talvez a mais popular seja a ausência de cache built-in. Com REST, por utilizarmos o protocolo HTTP, os mecanismos para caching já estão lá. Dos cabeçalhos utilizados para indicar caching à compreensão dos clientes. Diferentes recursos podem ter estratégias de caching diferentes, uma vez que eles são servidos em diferentes endpoints.

Com o GraphQL utilizamos apenas um endpoint. Portanto, precisamos implementar uma camada extra de caching. Nada impossível de ser alcançado, mas ainda assim, uma complexidade a mais.

Veja mais desvantagens em relação ao REST.

Falando GraphQL

Possivelmente a melhor forma de adicionar o GraphQL ao seu projeto é através de plataformas como o Apollo, ou com a utilização de bibliotecas, como é o caso (para servers em Django) da graphene-django e (para clients em Javascript) da Relay.

Diagrama exibindo uma arquitetura com Serverless e Apollo Platform
Exemplo de arquitetura GraphQL e Serverless com Apollo-Server-Lambda (serverless.com)

Por tratar-se de uma especificação, implementações existirão para diferentes linguagens, arquiteturas, e fins (o próprio Gatsby é um bom exemplo).

Não vamos focar em como desenvolver sua API com GraphQL, e sim em como utilizar a query language. Esse repositório no Github disponibiliza algumas APIs públicas na qual podemos utilizar para exercitar esse skill. A GraphQL Pokémon parece uma boa candidata para este momento do artigo.

Tipos

Partimos do princípio que queremos definir os tipos que serão servidos pela nossa API. O GraphQL possui uma sintaxe específica para definição de tipos chamada Schema Definition Language (SDL). No caso do exemplo do Pokémon, temos o seguinte:

type Pokemon {
  id: ID!
  number: String
  name: String
  weight: PokemonDimension
  height: PokemonDimension
  classification: String
  types: [String]
  resistant: [String]
  attacks: PokemonAttack
  weaknesses: [String]
  fleeRate: Float
  maxCP: Int
  evolutions: [Pokemon]
  evolutionRequirements: PokemonEvolutionRequirement
  maxHP: Int
  image: String
}

Veja o schema na íntegra.

Para quem já teve o mínimo contato com Swagger, a sintaxe acima não é estranha. A estrutura consiste em basicamente field: type. Onde em field damos o nome do campo, e type descrevemos o tipo.

Além dos tipos básicos (como String, ID, Int), temos estruturas mais complexas, como as listas [String] e [Pokemon]. PokemonDimension, PokemonAttack e PokemonEvolutionRequirement, são tipos customizados criados com a mesma sintaxe usada no próprio Pokemon. Por exemplo, na linha evolutions: [Pokemon], está expresso o relacionamento de um Pokemon para muitas evoluções (que tratam-se de outras "instâncias" do tipo Pokemon).

Por fim, vale notar o ! em ID, que anota o campo como required.

Consulta

Todas as queries abaixo podem ser reproduzidas na interface Pokémon GraphiQL. Ou ainda, se você preferir, através de API calls.

Para aquecer, vamos visualizar o schema:

{
  __schema {
    types {
      name
    }
  }
}

Ou utilizando o curl:

curl 'https://graphql-pokemon.now.sh/?' \
  -H 'content-type: application/json' \
  --data-binary '{"query":"{ __schema { types{ name } } }", "variables":null, "operationName":null}'

Como resultado teremos o seguinte:

{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Query"
        },
        {
          "name": "Int"
        },
        {
          "name": "Pokemon"
        },
        {
          "name": "ID"
        },
        {
          "name": "String"
        },
        {
          "name": "PokemonDimension"
        },
        {
          "name": "PokemonAttack"
        },
        {
          "name": "Attack"
        },
        {
          "name": "Float"
        },
        {
          "name": "PokemonEvolutionRequirement"
        },
        {
          "name": "__Schema"
        },
        {
          "name": "__Type"
        },
        {
          "name": "__TypeKind"
        },
        {
          "name": "Boolean"
        },
        {
          "name": "__Field"
        },
        {
          "name": "__InputValue"
        },
        {
          "name": "__EnumValue"
        },
        {
          "name": "__Directive"
        },
        {
          "name": "__DirectiveLocation"
        }
      ]
    }
  }
}

Através do __schema descobrimos quais tipos são utilizados pelo servidor. Não muito útil para o propósito desse artigo, mas ainda assim, uma boa forma de olharmos pela primeira vez para uma resposta. Além do fato dela ser em JSON, repare o "envelopamento" do payload através da chave data.

Para compreender os tipos listados acima, podemos utilizar outra ferramenta de introspecção, chamada __type:

{
  __type(name: "ID") {
    name
    kind
  }
}

E como resposta, temos a descrição do tipo ID como SCALAR:

{
  "data": {
    "__type": {
      "name": "ID",
      "kind": "SCALAR"
    }
  }
}

Vamos listar todos os 151 Pokémons originais:

{
  pokemons(first: 151) {
    name
  }
}

A resposta será algo semelhante com o JSON abaixo:

{
  "data": {
    "pokemons": [
      {
        "name": "Bulbasaur"
      },

      (...)

      {
        "name": "Mew"
      }
    ]
  }
}

Com o parâmetro first, estamos solicitando todos os n primeiros resultados. Essa query é declarada na linha 88 do schema.graphql:

type Query {
  query: Query
  pokemons(first: Int!): [Pokemon]
  pokemon(id: String, name: String): Pokemon
}

Com o type Query estamos expressando quais serão as consultas que poderemos executar. Em pokemons, o atributo first é do tipo inteiro e obrigatório. A sua resposta será uma lista de instâncias do tipo Pokemon. É possível também requisitar um Pokémon através do seu id ou name, como explícito na instrução pokemon, dentro do bloco Query:

{
  pokemon(name: "Pikachu") {
    id
    number
    name
    weight {
      maximum
      minimum
    }
    height {
      maximum
      minimum
    }
    classification
    types
    resistant
    attacks {
      fast {
        name
        type
        damage
      }
      special {
        name
        type
        damage
      }
    }
    weaknesses
    fleeRate
    maxCP
    evolutions {
      id
      number
      name
    }
    evolutionRequirements {
      amount
      name
    }
    maxHP
    image
  }
}

E como resultado teremos todos os dados do nosso queridíssimo Pikachu.

Mutações

Além de requisitar informação, podemos também manusear dados. Isso é realizado através do conceito de Mutations.

Para esse exemplo vamos utilizar a GraphQL Jobs API, acessível através dessa interface.

Vamos dar subscribe na API, para que assim possamos receber propostas de empregos que envolvam GraphQL:

mutation {
  subscribe(input: { name: "<seu-nome>", email: "<seu-email>" }) {
    id
    name
    email
  }
}

Como resposta, temos um JSON com id, name e email:

{
  "data": {
    "subscribe": {
      "id": "ckc3r7uqs004w0725u5sf0g1o",
      "name": "John Doe",
      "email": "myamazingemail@mail.com"
    }
  }
}

Ok... Aqui pode ter ficado um pouco confuso. Vamos verificar quais Mutations temos disponíveis:

{
  __schema {
    mutationType {
      fields {
        name
        args {
          name
          type {
            kind
            ofType {
              name
              kind
              inputFields {
                name
                type {
                  ofType {
                    name
                  }
                }
              }
            }
          }
        }
        type {
          kind
          ofType {
            name
            fields {
              name
              type {
                kind
                ofType {
                  name
                }
              }
            }
          }
        }
      }
    }
  }
}

O resultado, de forma resumida, será o seguinte:

{
  "data": {
    "__schema": {
      "mutationType": {
        "fields": [
          {
            "name": "subscribe",
            "args": [
              {
                "name": "input",
                "type": {
                  "kind": "NON_NULL",
                  "ofType": {
                    "name": "SubscribeInput",
                    "kind": "INPUT_OBJECT",
                    "inputFields": [
                      {
                        "name": "name",
                        "type": {
                          "ofType": {
                            "name": "String"
                          }
                        }
                      },
                      {
                        "name": "email",
                        "type": {
                          "ofType": {
                            "name": "String"
                          }
                        }
                      }
                    ]
                  }
                }
              }
            ],
            "type": {
              "kind": "NON_NULL",
              "ofType": {
                "name": "User",
                "fields": [
                  {
                    "name": "id",
                    "type": {
                      "kind": "NON_NULL",
                      "ofType": {
                        "name": "ID"
                      }
                    }
                  },
                  {
                    "name": "name",
                    "type": {
                      "kind": "SCALAR",
                      "ofType": null
                    }
                  },
                  {
                    "name": "email",
                    "type": {
                      "kind": "NON_NULL",
                      "ofType": {
                        "name": "String"
                      }
                    }
                  },
                  {
                    "name": "subscribe",
                    "type": {
                      "kind": "NON_NULL",
                      "ofType": {
                        "name": "Boolean"
                      }
                    }
                  },
                  {
                    "name": "createdAt",
                    "type": {
                      "kind": "NON_NULL",
                      "ofType": {
                        "name": "DateTime"
                      }
                    }
                  },
                  {
                    "name": "updatedAt",
                    "type": {
                      "kind": "NON_NULL",
                      "ofType": {
                        "name": "DateTime"
                      }
                    }
                  }
                ]
              }
            }
          }
        ]
      }
    }
  }
}

A query ficou um pouco mais clara agora. A Mutation subscribe aceita um parâmetro com nome input, do tipo SubscribeInput. Esse, além de ser obrigatório (NON_NULL), possui dois campos: name e email. Isso explica a linha subscribe(input: { name: "<seu-nome>", email: "<seu-email>" }) {, na consulta anterior.

subscribe retorna um tipo User, também obrigatório. Esse tipo possui os campos id, name, subscribe, createdAt, e updatedAt. Como especificamos dentro do bloco subscribe os campos id, name, e email, esse é o resultado que obtemos no JSON de resposta. Que representa a subscription sendo efetuada, e meus dados como usuário sendo retornados.

Por baixo dos panos

Existem outros conceitos não explorados nesse artigo, como por exemplo as Subscriptions. O que pode estar confuso para você é como que o servidor interpreta o GraphQL, e coleta os dados para formar a resposta da requisição.

Imagem do filme Detective Pikachu, mostrando o Pikachu com uma lupa
Será que conseguimos encontrar um emprego de detetive, para Pokémons, utilizando GraphQL? (www.geekgirlauthority.com/)

Comigo o "click" aconteceu depois de checar o código dos repositórios abaixo:

Não é feitiçaria...

Então devo adotar o GraphQL?

Possivelmente. E isso não significa necessariamente abandonar o REST.

A retórica do "GraphQL ser o substituto do REST", inclusive, foi um dos motivos do meu ceticismo em relação ao mesmo por muito tempo.

Uma das análises mais sóbrias que encontrei foi da ThoughtWorks, que categoriza o GraphQL como ASSESS (ou seja, recomendada a exploração para compreender como ele impactará o seu business):

We've seen many successful GraphQL implementations on our projects. We've seen some interesting patterns of use too, including GraphQL for server-side resource aggregation. That said, we've concerns about misuse of this framework and some of the problems that can occur. Examples include performance gotchas around N+1 queries and lots of boilerplate code needed when adding new models, leading to complexity. There are workarounds to these gotchas such as query caching. Even though it's not a silver bullet, we still think it's worth assessing as part of your architecture.

Se o seu produto compartilha do mesmo contexto do Facebook, de ter controle total do backend e do client (web e mobile), utilizar o GraphQL faz muito sentido. Ou ainda, se a sua motivação for de reduzir o número de chamadas HTTP, assim como foi a do Github, é um argumento completamente válido para a adoção da linguagem.

O que talvez precise ficar claro na sua tomada de decisão é que um não substitui o outro completamente, como afirma Mike Stowe, no ProgrammableWeb:

However, that does not mean that in GitHub’s case, pushing out a public GraphQL API was the right choice. In an effort to reduce calls, they are giving up the very layers of flexibility that I believe will drive future APIs. In essence, they chose a solution to one problem they were facing, but in doing so disregarded solutions for the problems REST was designed to solve (...)

Dependendo da sua stack, disponibilizar os dois formatos não é nenhum sacrifício. Aliás, ter o GraphQL como uma espécie de API Gateway, agregando o resultado de demais endpoints REST parece ser uma tendência.

Topologia de serviços quando adicionado GraphQL como API Gateway
Exemplo de uso de GraphQL como API Gateway, à frente de outras APIs GraphQL e REST (labs.getninjas.com.br)

Considerações finais

Para mim, a linguagem brilhou quando migrei o blog para Gatsby. A flexibilidade e assertividade que ela me proporcionou, mesmo em um ambiente de static site generator, me deixou impressionado e até mesmo apaixonado por toda a ideia por trás da tecnologia.

E esse caso de uso, na minha opinião, é motivação o suficiente para pelo menos compreendermos a linguagem. Se há ceticismo em relação ao uso dela em uma solução que já existe, talvez ele dê uma abrandada quando você experimentá-la em um contexto diferente.

Como o livro "GraphQL or Bust: To Use It Or Not: That is the Question" dá a entender, não tem almoço grátis:

GraphQL has often been sold as a perfect solution for every problem, but the reality is that it meets one requirement better than most other, and if that’s not part of your requirements, it may not be the best solution for your implementation.

A discussão continua nas referências abaixo. Não deixe de conferir!

Até a próxima.

Referências