Upload
bruno-lemos
View
701
Download
2
Embed Size (px)
Citation preview
GraphQL
APIs mais robustas e flexíveis@brunolemos
Sobre mim
➔ Desenvolvedor Web desde ~2005➔ Formado na USP de São Carlos
➔ Full Stack Developer na startup Easy Carros
➔ Hackathons◆ 1º lugar - Hackathon Globo 2016
◆ 1º lugar - MasterCard Code4Inclusion Miami
◆ 2º lugar - Masters of Code São Paulo
◆ 1º lugar - Destination Hack
◆ 1º lugar - API Hackday SP
@brunolemos
O que iremos abordar?
1. Motivação // que problemas resolve?
2. Características // query language, ...
3. Queries & Mutations // query { user(id: 1) { name } }
4. Na prática // adicionando GraphQL à uma API já existente
5. Autenticação // segurança
6. Client & Libs // relay, apollo, …
7. Próximos passos // o que não abordamos + futuro do graphql
1.Motivação
Imagine uma aplicação na qual você pode:
1. Adicionar amigos2. Publicar posts3. Curtir páginas
1. Motivação
Como você pegaria o último post de cada amigo seu?
1. Motivação
Usando REST
GET /v1/me { _id: 1, friends: [2, 3, 4,5] }
GET /v1/users/2/posts/last {_id: ‘post_2a’}
GET /v1/users/3/posts/last {_id: ‘post_3a’}
GET /v1/users/4/posts/last {_id: ‘post_4a’}
GET /v1/users/5/posts/last {_id: ‘post_5a’}
1. Motivação
Muitas requisições… Já sei, vou criar um endpoint para isso.
GET /v1/myFriendsLastPosts [{_id: 1, lastPost: {...}, {_id: 2, lastPost: {...}]
Usando “REST”
E se eu quiser obter as páginas que meus amigos curtiram?
1. Motivação
E se eu quiser obter as páginas que meus amigos curtiram e os últimos posts?
GET /v1/myFriendsLikedPages [{_id: 1, pages: [...]}, {_id: 2, pages: [...]}]
Usando “REST”1. Motivação
GET /v1/myFriendsLikedPages [{_id: 1, pages: [...]}, {_id: 2, pages: [...]}]
GET /v1/myFriendsLastPosts [{_id: 1, lastPost: {...}}, {_id: 2, lastPost: {...}}]
Usando “REST”1. Motivação
// faço o merge dos resultados no client
[{_id: 1, lastPost: {...}, pages: [...]}, ...]
// já sei! que tal um novo endpoint?
GET /v1/myFriendsLastPostsAndPages
👎👎👎👎👎👎
No REST, é fácil você se encontrar criando endpoints para retornos específicos. Isto não é escalável.
1. Motivação
Além disso…Ao fazer um GET em um endpoint, que dados serão
retornados? #surpriseComo descobrir:
1. Fazer uma requisição de teste2. Ler a documentação (pode estar desatualizada)3. Ler o código
Dados retornados:4. Provavelmente muito mais do que você precisa
1. Motivação
“Analisamos alternativas, como o REST. (...) Ficamos frustados com as diferenças entre os dados que queríamos e as requests que eram necessárias para obtê-los.”
2012Lee Byron, Facebook Software Engineer
1. Motivação
GraphQL é criado pelo Facebook
Usado apenas internamente
20121. Motivação
GraphQL liberado para o público (open source)
20151. Motivação
Características
O client declara os dados que precisa e a resposta é um espelho da entrada“Retorne isto. Nada mais, nada menos.”
Declarative query language//REQUEST
query { user(_id: “xxx”) { _id name email }}
//RESPONSE
{ "data": { "user": { "_id": "xxx", "name": "Bruno Lemos", "email": "[email protected]" } }}
Características
Funções “resolve”
Visão geralCaracterísticas
Resposta em JSON no mesmo
formato da entrada
Entrada dos dados que
precisaCamada do
GraphQL
Diferentes clients
Bancos de dados
Servidor
Retorno da função “resolve” com os dados desejados
Na função resolve, você é livre para pegar o dado de onde quiser
// Funções “resolve” são as responsáveis por dizer onde pegar os dados.// Podem retornar dados de qualquer lugar, desde que retorne o valor final ou uma Promise.
// Exemplos para query { user(_id: “xxx”) { name } }resolve: (root, args, context) => ({ name: ‘Bruno Lemos’, outroCampo: ‘X’ }), // Dado arbitrárioresolve: (root, args, context) => User.findById(args._id), // Método que retorna uma promiseresolve: (root, args, context) => fetch(‘http://api.site.com/v1/user’), // API externa
Características
Funções “resolve”
Sintaxe
Query
Queries são como o GET do REST:
Você usa para obter dados, não podendo fazer mutações.
QuerySintaxe
query { user(_id: “xxx”) { _id name }}
{ "data": { "user": { "_id": "xxx", "name": "Bruno Lemos" } }}
Query: Várias ao mesmo tempoSintaxe
query { user(_id: “xxx”) { _id name } vehicle(_id: “tesla_model_s”) { brand model }}
{ "data": { "user": { "_id": "xxx", "name": "Bruno Lemos" }, "vehicle": { "brand": "Tesla", "model": "Model S" } }}
Query: Várias ao mesmo tempoSintaxe
query { user(_id: “xxx”) { _id name } user(_id: “xxx”) { github }}
{ "data": { ? }}
Query: Várias ao mesmo tempoSintaxe
query { user(_id: “xxx”) { _id name } user(_id: “xxx”) { github }}
{ "data": { "user": { "_id": "xxx", "name": "Bruno Lemos", "github": "brunolemos" }, }}
Query: Várias ao mesmo tempoSintaxe
query { user(_id: “xxx”) { _id name } user(_id: “xxx_2”) { github }}
{ "data": { ? }}
Query: Várias ao mesmo tempoSintaxe
query { user(_id: “xxx”) { _id name } user(_id: “xxx_2”) { github }}
{ "errors": [{ "message": "Fields \"user\" conflict because they have differing arguments. use different aliases on the fields to fetch both if this was intentional." }]}
Query: AliasSintaxe
query { dan: user(_id: “dan_id”) { _id name } arunoda: user(_id: “arunoda_id”) { _id name }}
{ "data": { "dan": { "_id": "dan_id", "name": "Dan Abramov", }, "arunoda": { "_id": "arunoda_id", "name": "Arunoda Susiripala", } }}
Query: NestedSintaxe
query { user(_id: “xxx”) { _id name friends(limit: 1) { name } }}
{ "data": { "user": { "_id": "xxx", "name": "Bruno Lemos", "friends": [{ "name": "Dan Abramov" ]} } }}
Query: Nested!!!Sintaxe
query { user(_id: “xxx”) { _id name friends(limit: 1) { name friends(limit: 1) { name } } }}
{ "data": { "user": { "_id": "xxx", "name": "Bruno Lemos", "friends": [{ "name": "Dan Abramov", "friends": [{ "name": "Arunoda Susiripala" }], }], }, }}
Query: Nested (exemplo inicial)Sintaxe
{ "data": { "user": { "name": "Bruno Lemos", "friends": [{ "name": "Sashko Stubailo", "latestPost": { "title": "GraphQL is the future" }, "pages": [{ "name": "Apollo Client" }], }], }, }}
query { user(_id: “xxx”) { name friends { name latestPost { title } pages { name } } }}
Query: Nested + UtilsSintaxe
query { user(_id: “xxx”) { thumbnail: image(size: 100) { url width height } fullPicture: image { url width height } }}
{ "data": { "user": { "thumbnail": { "url": "thumbnail_100x100.jpg", "width": 100, "height": 100 }, "fullPicture": { "url": "picture.jpg", "width": 2048, "height": 2048 } } }}
Query: Nested + UtilsSintaxe
query { user(_id: “xxx”) { createdAt { format(format: "DD/MM/YYYY HH:mm") timezone iso timestamp } }}
{ "data": { "user": { "createdAt": { "format": "01/09/2016 19:30", "timezone": "America/Sao_Paulo", "iso": "2016-09-01T22:30:00.000Z", "timestamp": "1472769000000" } } }}
Query: Nested + UtilsSintaxe
query { user(_id: “xxx”) { createdAt(timezone: “America/New_York”) { format(format: "DD/MM/YYYY HH:mm") timezone } }}
{ "data": { "user": { "createdAt": { "format": "01/09/2016 18:30", "timezone": "America/New_York" } } }}
Query
Ok, o campo name é sempre String, o campo age é sempre Int, …
E se eu tiver um campo que possa retornar mais de um tipo?
Exemplo: campo user que pode ser tanto do tipo User quanto Admin
query { me { __typename ... on Admin { name } ... on User { name age } }}
Query: Múltiplos tiposSintaxe
{ "data": { "me": { "__typename": "Admin", "name": "Bruno Lemos" } }}
A query ‘me’ pode retornar um tipo diferentedependendo de quem está logado no momento
query { me { __typename ... on Admin { name } ... on User { name age } }}
Query: Múltiplos tiposSintaxe
{ "data": { "me": { "__typename": "User", "name": "Bruno Lemos", "age": 23 } }}
A query ‘me’ pode retornar um tipo diferentedependendo de quem está logado no momento
Ok, chega de query
Se as queries são como o GET do REST, como fazer o POST / PUT / DELETE?
Mutation
Mutations são como o POST / PUT / DELETE do REST:
Você usa quando haverá alteração nos dados.
[POST] /v1/users
[PUT] /v1/users/1
[DELETE] /v1/users/1
addUser(name: “Mateus”)
updateUser(_id: 1, name: “Matheus”)
deleteUser(_id: 1)
MutationSintaxe
mutation { addUser(name: “Bruno Lemos”) { _id name }}
{ "data": { "addUser": { "_id": "xxx_2", "name": "Bruno Lemos" } }}
MutationSintaxe
mutation { deleteUser(_id: “xxx”)}
{ "data": { "deleteUser": true }}
MutationSintaxe
mutation { deleteUser(_id: “id_nao_existente”)}
{ "data": { ? }}
MutationSintaxe
mutation { deleteUser(_id: “id_nao_existente”)}
{ "data": { "deleteUser": null }, "errors": [ { "message": "Usuário não encontrado.", "path": [ "deleteUser" ], } ]}
VariáveisSintaxe
mutation($name: String!) { addUser(name: $name) { _id name }}
//QUERY VARIABLES{ "name": "Bruno Lemos"}
{ "data": { "addUser": { "_id": "xxx_3", "name": "Bruno Lemos" } }}
FragmentSintaxe
{ "data": { "user": { "_id": "xxx", "name": "Bruno Lemos", "github": "brunolemos" } }}
query { user(_id: “xxx”) { _id ...RetornoPadrao }}
fragment RetornoPadrao on User { name github}
Na práticaAdicionando GraphQL à uma API já existente
Vamos criar uma query que receba um argumento _id e retorne o usuário correspondente.
Na prática
// Vamos usar Node.js
// Dependências:
$ npm i -S express graphql express-graphql
Antes de tudo...Servidor
index.jsconst express = require('express');
const app = express();
const server = app.listen(process.env.PORT || 3000, () => { const { address, port } = server.address(); console.log(`Running at http://${address}:${port}`);});
Criando o servidor
const graphqlHTTP = require('express-graphql');const schema = require('./schema'); //será criado em breve
app.use('/graphql', graphqlHTTP({ schema, graphiql: true }));
./user/type.js
export default new GraphQLObjectType({ name: 'User', fields: { _id: { type: new GraphQLNonNull(GraphQLID) }, name: { type: GraphQLString }, },});
Precisamos criar o tipo Usuário, que será o retorno da queryTipos já existentes: ID, String, Int, Boolean, Object, Enum, List, … (ver lista completa)
Criando o servidor
./user/query.js
Cada Query é um objeto comumDefine o tipo de retorno, os argumentos de entrada e a função “resolve”
Criando o servidor
// bluebird converte uma funcao que pussui callback em uma promiseconst getUserAsync = Bluebird.promisify(myMethodFromOldApiThatUsesCallback);
export default { type: UserType, // arquivo criado anteriormente args: { _id: { type: new GraphQLNonNull(GraphQLID) }, }, resolve: (root, args, context) => getUserAsync(args._id), // onde a mágica acontece};
schema.jsexport default new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQuery', fields: { user: UserQuery, // arquivo criado anteriormente // users: ..., // aqui iria as outras queries // posts: ..., }, }), mutation: ..., // definirá todas as mutations existentes (mesma sintaxe acima)});
Schemas possuem uma RootQuery e uma RootMutation
Criando o servidor
Autenticação
Autenticação
query { login(email: “[email protected]”, password: “123”) { token _id name }}
{ "data": { "login": { "token": "ABCDEF", "_id": "xxx", "name": "Bruno Lemos" } }}
Autenticação
Autenticação
query { viewer(token: “ABCDEF”) { me { _id name } }}
{ "data": { "viewer": { "me": { "_id": "xxx", "name": "Bruno Lemos" } } }}
Autenticação
Autenticação
mutation { viewer(token: “ABCDEF”) { deleteUser(_id: “xxx”) }}
{ "data": { "viewer": { "deleteUser": true } }}
Autenticação
Autenticação
mutation { viewer(token: “ABCDEF”) { deleteUser(_id: “id_de_outro_usuario”) }}
{ "data": { "viewer": { "deleteUser": null } }, "errors": [ { "message": "Não autorizado.", "path": [ "deleteUser" ], } ]}
Autenticação
./viewer/query.jsexport default { ViewerRootQuery, // todas as queries que estarão dentro da query viewer args: { token: { type: GraphQLString }, }, resolve: (root, { token }, context) => { // context é global, acessível de todas as queries context.token = token;
try { context.user = jwt.verify(token, config.jwtSecret) || {}; } catch (err) { context.user = {}; }
return {}; },};
Autenticação
GraphiQL
GraphiQL
http://localhost:3000/graphql
http://localhost:3000/graphql
GraphiQLDocumentação automática
GraphiQLDocumentação automática
http://localhost:3000/graphql
GraphiQL Documentação automática, execução de queries
http://localhost:3000/graphql
GraphiQL Documentação automática, execução de queries
http://localhost:3000/graphql
GraphiQL Documentação automática, execução de queries, autocomplete
http://localhost:3000/graphql
GraphiQL = Documentação automática, execução de queries, autocomplete, …
http://localhost:3000/graphql
GraphiQL = Documentação automática, execução de queries, autocomplete, … É como ter um Graph API Explorer próprio!
https://developers.facebook.com/tools/explorer
Client
No React, cada parte da aplicação é um Component
Cada Component sabe os dados que precisa
Como obter estes dados do GraphQL?
Client
Client: Relay
Client: Relay
Client: Relay
class UserProfile extends Component { render() { var { name, avatar } = this.props.user; return ( <div> <img src={avatar}/> <p>{name}</p> </div> ); }}
// continua...
Relay
Client: RelayRelay
UserProfile = Relay.createContainer(UserProfile, { fragments: { user: () => Relay.QL` fragment on User { name, avatar, } `, },});
Libs
Server
GraphQL não é apenas para Node.js.
Existem implementações para Ruby, PHP, Go, Python, Haskell, ...
Client
Relay (react)
Apollo (react, angular, ios swift, ...)
Libs
Libs
Utils
Graffiti (usar schema do Mongoose)
Model Visualizer (converta seu schema em um diagrama)
Services
Reindex (backend as a service)
Lista completa: Awesome GraphQL
Próximos passos
Não abordamos:
Cache / DataLoader
Client a fundo (Como fazer mutations, …)
Do GraphQL:
Subscriptions / realtime
Directives (@defer, @export, …)
Próximos passos
GraphQL é o futuro?
Obrigado@brunolemos