Todo o poder emana do cliente: explorando uma API GraphQL

foto por Elena

Esse tal de GraphQL tem causado bastante burburinho. Dizem que é uma alternativa mais flexível e eficiente a APIs REST. Já foi detectado pelo seu radar?

No episódio 55 do Hipsters Ponto Tech, o pessoal da Pipefy disse que uma API GraphQL no back-end, junto ao React e shadow DOM no front-end, levaram a uma melhoria drástica na performance. Outras empresas brasileiras como a GetNinjas e a Taller também tem usado o GraphQL.

Empresas gringas como a Shopify, Artsy e Yelp provêem APIs com suporte ao GraphQL. O GitHub migrou sua API de REST para GraphQL.

E claro, o Facebook, que desenvolveu o GraphQL em 2012 para uso interno e o abriu ao público em 2015.

Quais as limitações de uma API REST?

Para ilustrar o que pode ser melhorado em uma API REST, vamos utilizar a versão 3 da API do GitHub, considerada muito consistente e aderente aos princípios REST.

Queremos uma maneira de avaliar bibliotecas open-source. Para isso, dado um repositório do GitHub, desejamos descobrir:

  • o número de stars
  • o número de pull requests abertos

Como exemplo, vamos usar o repositório de uma biblioteca NodeJS muito usada: o framework Web minimalista Express.

Obtendo detalhes de um repositório

Lendo a documentação da API do GitHub, descobrimos que para obter detalhes sobre um repositório, devemos enviar uma requisição GET para /repos/:owner/:repo. Então, para o repositório do Express, devemos fazer:


GET https://api.github.com/repos/expressjs/express

Como resposta, obtemos:

  • 2.2 KB gzipados transferidos, incluindo cabeçalhos
  • 6.1 KB de JSON em 110 linhas, quando descompactado

200 OK
Content-type: application/json; charset=utf-8

{
    "id": 237159,
    "name": "express",
    "full_name": "expressjs/express",
    "private": false,
    "html_url": "https://github.com/expressjs/express",
    "description": "Fast, unopinionated, minimalist web framework for node.",
    "fork": false,
    "issues_url": "https://api.github.com/repos/expressjs/express/issues{/number}",
    "pulls_url": "https://api.github.com/repos/expressjs/express/pulls{/number}",
    "stargazers_count": 33508,
    ... 
}

O JSON retornado tem diversas informações sobre o repositório do Express. Por meio da propriedade stargazers_count, descobrimos que há mais de 33 mil stars.

Porém, não temos o número de pull requests abertos.

Obtendo os pull requests de um repositório

Na propriedade pulls_url, temos apenas uma URL: https://api.github.com/repos/expressjs/express/pulls{/number}.

Um bom palpite é que sem esse {/number} teremos a lista de todos os pull requests, o que pode ser confirmado na seção de pull requests da documentação da API REST do GitHub.

O {/number} da URL segue o modelo proposto pela RFC 6570 (URI Template).

Mas como filtrar apenas pelos pull requests abertos?

Na mesma documentação, verificamos que podemos usar a URL /repos/:owner/:repo/pulls?state=open ou simplesmente /repos/:owner/:repo/pulls, já que o filtro por pull requests abertos é aplicado por padrão. Em outras palavras, precisamos de outra requisição:


GET https://api.github.com/repos/expressjs/express/pulls

A resposta é:

  • 54.1 KB gzipados transferidos, incluindo cabeçalhos
  • 514 KB de JSON em 9150 linhas, quando descompactado

200 OK
Content-type: application/json; charset=utf-8
Link: <https://api.github.com/repositories/237159/pulls?page=2>; rel="next",
      <https://api.github.com/repositories/237159/pulls?page=2>; rel="last"

[
    {
        //um pull request...
        "url": "https://api.github.com/repos/expressjs/express/pulls/3391",
        "id": 134639441,
        "html_url": "https://github.com/expressjs/express/pull/3391",
        "diff_url": "https://github.com/expressjs/express/pull/3391.diff",
        "patch_url": "https://github.com/expressjs/express/pull/3391.patch",
        "issue_url": "https://api.github.com/repos/expressjs/express/issues/3391",
        "number": 3391,
        "state": "open",
        "locked": false,
        "title": "Update guide to ES6",
        "user": {
            "login": "jevtovich",
            "id": 13847095,
            "avatar_url": "https://avatars3.githubusercontent.com/u/13847095?v=4",
            ...
        },
        "body": "",
        "created_at": "2017-08-08T11:40:32Z",
        "updated_at": "2017-08-08T17:28:01Z",
        ...
    },
    {
        //outro pull request...
        "url": "https://api.github.com/repos/expressjs/express/pulls/3390",
        "id": 134634529,
        ...
    },
    ...
]

