Back-End

1 out, 2018

GraphQL: REST in peace?

Publicidade

Nossa história começa em fevereiro de 2012, quando alguns engenheiros do Facebook passavam por um grande dilema de arquitetura. Naquele exato momento da história, e após alguns anos de o Facebook tentar o sonho de grande parte dos desenvolvedores, que era escrever um só código para múltiplas plataformas, a empresa enfim iniciava o desenvolvimento de seus aplicativos mobile em código nativo, depois de sofrer muito em busca de performance e estabilidade.

Durante o desenvolvimento dos novos aplicativos, ocorreu o tal dilema de arquitetura: o Facebook não tinha uma API para o News Feed, que até o momento era entregue diretamente em HTML. E depois de avaliarem padrões como REST e FQL tables (Facebook SQL-like API), não encontraram uma forma inteligente de consumir (via API) dados estruturados da mesma forma que armazenavam, ou seja, grafos de objetos.

Pode parecer meio estranho o problema encontrado por eles, mas quem nunca implicou com o fato de precisar reorganizar/reprocessar os dados enviados pela API ou, ao contrário, reorganizar/reprocessar os dados enviados pela aplicação, para ficarem no padrão que você precisa/espera?

Após uma extensa pesquisa, uma equipe de três engenheiros estava determinada a produzir uma ferramenta própria, e foi então que surgiu o SuperGraph, o protótipo do que viria a ser o que conhecemos hoje como GraphQL.

Saindo do REST

Não existe forma melhor de elucidar como o GraphQL funciona sem sair de outra maneira de construção de APIs, então vamos utilizar uma das mais adotadas nos últimos anos, REST (Representational State Transfer), que, diferentemente do GraphQL, é um conceito arquitetural (sem especificações e ferramentas oficiais).

Imagine que temos um site de receitas culinárias, o “Tudo Delicioso” (qualquer semelhança é mera coincidência), e resolvemos fazer um aplicativo para ele. Já temos uma API REST, com os seguintes endpoints:

GET /recipes
POST /recipes
GET /recipes/{id}
GET /authors
GET /authors/{id}

Nosso aplicativo, assim que for aberto, mostrará uma lista das últimas receitas cadastradas, o que já é entregue pelo endpoint de receitas:

GET /recipes
{
  "data": [
    {
      "id": 1,
      "name": "Torta de Legumes",
      "category": "salgado",
      "author": {
        "id": 9,
        "name": "Paulo Panela"
      },
      "ingredients": [
        "500mg de Farinha",
        "Sal",
        "Fermento",
        ...
      ],
      "steps": [
        {
            "description": "Junte a farinha com o sal, fermento..."
        },
        {
            "description": "Misture bem..."
        },
        ...
      ]
    },
    {
      ...
    },
    {
      ...
    }
  ]
}

E aqui temos o nosso primeiro problema: não precisamos dos “steps”, nem dos “ingredients”, para mostrar uma lista de últimas receitas. Como estamos no mundo mobile, dados custam tempo, dinheiro e bateria, por isso precisamos de uma certa liberdade para requisitar à API somente alguns campos, e isso é uma das coisas que o GraphQL resolve.

Vamos implementar o GraphQL em nossa API, criando um único endpoint:

POST /graphql

Diferentemente do REST, não precisamos mais de um endpoint para representar cada recurso, nem utilizar vários verbos HTTP – toda “magia” acontece na especificação do endpoint e no conteúdo do request, ambos escritos utilizando uma linguagem, a GraphQL Query Language.

Além de criar um endpoint, precisamos utilizar uma biblioteca que implemente a especificação do GraphQL, a qual nos ajudará a processar os requests. Existem várias bibliotecas para várias linguagens de programação. Para deixar este artigo o mais genérico possível, não vamos nos aprofundar em uma biblioteca de uma linguagem de programação, e sim nas especificações do GraphQL, por isso, caso queira ver uma lista de bibliotecas indicadas para a sua linguagem, acesse https://graphql.org/code/.

Legal, temos o endpoint e a biblioteca configurados, agora precisamos especificar o que o endpoint “sabe responder”.

Schema: “Ei, cliente, pode me pedir isso que eu sei responder”.

Uma das coisas mais legais do GraphQL é o fato de o endpoint se autodescrever, ou seja, através de uma especificação, é possível ver o que a API sabe fazer.

Para exemplificar, vamos escrever um schema que permitirá listarmos nossas receitas culinárias. O schema é escrito em “GraphQL Schema Language”, uma forma agnóstica de linguagem de programação:

type Query {
  recipes: [Recipe]
}

type Recipe {
  id: ID!
  name: String!
  category: Category!
  author: [Author]!
  ingredients: [String]!
  steps: [Step]!
}

