GraphQL: The Good, the Bad and the Ugly

Hubcon - Fevereiro, 2019

Gabriel Prates

Front-end Developer

Rodrigo Brito

Back-end Developer

/gabsprates

@gabsprates

/rodrigo-brito

@RodrigoFBrito

PalcoMP3

O maior plataforma de artistas independentes do Brasil.

+1M Músicas

+100K Artistas

+100M de plays mensais

GraphQL

Dê ao seu cliente o poder de pedir exatamente o que ele precisa

REST

https://api.example/v1/user/{ID}

Exemplo: api.example/v1/user/123

Parâmetros

Operação

Resposta Fixa

{

       id: 123,

       name: "Fulano da Silva",

       email: "fulano@fulano.com.br",

       phone: "(31) 1234-4567",

       city: "Belo Horizonte"

}

GraphQL

Exemplo: api.example/graphql

{

       user(id: 123) {

            name

       }

}

Parâmetros

Operação

O usuário pode fazer várias consultas em uma requisição

Resposta Dinâmica

{

       name: "Fulano da Silva"

}

Schema

type User {

     id: Number
     name: String!
     phone: String!
}

 

type Query {
     user(id: Number!): User
}

Interface / Assinaturas da API

Back-end

Go

Go é uma linguagem compilada e estaticamente tipada criada pelo Google em 2007

SIMPLICIDADE

&

PERFORMANCE

Bibliotecas e abordagens

Início de jornadas exigem escolhas

Qual você escolheria?

1

2

3

Struct Based

Example: github.com/samsarahq/thunder

// Example of Entity.
type User struct {
	FirstName string `graphql:"firstName"`
	LastName  string `graphql:"lastName"`
}

func initSchema(schema *schemabuilder.Schema) {
  object := schema.Object("User", User{})
}
  • Schema definido por mapeamento de estruturas
  • Bonito, porém muito reflection e mágica por trás

Mapping Based

Exemplo: github.com/graphql-go/graphql

// Example of Entity.
fields := graphql.Fields{
    "user": &graphql.Field{
        Type: graphql.String,
	Resolve: func(p graphql.ResolveParams) (interface{}, error) {
	    // fetch user from database
            return user, nil
	},
    },
}

rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields}
  • Altamente verboso
  • Muitos reflections / mágica (interface{})
  • Dificulta mocks / testes

 

* Biblioteca portada do graphql/graphql-js

Schema First (Geração de código)

Exemplo: github.com/99designs/gqlgen

type User {
    lastName: string!
    firstName: string!
}

query {
    user(id: number!): User
}
  • Elegante, sem reflection, sem mágica.

  • Base gerada é otimizada para a sua estrutura

  • Gera os models, interfaces e estrutura base automaticamente

type User struct {
	FirstName string
	LastName  string
}

type Query interface {
	User(ID int) (*User, error)
}

func (resolver) User(ID int) (*User, error) {
	return &User{}, nil
}

schema.graphql

graphql.go

Acabou a mágica?

NÃO

API REST

https://api.example/v1/user/{ID}

Exemplo: api.example/v1/user/123

Parâmetros

Operação

Capturar os dados possui baixo custo (processamento)

API GRAPHQL

{

       user(id: 123) {

            name

       }

       batata(type: DOCE) { url }

}

Parâmetros

Operação

Queries variam por requisição

Como que esses dados são extraídos? Mágica?

?

API GRAPHQL

  • Múltiplas operações por query

  • Parâmetros podem ser enviados na query

  • Validação em tempo de requisição

  • Alto custo de requisição (processamento)

Analisa sintaxe e extraí dados a cada requisição

Solução = Cache

Cache LRU

  • Aplicar LRU (Last Recent Used) para as últimas X queries.

  • 80% dos usuários tendem a usar apenas 20% das features

  • Nem toda biblioteca tem suporte a isso, fique ligado!

Tratamento de Erros

API REST

  • Status 200: Success
  • Status 404: Not Found
  • Status 500: Internal Server Error
  • Status 418: I'm a teapot
  • e por ai vai...

 

Semântica de erros HTTP

GraphQL