É retornado um array de 30 objetos que representam os pull requests. Cada objeto ocupa uma média de 300 linhas, com informações sobre status, descrição, autores, commits e diversas URLs relacionadas.

Disso tudo, só queremos saber a contagem: 30 pull requests. Não precisamos de nenhuma outra informação.

Mas há outra questão: o resultado é paginado com 30 resultados por página, por padrão, conforme descrito na seção de paginação da documentação da API REST do GitHub.

As URLs das próximas páginas devem ser obtidas a partir do cabeçalho de resposta Link, extraindo o rel (link relation) next.

Os links para as próximas páginas seguem o conceito de hipermídia do REST e foram implementados usando o cabeçalho Link e o formato descrito na RFC 5988 (Web Linking). Essa RFC sugere um punhado de link relations padronizados.

Então, a partir do next, seguimos para a próxima página:


GET https://api.github.com/repositories/237159/pulls?page=2

Temos como resposta:

  • 26.9 KB gzipados transferidos, incluindo cabeçalhos
  • 248 KB de JSON em 4394 linhas, quando descompactado

200 OK
Content-type: application/json; charset=utf-8
Link: <https://api.github.com/repositories/237159/pulls?page=1>; rel="first",
      <https://api.github.com/repositories/237159/pulls?page=1>; rel="prev"

[
  {
    //um pull request...
    "url": "https://api.github.com/repos/expressjs/express/pulls/2730",
    "id": 41965836,
    ...
  },
  {
    //outro pull request...
    "url": "https://api.github.com/repos/expressjs/express/pulls/2703",
    "id": 39735937,
     ...
  },
  ...
]

O array retornado contabiliza mais 14 objetos representando os pull requests. Dessa vez, não há o link relation next, indicando que é a última página.

Então, sabemos que há 44 (30 + 14) pull requests abertos no repositório do Express.

Resumindo

No momento da escrita desse artigo, o número de stars do Express no GitHub é 33508 e o de pull requests abertos é 44. Para descobrir isso, tivemos que:

  • disparar 3 requisições ao servidor
  • baixar 83.2 KB de informações gzipadas e cabeçalhos
  • fazer parse de 768.1 KB de JSON ou 13654 linhas

O que daria pra melhorar? Ir menos vezes ao servidor, baixando menos dados!

Não é um problema com o REST em si, mas uma discrepância entre a modelagem atual da API e as nossas necessidades.

Poderíamos pedir para o GitHub implementar um recurso específico que retornasse somente as informações, tudo em apenas um request.

Mas será que o pessoal do GitHub vai nos atender?

Mais flexibilidade e eficiência com GraphQL

Numa API GraphQL, o cliente diz exatamente os dados que quer da API, tornando a requisição muito flexível.

A API, por sua vez, retorna apenas os dados que o cliente pediu, fazendo com que a transferência da resposta seja bastante eficiente.

Mas afinal de contas, o que é GraphQL?

GraphQL não é um banco de dados, não é um substituto do SQL, não é uma ferramenta do lado do servidor e não é específico para React (apesar de muito usado por essa comunidade).

Um servidor que aceita requisições GraphQL poderia ser implementado em qualquer linguagem usando qualquer banco de dados. Há várias bibliotecas de diferentes plataformas que ajudam a implementar esse servidor.

Clientes que enviam requisições GraphQL também poderiam ser implementados em qualquer tecnologia: web, mobile, desktop, etc. Diversas bibliotecas auxiliam nessa tarefa.

GraphQL é uma query language para APIs que foi especificada pelo Facebook.

A query language do GraphQL é fortemente tipada e descreve, através de um schema, o modelo de dados oferecido pelo serviço. Esse schema pode ser usado para verificar se uma dada requisição é válida e, caso seja, executar as tarefas no back-end e estruturar os dados da resposta.

Um cliente pode enviar 3 tipos de requisições GraphQL, os root types:

Montando uma consulta GraphQL

A versão 4 da API do GitHub, a mais recente, dá suporte a requisições GraphQL.

Para fazer nossa consulta às stars e aos pull requests abertos do repositório do Express usando a API GraphQL do GitHub, devemos começar com a query:


