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

Eb8975af8e49e19e3dd6b6b84a542e26?s=128

Nick Quaranto

April 17, 2018
Tweet

Transcript

  1. The GraphQL Way @qrush RailsConf 2018

  2. Reconsider the Basics!

  3. @qrush

  4. rubygems.org

  5. chatterbug

  6. !"# $

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

  8. Live Lessons!

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

  11. None
  12. Rails 5.1 Webpack (some) React JSON

  13. JSON

  14. REST

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

  16. None
  17. 1. What is GraphQL?

  18. History 2012: 
 Started by Facebook 2015: Open sourced 2016:

    graphql-ruby
  19. Companies GitHub Facebook Shopify

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

    API
 could be used
  21. A Query query { currentUser { id login email isStudent

    updatedAt } }
  22. Returns JSON! { "data": { "currentUser": { "id": "106", "login":

    "qrush", "email": "nick@quaran.to", "isStudent": true, "updatedAt": "2018-03-13T15:20:14Z" } } }
  23. Basics Yep, it’s still JSON Strongly typed Change-resilient Paging built-in

    Introspection
  24. POST JSON

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

  26. Every app: QueryType MutationType InputObjectType

  27. Our app: UserType UpdateUserMutation UserInputType

  28. GraphiQL will change the way you write APIs

  29. Free API Docs!

  30. Changes No more API versioning Extend with graphs

  31. New data model? query { currentUser { login upcomingAppointments {

    startTime language { code } } } }
  32. No problem! { "data": { "currentUser": { "login": "qrush", "upcomingAppointments":

    [ { "startTime": "2018-03-25T04:00:00Z", "language": { "code": "de" } } ] } } }
  33. Not included: Data model Authorization Authentication Caching

  34. GraphQL Way Evolve Server +
 Client Introspect It All Shape

    Your
 Data
  35. 2. Using in your app

  36. graphql-ruby.org

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

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

    rails generate graphql:install
  39. Structure $ tree app/graphql/ app/graphql/ ├── chatterbug_schema.rb ├── mutations │

    ├── mutation_type.rb │ └── update_user.rb └── types ├── language_type.rb └── query_type.rb
  40. Schema ChatterbugSchema = GraphQL::Schema.define do query Types::QueryType mutation Mutations::MutationType end

  41. Types Name Fields Description

  42. Fields Name Arguments Resolver Description

  43. Query Example query { germanLanguage { name } }

  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
  45. A type Types::LanguageType = GraphQL::ObjectType.define do name "Language" field :name,

    types.String, description: "Name of the language” end
  46. Mutations mutation { updateUser(user: { timezone: "UTC" }) { login

    timezone updatedAt } }
  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
  48. Authentication No built-in pattern Enforce via controller Pass a current

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

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

    …lookup based on token end end
  51. Authorization No built-in pattern graphql.pro graphql-guard graphql-pundit

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

    like framework code
  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
  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
  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
  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 "schacon@gmail.com", current_user["email"] end
  57. 3. Potential pitfalls

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

  59. Resolve functions aren't sequential

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

  61. 1 3 7 5 3 5 8 7

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

  63. CacheQL # Released today! # All the goodies from this

    talk # Gemfile gem 'cacheql' https:/ /github.com/chatterbugapp/cacheql
  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
  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
  66. Caching HTTP Etags Rails.cache Store results

  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
  68. Instrumentation Apollo Engine Scout APM Rails.logger

  69. Scout

  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
  71. The New Way? Will be hard if app
 logic coupled

    For fresh apps For new APIs
  72. thanks! github.com/chatterbugapp/cacheql