{
  viewer {
    name
    url
  }

  repository(
    owner:"iválid",
    name: "fail"
  ) {
    name
  }
}
{
  "data": {
    "viewer": {
      "name": "Rodrigo Brito",
      "url": "https://github.com/rodrigo-brito"
    },
    "repository": null
  },
  "errors": [
    {
      "type": "NOT_FOUND",
      "path": [
        "repository"
      ],
      "message": "Could not resolve..."
    }
  ]
}

Erro com Status Code 200

Performance

API REST

Uma requisição REST retornará todos os campos de um objeto, até mesmo campos depreciados e não requisitados no contexto

GitHub API V3 - REST

Exemplo:  https://api.github.com/users/rodrigo-brito

1,296 bytes sem gzip e cabeçalhos

 

{
    "login": "rodrigo-brito",
    "id": 7620947,
    "node_id": "MDQ6VXNlcjc2MjA5NDc=",
    "bio": "Software Developer at @StudioSol | Gopher",
    "public_repos": 101,
    "public_gists": 19,
    "followers": 129,
    "following": 181,
    "created_at": "2014-05-18T14:12:53Z",
    "updated_at": "2019-01-27T15:21:48Z",
    "avatar_url": "https://avatars0.githubusercontent.com/u/7620947?v=4",
    "gravatar_id": "",
    "url": "https://api.github.com/users/rodrigo-brito",
    "html_url": "https://github.com/rodrigo-brito",
    "followers_url": "https://api.github.com/users/rodrigo-brito/followers",
    "following_url": "https://api.github.com/users/rodrigo-brito/following{/other_user}",
    "gists_url": "https://api.github.com/users/rodrigo-brito/gists{/gist_id}",
    "starred_url": "https://api.github.com/users/rodrigo-brito/starred{/owner}{/repo}",
    "subscriptions_url": "https://api.github.com/users/rodrigo-brito/subscriptions",
    "organizations_url": "https://api.github.com/users/rodrigo-brito/orgs",
    "repos_url": "https://api.github.com/users/rodrigo-brito/repos",
    "events_url": "https://api.github.com/users/rodrigo-brito/events{/privacy}",
    "received_events_url": "https://api.github.com/users/rodrigo-brito/received_events",
    "type": "User",
    "site_admin": false,
    "name": "Rodrigo Brito",
    "company": "Studio Sol",
    "blog": "https://rodrigobrito.net",
    "location": "Belo Horizonte - MG / Brazil",
    "email": null,
    "hireable": true
}

API GraphQL

Uma requisição GraphQL retornará apenas os campos solicitados, podendo ignorar campos depreciados.

Versionamento de APIs

GitHub API V4 - GraphQL

Exemplo:  https://api.github.com/graphql

54 bytes sem gzip e cabeçalhos

 

{
  user(login: "rodrigo-brito") {
    createdAt
  }
}
{
  "data": {
    "user": {
      "createdAt": "2014-05-18T14:12:53Z"
    }
  }
}

Resultado

REST vs GraphQL

1 Milhão de requests*

REST           1.3 GB

GraphQL    54 MB

23 Vezes mais fluxo de rede

23 Vezes mais dados para o usuário transferir

Rede é um dos recursos mais caros

*Considerando caso apresentado anteriormente

REST vs GraphQL no PalcoMP3

*Amostra de 10 milhões de requests (Janeiro, 2019)

REST           650 ms

GraphQL   181 ms

Tempo 72% menor

Considerando 3G / Wifi

Documentação

GraphIQL - Explorer

Vale a pena utilizar GraphQL no back-end?

SIM

Front-end

Tecnologias

  • React
  • TypeScript
  • Apollo Client

React

Por quê?

JSX

const handleSubmit = e => {
  /* ... */
};

const SingIn = props => (
  <form onSubmit={handleSubmit} className="singin">
    <label htmlFor="login">Login</label>
    <input name="login" id="login" />

    <label htmlFor="pass">Password</label>
    <input name="pass" id="pass" type="password" />

    <button type="submit">sing in</button>
  </form>
);

JavaScript puro

const Notifications = props => {
  const getList = notification => (
    <NotificationItem key={notification.id} {...notification} />
  );

  return (
    <div>
      {props.notifications.length ? (
        <ul>{props.notifications.map(getList)}</ul>
      ) : (
        <p>Nothing new...</p>
      )}
    </div>
  );
};