query {
}

Vamos usar o campo repository da query, que recebe os argumentos owner e name, ambos obrigatórios e do tipo String. Para buscar pelo Express, devemos fazer:


query {
  repository (owner: "expressjs", name: "express") {
  }
}

A partir do objeto repository, podemos descobrir o número de stars por meio do campo stargazers. Como queremos apenas a quantidade de itens, só precisamos obter propriedade totalCount dessa connection.


query {
  repository (owner: "expressjs", name: "express") {
    stargazers {
      totalCount
    }
  }
}

Para encontrarmos o número de pull requests abertos, basta usarmos o campo pullRequests do repository. O filtro por pull requests abertos não é aplicado por padrão. Por isso, usaremos o argumento states. Da connection, obteremos apenas o totalCount.


query { 
  repository(owner: "expressjs", name: "express") {
    stargazers {
      totalCount
    }
    pullRequests(states: OPEN) {
      totalCount
    }
  } 
}

Basicamente, é essa a nossa consulta! Bacana, não?

Uma maneira de “rascunhar” consultas GraphQL é usar a ferramenta GraphiQL, que permite explorar APIs pelo navegador. Há até code completion! Boa parte das APIs GraphQL dá suporte, incluindo a do GitHub.

Tá, mas como enviar a consulta para a API?

A maneira mais comum de publicar APIs GraphQL é usar a boa e velha Web, com seu protocolo HTTP.

Apesar do HTTP ser o mais usado para publicar APIs GraphQL, teoricamente não há limitações em usar outros protocolos.

Uma API GraphQL possui apenas um endpoint e, consequentemente, só uma URL.

É possível enviar requisições GraphQL usando o método GET do HTTP, com a consulta como um parâmetro na URL. Porém, como as consultas são relativamente grandes e requisições GET tem um limite de tamanho, o método mais utilizado pelas APIs GraphQL é o POST, com a consulta no corpo da requisição.

No caso do GitHub a URL do endpoint GraphQL é: https://api.github.com/graphql

O GitHub só dá suporte ao método POST e o corpo da requisição deve ser um JSON cuja propriedade query conterá uma String com a nossa consulta.

Mesmo para consultas a repositórios públicos, a API GraphQL do GitHub precisa de um token de autorização.


POST https://api.github.com/graphql
Content-type: application/json
Authorization: bearer f023615deb415e...

{
"query":    "query {
                repository(owner: \"expressjs\", name: \"express\") { 
                    stargazers {
                        totalCount
                    } 
                    pullRequests(states: OPEN) {
                        totalCount
                    }
                }
            }"
}

O retorno será um JSON em que os dados estarão na propriedade data:


{
    "data": {
        "repository": {
            "stargazers": {
                "totalCount": 33508
            },
            "pullRequests": {
                "totalCount": 44
            }
        }
    }
}

Na verdade, os JSONs de requisição e resposta ficam em apenas 1 linha. Formatamos o código anterior em várias linhas para melhor legibilidade.

Repare que os campos da consulta, dentro da query, tem exatamente a mesma estrutura do retorno da API. É como se a resposta fosse a própria consulta, mas com os valores preenchidos. Por isso, montar consultas com GraphQL é razoavelmente intuitivo.

Resumindo

Obtivemos os mesmos resultados: 33508 stars e 44 pull requests. Para isso, tivemos que:

  • disparar apenas 1 requisição ao servidor
  • baixar somente 996 bytes de informações gzipadas, incluindo cabeçalhos
  • fazer parse só de 93 bytes de JSON

São 66,67% requisições a menos, 98,82% menos dados e cabeçalhos trafegados e 99,99% menos JSON a ser “parseado”. Ou seja, MUITO mais rápido.

Considerações finais

Poderíamos buscar outros dados da API do GitHub: o número de issues abertas, a data da última release, informações sobre o último commit, etc.

Uma coisa é certa: com uma consulta GraphQL, eu faria menos requisições e receberia menos dados desnecessários. Mais flexibilidade e mais eficiência.

Porém, existem várias outras questões que surgem ao estudar o GraphQL:

  • como fazer um servidor que atenda a toda essa flexibilidade?
  • é possível gerar uma documentação a partir do código para a minha API?
  • vale a pena migrar minha API pra GraphQL?
  • posso fazer uma “casca” GraphQL para uma API REST já existente?
  • como implementar um cliente sem muito trabalho?
  • quais os pontos ruins dessa tecnologia e desafios na implementação?