type Author {
  id: ID!
  name: String!
  photo: String
  friends: [Author]
}

type Step {
  description: String!
}

enum Category {
  salgado
  doce
}

No schema, nós definimos tipos de objetos e seus campos. Cada campo possui um tipo, como o campo “author”, que é um array de objetos do tipo “Author”, ou o “photo”, que é uma String.

Existem dois tipos especiais de objetos, Query e Mutation, nos quais vamos nos aprofundar logo em seguida.

Todo request para o GraphQL tem em seu conteúdo um JSON, que contém os seguintes campos:

{
  "query": "...",
  "operationName": "...",
  "variables": { "myVariable": "someValue", ... }
}

Sempre que for citado “conteúdo da nossa query”, será o valor do campo “query” no JSON mostrado. No campo “query”, podem existir N definições de objetos do tipo Query e Mutation (chamadas de operation), mas apenas uma pode ser executada em um request, por isso a presença do campo “operationName”. É possível também usar variáveis, o que será explicado ainda neste artigo.

Query: “Ei, API, preciso disso

Ao definirmos no schema um objeto do tipo Query, estamos deixando explícitos quais os entry points que nossa API permite serem usados nas respostas, ou seja, os objetos iniciais, aqueles que ficam na raiz da resposta.

Para o nosso aplicativo, precisamos listar apenas alguns campos das receitas, então o conteúdo da nossa query será:

{
  recipes {
    id
    name
    category
    author {
      name
      photo
    }
  }
}

Note que “recipes” é nosso objeto raiz (entry point), e a partir dele podemos especificar os campos da receita que queremos na resposta, que seria algo como:

{
  "data": {
    "recipes": [
      {
        "id": "MQ==",
        "name": "Torta de Legumes",
        "category": "salgado",
        "author": {
          "name": "Paulo Panela",
          "photo": "https://photo.co"
        }
      },
      ...
    ]
  }
}

Conseguimos nossa lista de receitas de forma mais enxuta. Agora, como requisitamos uma receita em específico?

Para isso, podemos usar um recurso chamado “Arguments”. Vejamos como ficaria o objeto Query do nosso schema:

type Query {
  recipes(id: ID, byMe: Boolean, byFriends: Boolean): [Recipe]
}

É importante lembrar que, ao definirmos os argumentos, cabe à nossa própria lógica da API fazer a filtragem dos dados. A maioria das bibliotecas de GraphQL apenas irá lhe passar quais argumentos foram utilizados na query.

Para requisitar apenas uma receita, nossa query ficaria assim:

{
  recipes(id: "MQ==") {
    id
    name
    ingredients
    steps
    author {
      name
    }
  }
}

Note que adicionamos alguns campos na resposta e deixamos de pedir outros. Este é um dos melhores recursos do GraphQL, como falamos anteriormente: podemos pedir somente aquilo de que precisamos.

Agora, imagine que em nosso aplicativo teremos uma tela com “Minhas Receitas” e “Receitas de Amigos”. Se notar, no schema já temos dois argumentos que podemos utilizar para isso: “byMe” e “byFriends”. Vejamos como ficará nossa query:

{
  recipesByMe: recipes(byMe: true) {
    ...commonFields
  }
  recipesByFriends: recipes(byFriends: true) {
    ...commonFields
    author {
      name
    }
  }
}

fragment commonFields on Recipe {
  id
  name
}

Aqui, podemos ver outros dois recursos das queries – os “Aliases”, que são “apelidos” para os objetos e campos (ex.: “recipesByMe”), e os “Fragments”, que são fragmentos de query que podem ser reutilizados, superúteis para evitar repetição.

A resposta para a nossa query seria algo como:

{
  "data": {
    "recipesByMe": [
      {
        "id": "MQ==",
        "name": "Torta de Legumes"
      },
      ...
    ],
    "recipesByFriends": [
      {
        "id": "MjA==",
        "name": "Pudim de Chocolate",
        "author": {
          "name": "Roberta Sweet",
        }
      },
      ...
    ],
  }
}

Estamos quase no fim de todas as telas do nosso aplicativo. Agora, vamos para a penúltima, na qual será possível ver os autores cadastrados no site. A tela terá dois campos: o nome, que poderá ser digitado pelo usuário (como um filtro de busca), e “ver os amigos”, que mostrará os amigos de cada autor. Para fazer essa tela, precisaremos mudar um pouco o nosso schema:

type Query {
  recipes(id: ID, byMe: Boolean, byFriends: Boolean): [Recipe]
  authors(name: String, showFriends: Boolean): [Author]
}

