Slide 1

Slide 1 text

The GraphQL Way @qrush RailsConf 2018

Slide 2

Slide 2 text

Reconsider the Basics!

Slide 3

Slide 3 text

@qrush

Slide 4

Slide 4 text

rubygems.org

Slide 5

Slide 5 text

chatterbug

Slide 6

Slide 6 text

!"# $

Slide 7

Slide 7 text

!"#$% !"#$%

Slide 8

Slide 8 text

Live Lessons!

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

Agenda What is GraphQL Using in your app Potential pitfalls

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

Rails 5.1 Webpack (some) React JSON

Slide 13

Slide 13 text

JSON

Slide 14

Slide 14 text

REST

Slide 15

Slide 15 text

Endpoint explosion Expensive roundtrips Low discoverability Backwards compat REST Problems

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

1. What is GraphQL?

Slide 18

Slide 18 text

History 2012: 
 Started by Facebook 2015: Open sourced 2016: graphql-ruby

Slide 19

Slide 19 text

Companies GitHub Facebook Shopify

Slide 20

Slide 20 text

Use Cases Mobile app Single page app Anywhere a JSON API
 could be used

Slide 21

Slide 21 text

A Query query { currentUser { id login email isStudent updatedAt } }

Slide 22

Slide 22 text

Returns JSON! { "data": { "currentUser": { "id": "106", "login": "qrush", "email": "[email protected]", "isStudent": true, "updatedAt": "2018-03-13T15:20:14Z" } } }

Slide 23

Slide 23 text

Basics Yep, it’s still JSON Strongly typed Change-resilient Paging built-in Introspection

Slide 24

Slide 24 text

POST JSON

Slide 25

Slide 25 text

Types No more validations All I/O must be a type

Slide 26

Slide 26 text

Every app: QueryType MutationType InputObjectType

Slide 27

Slide 27 text

Our app: UserType UpdateUserMutation UserInputType

Slide 28

Slide 28 text

GraphiQL will change the way you write APIs

Slide 29

Slide 29 text

Free API Docs!

Slide 30

Slide 30 text

Changes No more API versioning Extend with graphs

Slide 31

Slide 31 text

New data model? query { currentUser { login upcomingAppointments { startTime language { code } } } }

Slide 32

Slide 32 text

No problem! { "data": { "currentUser": { "login": "qrush", "upcomingAppointments": [ { "startTime": "2018-03-25T04:00:00Z", "language": { "code": "de" } } ] } } }

Slide 33

Slide 33 text

Not included: Data model Authorization Authentication Caching

Slide 34

Slide 34 text

GraphQL Way Evolve Server +
 Client Introspect It All Shape Your
 Data

Slide 35

Slide 35 text

2. Using in your app

Slide 36

Slide 36 text

graphql-ruby.org

Slide 37

Slide 37 text

Off the Golden Path No Controllers No Views GraphQL DSL

Slide 38

Slide 38 text

Installing # Gemfile gem 'graphql' gem 'graphiql-rails' $ bundle $ rails generate graphql:install

Slide 39

Slide 39 text

Structure $ tree app/graphql/ app/graphql/ ├── chatterbug_schema.rb ├── mutations │ ├── mutation_type.rb │ └── update_user.rb └── types ├── language_type.rb └── query_type.rb

Slide 40

Slide 40 text

Schema ChatterbugSchema = GraphQL::Schema.define do query Types::QueryType mutation Mutations::MutationType end

Slide 41

Slide 41 text

Types Name Fields Description

Slide 42

Slide 42 text

Fields Name Arguments Resolver Description

Slide 43

Slide 43 text

Query Example query { germanLanguage { name } }

Slide 44

Slide 44 text

Expose it Types::QueryType = GraphQL::ObjectType.define do field :germanLanguage do type Types::LanguageType resolve -> (obj, args, ctx) do Language.find_by(code: "de") end end end

Slide 45

Slide 45 text

A type Types::LanguageType = GraphQL::ObjectType.define do name "Language" field :name, types.String, description: "Name of the language” end

Slide 46

Slide 46 text

Mutations mutation { updateUser(user: { timezone: "UTC" }) { login timezone updatedAt } }

Slide 47

Slide 47 text

A mutation Mutations::MutationType = GraphQL::ObjectType.define do field :updateUser, Types::UserType do argument :user, Types::UserInputType resolve -> (obj, args, ctx) { user = ctx[:current_user] user.update_attributes!(args.to_h['user']) user } end end

Slide 48

Slide 48 text

Authentication No built-in pattern Enforce via controller Pass a current user

Slide 49

Slide 49 text