React

Por quê?

Flexibilidade na arquitetura

  • Framework de testes
  • Ou SASS, ou LESS, ou Stylus, ou CSS puro
  • Bibliotecas para requests
  • Bundler

React

Por quê?

Componentes reutilizáveis

  • Componentes próprios
  • Bibliotecas de terceiros

React

Por quê?

TypeScript

Por quê?

JS é uma linguagem de tipagem fraca

> 1 + 1
2

> 1 + "1"
"11"
> Array(16).join("js" - 1)
"NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN Batman!"
> Array(16).join("js" - 1) + " Batman!"
  • Mesma sintaxe e mesma semântica
  • Manutenção
  • Garantia em tempo de codificação

TS é um superset de tipos para o JS

TypeScript

Por quê?

prop-types vs TypeScript

import React from "react";
import PropTypes from "prop-types";

const UserType = PropTypes.shape({
  name: PropTypes.string.isRequired,
  login: PropTypes.string.isRequired,
  location: PropTypes.string,
  websiteUrl: PropTypes.string
});

const User = props => (
  <div>
    <h1>
      {props.name} ({props.login})
    </h1>
    {props.location && (
      <p>{props.location}</p>
    )}
    {props.websiteUrl && (
      <p>
        <a href={props.websiteUrl}>
          website
        </a>
      </p>
    )}
  </div>
);

User.propTypes = UserType.isRequired;
import React from "react";

type UserType = {
  name: string,
  login: string,
  location?: string,
  websiteUrl?: string
};

const User = (props: UserType) => (
  <div>
    <h1>
      {props.name} ({props.login})
    </h1>
    {props.location && (
      <p>{props.location}</p>
    )}
    {props.websiteUrl && (
      <p>
        <a href={props.websiteUrl}>
          website
        </a>
      </p>
    )}
  </div>
);

TypeScript

Por quê?

  • Documentação e comunidade
  • Gerenciamento de cache
  • Single source of truth

Apollo GraphQL

Por quê?

Nem tudo são flores

Curva de aprendizado muito alta

Tipos incongruentes

Schema e Tipos

type Planet implements Node {
  name: String
  diameter: Int
  rotationPeriod: Int
  orbitalPeriod: Int
  gravity: String
  population: Float
  climates: [String]
  terrains: [String]
  surfaceWater: Float
  residentConnection(after: String, first: Int, before: String, last: Int): PlanetResidentsConnection
  filmConnection(after: String, first: Int, before: String, last: Int): PlanetFilmsConnection
  created: String
  edited: String
  id: ID!
}
export interface Planet extends Node {
  name?: string;
  diameter?: number;
  rotationPeriod?: number;
  orbitalPeriod?: number;
  gravity?: string;
  population?: number;
  climates?: Array<string | null>;
  terrains?: Array<string | null>;
  surfaceWater?: number;
  residentConnection?: PlanetResidentsConnection;
  filmConnection?: PlanetFilmsConnection;
  created?: string;
  edited?: string;
  id: string;
}

Queries e Mutations

<Query />
query USER_QUERY ($login: String!) {
  user (login: $login) {
    id
    name
    login
    websiteUrl
    repositories (first: 100) {
      edges {
        node {
          id
          name
        }
      }
    }
  }
}
export default () => (
  <Query
    query={USER_QUERY}
    variables={{ login: "gabsprates" }}
  >
    {({ data, loading, error }) => {
      if (loading) return "loading...";
      if (error) return "error...";

      const { repositories } = data.user;

      return (
        <Menu 
          repositories={repositories.edges || []}
        />
      );
    }}
  </Query>
);
type MenuType = {
  repositories: RepositoryEdge[];
}

export const Menu = (props: MenuType) => (
  <ul className="menu">
    {props.repositories
      .filter(repo => repo && repo.node)
      .map(repo => (
        <li key={repo.node.id}>
          {repo.node.name}
        </li>
      ))}
  </ul>
);

Consumindo tipos da API

export interface RepositoryEdge {
  cursor: string;
  node?: Repository;
}