Não é uma boa prática colocar campos que são preenchidos pelo usuário diretamente na query, precisamos passá-los como variáveis. Para isso, na definição da query, colocamos o nome das variáveis (com “#8221; antes) e o tipo de cada uma.

Além disso, precisamos que alguns campos sejam incluídos na resposta seguindo uma condição (somente se “ver os amigos” estiver marcado). Esse recurso se chama “Directives”, e possui duas opções: “@include(if: Boolean)”, para incluir um campo caso uma variável seja verdadeira, e seu oposto, o “@skip(if: Boolean)”.

Nossa query ficaria assim:

query ListOfAuthors($name: String, $withFriends: Boolean!) {
  authors(name: $name) {
    id
    name
    photo
    friends @include(if: $withFriends) {
      name
    }
  }
}

Note que agora demos um nome à query (ListOfAuthors), o que é uma boa prática para facilitar o debug da aplicação, mas também necessário caso tenhamos várias queries especificadas no mesmo request.

Como estamos utilizando variáveis, precisamos defini-las (campo “variables” do nosso request):

{
  "name": "João",
  "withFriends": true
}

A resposta seria algo como:

{
  "data": {
    "authors": [
      {
        "id": 1,
        "name": "João da Silva",
        "photo": "https://photo.co",
        "friends": [
            {
              "name": "Paulo Panela"
            },
            {
              "name": "Rodrigo Cardoso"
            }
          ]
        },
        ...
      ]
  }
}

Mutation: “Ei, API, muda isso“.

Ao sairmos do REST para o GraphQL, uma das dúvidas inevitáveis é como enviar dados. Já que tudo é um POST e vimos diversas definições de query, onde se encaixa um cadastro, por exemplo?

Para isso, o GraphQL tem um tipo especial de objeto chamado Mutation. Vamos alterar o nosso schema, declarando a mutation de cadastro de receitas:

input CreateRecipeInput {
  name: String!
  ingredients: [String]!
  steps: [String]!
}

type CreateRecipePayload {
  recipe: Recipe
}

type Mutation {
  createRecipe(recipe: CreateRecipeInput!): CreateRecipePayload
}

Uma boa prática é criar um objeto do tipo input, que agrupa todos os campos de entrada da nossa mutation, deixando mais simples o uso com variáveis. Outra boa prática é ter um objeto que representa a resposta da mutation (payload), o qual poderá ser reutilizado por outras mutations (imagine uma mutation de atualização, o payload seria o mesmo que a de criação).

Para utilizar a mutation que criamos, precisamos enviar no campo “query” o seguinte conteúdo no nosso request:

mutation CreateRecipe($input: CreateRecipeInput!) {
  createRecipe(recipe: $input) {
    recipe {
      id
    }
  }
}

E lembrando que, como estamos utilizando variáveis, precisamos defini-las:

{
  "input": {
    "name": "Bolo de Fubá",
    "ingredients": [
      "Farinha de Fubá",
      ...
    ],
    "steps": [
      "Misture os ingredientes secos",
      ...
    ]
  }
}

A resposta será algo como:

{
  "data": {
    "createRecipe": {
      "recipe": {
        "id": "MTAw"
      }
    }
  }
}

Authorization: “Ei, API, quero ver isso“.

Uma das primeiras dúvidas ao implementarmos uma API inteira em GraphQL é sobre a autorização. Os autores e a documentação do GraphQL são bem enfáticos quanto a isso: a autorização fica na camada de regras de negócio.

Quando uma query ou mutation é processada, a maioria das bibliotecas permite que seja “injetado” um objeto de contexto, no qual poderemos colocar uma instância do usuário logado, por exemplo, e nossa camada de regras de negócio pode utilizar essa instância para verificar a permissão do usuário.

Imagine que um usuário do “Tudo Delicioso” requisitou os detalhes de algumas receitas (especificando os IDs). Em REST, estamos acostumados a rejeitar o request inteiro (HTTP 400-499), mas, no GraphQL, uma prática comum é simplesmente retornar null para aquele objeto. Vejamos a seguir um exemplo:

{
  semPermissao: recipes(id: "MQ==") {
    name
  }
  comPermissao: recipes(id: "Mg==") {
    name
  }
}

A resposta será:

{
  "data": {
    "semPermissao": null,
    "comPermissao": {
      "name": "Torta de Legumes",
    }
  }
}

Dessa forma, o usuário terá uma experiência melhor do aplicativo, já que foi “vetado” somente o conteúdo ao qual ele não tem acesso, não “quebrando” a tela inteira que estava visualizando.

Pagination: “Ei, API, página 2, por favor

Quando nossa API precisa retornar uma lista de itens, uma boa prática é retornar um número limitado, assim utilizamos menos recursos (CPU, memória, rede etc) do nosso back-end e front-end, dando respostas mais rápidas ao usuário.

A implementação de paginação no GraphQL fica a cargo do desenvolvedor, mas existe um padrão recomendado. Vamos usar como base a nossa query de receitas culinárias. Sua versão com paginação ficaria:

{
  recipes(first:2 after:"Mg==") {
    totalCount
    edges {
      cursor
      node {
        name
        category
        author {
          name
          photo
        }
      }
    }
    pageInfo {
      endCursor
      hasNextPage
    }
  }
}

Observação: Não veremos o schema, que precisaria ser alterado também com os campos definidos na query, mas não tem segredo, é parecido com o que já vimos anteriormente.

Vamos à explicação de cada alteração:

  • first e after: Especifica que queremos os N primeiros registros, após o registro com um determinado ID.
  • totalCount: Quantidade total de registros encontrados (por exemplo, 199 receitas).
  • edges: Em um grafo, edges são as arestas que conectam um nó a outro. No nosso exemplo, não têm muita utilidade, mas em casos como relacionamentos (“lista de amigos”), podem ser úteis para trazer dados sobre o relacionamento (a data em que duas pessoas se tornaram amigas, por exemplo)
  • cursor: É um identificador único da aresta. Normalmente, é a posição do item na lista (ex: 21) ou o ID (ex: “Mq==”). No nosso caso, uma possível implementação seria utilizar o ID da receita culinária.
  • node: É o nó ou, no nosso caso, a receita culinária.
  • pageInfo: Um objeto de auxílio à paginação. No exemplo, endCursor seria o último cursor da página (para facilitar pedir a próxima página), e hasNextPage um booleano que nos dirá se existe uma próxima página.

Error Handling: “Ei, cliente, não entendi o que você disse“.

Em alguns momentos, nossa API não conseguirá responder o que o cliente pediu. Para isso, o GraphQL define um padrão de resposta de erro. Um retorno simples seria algo como:

{
  "errors": [
    {
      "message": "Não foi possível excluir o objeto"
    }
  ]
}

Tools

Existe uma enorme gama de ferramentas voltadas para GraphQL, mas uma merece um grande destaque, a GraphiQL. Ela é uma espécie de Postman com autocomplete, syntax highlighting e até mesmo navegação pelos objetos definidos no schema.

A GraphiQL está incluída em várias bibliotecas, mas você pode usá-la de forma separada. Para ver o projeto, acesse https://github.com/graphql/graphiql.

Outros projetos que merecem destaque são:

É o fim do REST?

Depois de ver todas as vantagens do GraphQL, essa pergunta é inevitável. Sinceramente, existem muito mais pontos a favor do GraphQL do que do REST para a maioria dos casos de construção de uma API.

Estamos vendo duas coisas que foram pensadas de forma diferente em seus nascimentos, mas, no fundo, as soluções são parecidas em diversos pontos. Experimente trocar algumas nomenclaturas (ex.: queries por endpoints) e verá que estamos falando basicamente das mesmas ações (ex.: como obter ou modificar um recurso).

É importante deixar claro que nem tudo são flores para o GraphQL. Um dos principais problemas é em relação a Caching. Como o request pode ser ultrapersonalizado, todas as ferramentas de HTTP Caching com as quais estamos acostumados não funcionam para ele. Em contrapartida, várias soluções já foram criadas (ex.: Apollo Engine) e, com um pouquinho mais de trabalho, é possível contornar esse problema.

Após um tempo utilizando o GraphQL, pude sentir que resolveu a maioria dos problemas encontrados no REST, como não ter um padrão de formatos de requisição (pessoalmente, uso o padrão JSON API), passando por não ser autodocumentavel (Swagger, por exemplo, faz esse papel, mas adiciona trabalho), até não ser customizável (existem implementações como “?fields=name,steps,ingredients”, mas não tão profundas como as do GraphQL).

Na minha visão, a transição já está acontecendo e é questão de tempo para o mercado adotar com mais força o GraphQL. O REST ainda estará presente em N casos, como no uso para aplicações com Hypermedia (HATEOAS, que permite um computador navegar pelos recursos assim como um humano navega pela Internet) ou simplesmente o upload de arquivos, já que o conteúdo de um request pode ser o próprio arquivo (diferentemente do GraphQL, que ainda não tem um padrão oficial para isso).

Para finalizar, vale relembrar uma das maiores verdades do mercado: quando temos mais de uma opção, as coisas evoluem melhor.

***

Artigo publicado na revista iMasters, edição #27: https://issuu.com/imasters/docs/imasters_27_issuu