How we auth class Api::GraphqlController < ApplicationController before_action :find_current_user_from_http_token # ... end

Slide 50

Slide 50 text

HTTP! def find_current_user_from_http_token authenticate_with_http_token do |token, options| @current_user = # …lookup based on token end end

Slide 51

Slide 51 text

Authorization No built-in pattern graphql.pro graphql-guard graphql-pundit

Slide 52

Slide 52 text

Testing Integration tests are hard Test custom logic Treat types like framework code

Slide 53

Slide 53 text

Functions # in app/graphql/mutations/update_user.rb class Mutations::UpdateUser < GraphQL::Function argument :user, Types::UserInputType def call(input, args, ctx) user = ctx[:current_user] user.update_attributes!(args.to_h[‘user']) user end end

Slide 54

Slide 54 text

Function tests require 'test_helper' class UpdateUserTest < ActiveSupport::TestCase test "can change info" do user = users(:scott) function = Mutations::UpdateUser.new returned_user = function.call(nil, {'user' => {'login' => 'scott'}}, {current_user: user}) assert_equal user.id, returned_user.id assert_equal 'scott', user.reload.login end end

Slide 55

Slide 55 text

Integration tests require 'test_helper' class GraphQLTest < ActionDispatch::IntegrationTest test "current token is required" do post "/api/graphql", as: :json, params: { query: <<~QUERY { currentUser { email } } QUERY } assert_response :unauthorized end end

Slide 56

Slide 56 text

Test auth! test "requesting current user with token" do post "/api/graphql", as: :json, params: { query: <<~QUERY { currentUser { email } } QUERY }, headers: { 'HTTP_AUTHORIZATION': ActionController::HttpAuthentication::Token .encode_credentials(user_tokens(:scott).token) } assert_response :success current_user = JSON.parse(response.body) .dig(“data", "currentUser") assert_equal "[email protected]", current_user["email"] end

Slide 57

Slide 57 text

3. Potential pitfalls

Slide 58

Slide 58 text

Handling errors # NOT BUILT IN! # Gemfile gem 'graphql-errors'

Slide 59

Slide 59 text

Resolve functions aren't sequential

Slide 60

Slide 60 text

Batching # N+1’s are real # Gemfile gem 'graphql-batch'

Slide 61

Slide 61 text

1 3 7 5 3 5 8 7

Slide 62

Slide 62 text

(1,3,5,7,8)

Slide 63

Slide 63 text

CacheQL # Released today! # All the goodies from this talk # Gemfile gem 'cacheql' https:/ /github.com/chatterbugapp/cacheql

Slide 64

Slide 64 text

Foreign Keys resolve -> (obj, args, context) { RecordLoader.for(Language) .load(args["id"]) } https:/ /github.com/Shopify/graphql-batch/blob/master/examples/record_loader.rb https:/ /github.com/rmosolgo/graphql-batch-example/blob/master/good_schema/find_loader.rb

Slide 65

Slide 65 text

Polymorphic Keys # belongs_to :respondable, # polymorphic: true resolve -> (obj, args, context) { PolyKeyLoader.for(obj, :respondable) .load(obj.respondable) } https:/ /github.com/rmosolgo/graphql-batch-example/blob/master/good_schema/polymorphic_key_loader.rb

Slide 66

Slide 66 text

Caching HTTP Etags Rails.cache Store results

Slide 67

Slide 67 text

Cache results resolve CacheQL -> (obj, args, ctx) { # expensive operation # cached on obj.cache_key } https:/ /github.com/rmosolgo/graphql-batch-example/blob/master/good_schema/polymorphic_key_loader.rb

Slide 68

Slide 68 text

Instrumentation Apollo Engine Scout APM Rails.logger

Slide 69

Slide 69 text

Scout

Slide 70

Slide 70 text

Rails.logger [CacheQL::Tracing] User.displayLanguage took 7.591ms [CacheQL::Tracing] User.createdAt took 0.117ms [CacheQL::Tracing] User.intercomHash took 0.095ms [CacheQL::Tracing] User.id took 0.09ms [CacheQL::Tracing] User.friendlyTimezone took 0.087ms [CacheQL::Tracing] User.utmContent took 0.075ms [GraphQL::Tracing] User.timezone took 0.048ms [CacheQL::Tracing] User.email took 0.046ms [CacheQL::Tracing] User.name took 0.042ms [CacheQL::Tracing] Query.currentUser took 0.041ms

Slide 71

Slide 71 text

The New Way? Will be hard if app
 logic coupled For fresh apps For new APIs

Slide 72

Slide 72 text

thanks! github.com/chatterbugapp/cacheql