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