export interface Repository extends Node, ProjectOwner, RegistryPackageOwner, Subscribable, Starrable, UniformResourceLocatable, RepositoryInfo {
  createdAt: DateTime;
  description?: string;
  descriptionHTML: HTML;
  diskUsage?: number;
  forkCount: number;
  homepageUrl?: URI;
  id: string;
  name: string;
  owner: RepositoryOwner;
  stargazers: StargazerConnection;
  url: URI;
  /*...*/
}
<Mutation />
mutation ADD_STAR_MUTATION
($input: AddStarInput!) {
  addStar (input: $input) {
    starrable {
      id
    }
  }
}
import { Mutation } from "react-apollo";
import { ADD_STAR_MUTATION } from "./addStar.graphql";

export const AddStar = (props: { id: string }) => (
  <Mutation mutation={ADD_STAR_MUTATION}>
    {(addStar, { loading, error }) => {
      if (loading) return "loading...";
      if (error) return "error...";

      const add = () =>
        addStar({
          variables: {
            input: {
              starrableId: props.id,
              clientMutationId: ""
            }
          }
        });

      return (
        <button onClick={add}>starize it</button>
      );
    }}
  </Mutation>
);

Gerenciamento de Cache

InMemoryCache

import { InMemoryCache } from 'apollo-cache-inmemory';
import { HttpLink } from 'apollo-link-http';
import { ApolloClient } from 'apollo-client';

const cache = new InMemoryCache();

const client = new ApolloClient({
  link: new HttpLink(),
  cache
});
<Query variables={...} />
ROOT_QUERY

Objeto de armazenamento do Apollo Client

ROOT_QUERY: {
  repository({"name":"gabsprates.github.io","owner":"gabsprates"}):
    > Repository:id_Repository

  user({"login":"gabsprates"}): User
    > User:id_User
}

Quando uma Mutation

ROOT_QUERY

Alterar um registo que tenha um ID salvo no cache:

Esse registro será atualizado automaticamente

Criar/apagar um registro:

Pode atualizar o cache manualmente

const AddTodo = () => (
  <Mutation
    mutation={ADD_TODO}
    update={(cache, { data: { addTodo } }) => {
      const { todos } = cache.readQuery({ query: GET_TODOS });
      cache.writeQuery({
        query: GET_TODOS,
        data: {
          todos: todos.concat([addTodo])
        },
      });
    }}
  >
    {(addTodo, { loading, error }) => ( ... )}
  </Mutation>
);

Atualizando cache manualmente

const AddTodo = () => (
  <Mutation
    mutation={ADD_TODO}
    update={(cache, { data: { addTodo } }) => {
      const { todos } = cache.readQuery({ query: GET_TODOS });
      cache.writeQuery({
        query: GET_TODOS,
        data: {
          todos: todos.concat([addTodo])
        },
      });
    }}

    optimisticResponse={{
      // objeto com um mock da resposta da mutation
    }}

  >
    {(addTodo, { loading, error }) => ( ... )}
  </Mutation>
);

Optimistic UI

A função update será chamada duas vezes

Tratamento de erros

{
  "data": { ... },
  "errors": [
    {
      "message": "E-mail inválido",
      "path": [ ... ],
    }
  ]
}
const Login = () => (
  <Mutation mutation={LOGIN}>
    {(login, { loading, error }) => (
      if (loading) return "loading...";

      if (error) {
        return (
          <div>
            <p><strong>Erro:</strong></p>
            {error.graphQLErrors
              .map(({ message }, i) => (
                <p key={i}>{message}</p>
              ))}
          </div>
        );
      }

      return ( ... );
    )}
  </Mutation>
);

Okay...

The Bad

  • Processamento de queries
  • Curva de aprendizado

The Ugly

  • Semântica Web (Status Code)
  • Tratamento de erros
  • Código verboso

The Good

  • Otimização de banda / requests
  • Versionamento de APIs
  • Alinhamento de ambientes (Schema)
  • Qualidade de código (Tipos)
  • Documentação e comunidade

REST           1.3 GB

GraphQL    54 MB

REST           650 ms

GraphQL   181 ms

Vale a pena?

Obrigado

(:

Dúvidas?

GraphQL: The Good, the Bad and the Ugly

By Gabriel Prates

GraphQL: The Good, the Bad and the Ugly

  • 1,451