$30 off During Our Annual Pro Sale. View Details »

The GraphQL Way: A new path for JSON APIs

The GraphQL Way: A new path for JSON APIs

Have you written JSON APIs in Rails a few times? Even if you’re escaped implementing a “render: :json” call, some day soon you’ll need to get JSON data out to a native client or your front-end framework. Your Rails apps might change, but the pitfalls for JSON APIs stay the same: They’re hard to discover, difficult to change from the client side, and documenting them is a pain. For your next app, or your current one, let’s consider GraphQL. I'll show you how to implement it in your app and offer real-world advice from making it work in an existing Rails app.

https://github.com/chatterbugapp/cacheql

Nick Quaranto

April 17, 2018
Tweet

More Decks by Nick Quaranto

Other Decks in Programming

Transcript

  1. The
    GraphQL Way
    @qrush
    RailsConf 2018

    View Slide

  2. Reconsider the
    Basics!

    View Slide

  3. @qrush

    View Slide

  4. rubygems.org

    View Slide

  5. chatterbug

    View Slide

  6. !"#
    $

    View Slide

  7. !"#$%
    !"#$%

    View Slide

  8. Live Lessons!

    View Slide

  9. View Slide

  10. Agenda
    What is GraphQL
    Using in your app
    Potential pitfalls

    View Slide

  11. View Slide

  12. Rails 5.1
    Webpack
    (some) React
    JSON

    View Slide

  13. JSON

    View Slide

  14. REST

    View Slide

  15. Endpoint explosion
    Expensive roundtrips
    Low discoverability
    Backwards compat
    REST Problems

    View Slide

  16. View Slide

  17. 1. What
    is GraphQL?

    View Slide

  18. History
    2012: 

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

    View Slide

  19. Companies
    GitHub
    Facebook
    Shopify

    View Slide

  20. Use Cases
    Mobile app
    Single page app
    Anywhere a JSON API

    could be used

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  24. POST
    JSON

    View Slide

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

    View Slide

  26. Every app:
    QueryType
    MutationType
    InputObjectType

    View Slide

  27. Our app:
    UserType
    UpdateUserMutation
    UserInputType

    View Slide

  28. GraphiQL
    will change the
    way you write APIs

    View Slide

  29. Free API Docs!

    View Slide

  30. Changes
    No more API versioning
    Extend with graphs

    View Slide

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

    View Slide

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

    View Slide

  33. Not included:
    Data model
    Authorization
    Authentication
    Caching

    View Slide

  34. GraphQL Way
    Evolve Server +

    Client
    Introspect It All
    Shape Your

    Data

    View Slide

  35. 2. Using
    in your app

    View Slide

  36. graphql-ruby.org

    View Slide

  37. Off the Golden Path
    No Controllers
    No Views
    GraphQL DSL

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  41. Types
    Name
    Fields
    Description

    View Slide

  42. Fields
    Name
    Arguments
    Resolver
    Description

    View Slide

  43. Query Example
    query {
    germanLanguage {
    name
    }
    }

    View Slide

  44. 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

    View Slide

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

    View Slide

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

    View Slide

  47. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  53. 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

    View Slide

  54. 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

    View Slide

  55. 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

    View Slide

  56. 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

    View Slide

  57. 3. Potential
    pitfalls

    View Slide

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

    View Slide

  59. Resolve functions
    aren't sequential

    View Slide

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

    View Slide

  61. 1 3 7 5
    3 5 8
    7

    View Slide

  62. (1,3,5,7,8)

    View Slide

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

    View Slide

  64. 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

    View Slide

  65. 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

    View Slide

  66. Caching
    HTTP Etags
    Rails.cache
    Store results

    View Slide

  67. 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

    View Slide

  68. Instrumentation
    Apollo Engine
    Scout APM
    Rails.logger

    View Slide

  69. Scout

    View Slide

  70. 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

    View Slide

  71. The New Way?
    Will be hard if app

    logic coupled
    For fresh apps
    For new APIs

    View Slide

  72. thanks!
    github.com/chatterbugapp/cacheql

    View Slide