E você? O que achou do GraphQL? Já usa? Usaria?

8 Comentários

  1. Rafael Ponte 14/09/2017 at 17:52 #

    Oi Alexandre,

    Excelente artigo, sem dúvida sanou muitas das dúvidas com relação a GraphQL e ainda desmentiu aqueles devs que insistem em dizer que GraphQL veio para substituir bancos relacionais e que iniciantes não deveriam estudar mais SQL pois essa linguagem morreu.

    O bacana é que você deixou no ar os desafios de implementar o lado servidor e até mesmo o lado cliente, se possível, acredito que isso você poderia explanar num post futuro.

    No mais, parabéns pelo artigo, MUITO didático e simples de entender (exemplo matador este do Github).

  2. Alexandre Aquiles 14/09/2017 at 18:08 #

    Fala Rafael! Valeu!

    Nunca ouvi ninguém falando que GraphQL veio substituir BDs relacionais. Que absurdo! [1]

    Uma coisa importante é não entrar nessa guerrinha de que APIs GraphQL vão matar as REST. Mas, para o problema proposto e a API existente do GitHub, realmente trouxe mais flexibilidade e mais eficiência. E é type-safe. É uma baita de uma tecnologia, diga-se de passagem…

    Não queria estender muito o post. Não posso prometer quando, mas a ideia é falar mais sobre o Schema do GraphQL e como implementar um servidor e um cliente. Se possível, em diferentes tecnologias!

    Ah, acabei descobrindo um bug nas APIs REST e GraphQL do GitHub:
    https://platform.github.community/t/number-of-forks-different-in-graphql-x-rest-x-web-ui/3113

    [1] Não podemos nos impressionar pelos nomes (confusos, é verdade) das coisas, né? Precisamos entender os conceitos por trás do nomes! É um grande problema da educação no Brasil, inclusive… Sei que você é do ramo! 😉

  3. Henrique 15/09/2017 at 14:54 #

    Alexandre,

    No seu código que apresenta o corpo da requisição POST em formato JSON, as aspas da query não deveriam ser “escapadas” (valores dos parâmetros owner e name)?

    No mais, obrigado pela didática!

  4. Alexandre Aquiles 15/09/2017 at 19:43 #

    Henrique,

    Excelente observação! Corrigido. Muito obrigado!

  5. Marcio Trindade 17/09/2017 at 22:19 #

    Alexandre parabéns pelo artigo acredito que com o tempo as pessoas vão acabar se encantando com GraphQl assim como nós aqui na Pipey.

    Sobre as dúvidas acredito que já devem estar preparando um próximo artigo pra responder estas perguntas, mas já dando um spoiler. Como o GraphQl é fortemente tipado e possui um schema bem definido o programador não precisa mais fazer muitos tratamentos que normalmente é feito nas API rest como validar tipo, validar parâmetros obrigatórios que não foram enviados ou se tem parâmetros extras que não são esperados.

    Sobre documentação o GraphiQl, que é a interface comentada, tem uma boa documentação e depois que as pessoas entendem como ele funciona é muito prático mas pra serviços esternos ainda não encontrei nada focado 100% em documentação de API GraphQl então estamos utilizando serviços convencionais de API rest que funciona da mesma forma.

    Abraços

  6. Alexandre Aquiles 18/09/2017 at 13:53 #

    Bacana Marcio,

    Ótimas considerações!

  7. Raphael lacerda 21/09/2017 at 22:07 #

    Tive que ler 2x pra realmente entender que diabo eh isso.

    Mas então. ..

    Elasticsearch pra fazer pesquisa é isso aí! Hehehe

    Eu entendi que realmente o frontend fica melhor, mas como fazer pra resolver isso no backend?

    Teria que extrair parâmetros e montar queries dinâmicas neh? Sem meio que saber quais tabelas vcs teria que relacionar

    No REST, para um determinado uri, vc sabe exatamente quais tabelas tem que relacionar

  8. Alexandre Aquiles 22/09/2017 at 07:34 #

    E aí, Rapha!

    Putz, vi aqui um exemplo do Elasticsearch e a Query DSL lembra um pouco o GraphQL mesmo… Mas acho que a abordagem é diferente.

    No GraphQL, a implementação do lado do servidor é bem funcional: cada objeto (repository, stargazers, pullRequests) é uma função ou, nos termos do GraphQL, um resolver.

    Mas isso fica pra outro post!

Deixe uma resposta