Upgrade to Pro — share decks privately, control downloads, hide ads and more …

One Year GraphQL in Production

One Year GraphQL in Production

Presented at DevDays Vilnius 2018

Show all mistakes and solution to problems, I have encountered during the last year and a half when replacing Product Hunt REST API with GraphQL. Show how GraphQL can improve the application structure of a Rails application. So it is effortless for backend developers to develop and maintain features.

– integrating GraphQL with Rails
– GraphQL schema design
– application structure
– authorization
– optimization and performance
– monitoring

Links:

- https://producthunt.com/
- https://graphql.org/
- https://rubygems.org/gems/graphql
- https://rubygems.org/gems/graphiql-rails
- https://rubygems.org/gems/search_object
- https://rubygems.org/gems/graphql-batch
- 
https://www.howtographql.com/
- https://newrelic.com/
- https://sentry.io/
- https://github.com/Shopify/graphql-batch/blob/master/examples/association_loader.rb
- https://gist.github.com/RStankov/48070003a31d71a66f57a237e27d5865

Radoslav Stankov

May 23, 2018
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. One Year GraphQL
    in
    Production
    Radoslav Stankov 23/05/2018

    View Slide

  2. Radoslav Stankov
    @rstankov

    blog.rstankov.com

    github.com/rstankov

    twitter.com/rstankov

    View Slide

  3. View Slide

  4. https://speakerdeck.com/rstankov/one-year-graphql-in-production

    View Slide

  5. View Slide

  6. View Slide

  7. early 2014 jQuery spaghetti
    October 2014 Backbone
    February 2015 React & Rails
    May 2015 custom Flux

    View Slide

  8. early 2014 jQuery spaghetti
    October 2014 Backbone
    February 2015 React & Rails
    May 2015 custom Flux
    December 2015 Redux

    View Slide

  9. early 2014 jQuery spaghetti
    October 2014 Backbone
    February 2015 React & Rails
    May 2015 custom Flux
    December 2015 Redux
    January 2016 React-Router

    View Slide

  10. early 2014 jQuery spaghetti
    October 2014 Backbone
    February 2015 React & Rails
    May 2015 custom Flux
    December 2015 Redux
    January 2016 React-Router
    April 2016 Redux Ducks

    View Slide

  11. early 2014 jQuery spaghetti
    October 2014 Backbone
    February 2015 React & Rails
    May 2015 custom Flux
    December 2015 Redux
    January 2016 React-Router
    April 2016 Redux Ducks
    Febuary 2017 GraphQL

    View Slide

  12. October 2014 Backbone
    February 2015 React & Rails
    May 2015 custom Flux
    December 2015 Redux
    January 2016 React-Router
    April 2016 Redux Ducks
    Febuary 2017 GraphQL

    View Slide

  13. February 2015 React & Rails
    May 2015 custom Flux
    December 2015 Redux
    January 2016 React-Router
    April 2016 Redux Ducks
    Febuary 2017 GraphQL

    View Slide

  14. December 2015 Redux
    January 2016 React-Router
    April 2016 Redux Ducks
    Febuary 2017 GraphQL

    View Slide

  15. early 2014 jQuery spaghetti
    October 2014 Backbone
    February 2015 React & Rails
    May 2015 custom Flux
    December 2015 Redux
    January 2016 React-Router
    April 2016 Redux Ducks
    Febuary 2017 GraphQL

    View Slide


  16. http://graphql.org/


    View Slide

  17. View Slide

  18. View Slide

  19. View Slide


  20. posts {
    id
    name
    tagline
    votesCount
    commentsCount
    topics {
    id
    name
    }
    isVoted
    }

    View Slide

  21. query {
    posts(date: '2018-05-05') {
    id
    name
    tagline
    votesCount
    commentsCount
    topics {

    id
    name
    }
    isVoted
    }
    }

    View Slide

  22. POST /graphql
    query {
    posts(date: '2018-05-05') {
    id
    name
    tagline
    votesCount
    commentsCount
    topics {
    id
    name
    }
    isVoted
    }
    }

    View Slide

  23. POST /graphql
    query {
    posts(date: '2018-05-05') {
    id
    name
    tagline
    votesCount
    commentsCount
    topics {
    id
    name
    }
    isVoted
    }
    }

    View Slide

  24. POST /graphql
    {
    "data": {
    "posts": {
    "id": 1,
    "name": "Google Duplex",
    "tagline": "An AI assistant that can tal
    "votesCount": 2843,
    "commentsCount": 45,
    "topics": [{
    "id": 1,
    "name": "AI",
    }],
    "isVoted": true,
    }
    }
    }

    query {
    posts(date: '2018-05-05') {
    id
    name
    tagline
    votesCount
    commentsCount
    topics {
    id
    name
    }
    isVoted
    }
    }

    View Slide


  25. http://graphql.org/


    View Slide

  26. View Slide

  27. View Slide

  28. gem "graphql"

    View Slide

  29. query {
    posts(date: '2018-05-05') {
    id
    name
    tagline
    }
    }

    View Slide

  30. Graph::Types::PostType = GraphQL::ObjectType.define do
    name 'Post'
    field :id, !types.ID
    field :name, !types.String
    field :tagline, !types.String
    end
    query {
    posts(date: '2018-05-05') {
    id
    name
    tagline
    }
    }

    View Slide

  31. Graph::Types::Query = GraphQL::ObjectType.define do
    name 'Query'
    field :post, !types[!Graph::Types::PostType] do
    argument :date, !types.String
    resolve -> (_obj, args, _ctx) { 

    Post.for_date(args[:date])
    }
    end
    end
    query {
    posts(date: '2018-05-05') {
    id
    name
    tagline
    }
    }

    View Slide

  32. Graph::Schema = GraphQL::Schema.define do
    query Graph::Types::Query
    end
    query {
    posts(date: '2018-05-05') {
    id
    name
    tagline
    }
    }

    View Slide

  33. class GraphqlController < Frontend::BaseController
    def index
    render json: Graph::Schema.execute(query, variables: variables, context: context)
    end
    private
    def query
    params[:query]
    end
    def context
    {
    current_user: current_user,
    }
    end
    def variables
    convert_to_hash params[:variables]
    end
    def convert_to_hash(variables)
    # ...
    end
    end

    View Slide

  34. Rails.application.routes.draw do
    post '/graphql' => 'graphql#index', defaults: { format: :json }
    # ...
    end

    View Slide

  35. gem "graphiql-rails"

    View Slide

  36. Rails.application.routes.draw do
    post '/graphql' => 'graphql#index', defaults: { format: :json }
    mount GraphiQL::Rails::Engine, graphql_path: '/graphql', at: '/graphiql' if Rails.env.development?
    # ...
    end

    View Slide

  37. View Slide

  38. Query

    View Slide

  39. Result

    View Slide

  40. Automatic
    Documentation

    View Slide

  41. View Slide

  42. " simplifies communication with frontend developers
    # no extra controllers
    $ no extra routes
    % build-in data serialization
    & build-in documentation
    ' ecosystem of tools
    GraphQL Benefits

    (backend developer edition)

    View Slide


  43. https://www.howtographql.com/


    View Slide


  44. https://www.howtographql.com/


    View Slide


  45. https://www.howtographql.com/


    View Slide

  46. Structure

    View Slide

  47. View Slide

  48. " 51 root query fields
    # 136 types
    $ 132 mutations
    & 130 resolvers
    ( 7 developers
    Stats

    View Slide

  49. class Graph::Resolvers::Posts::PostsResolver < Graph::Resolvers::SearchResolver
    scope { Post.featured.by_credible_votes }
    OrderType = GraphQL::EnumType.define do
    name 'PostOrder'
    value 'NEWEST'
    value 'POPULAR'
    end
    option :order, type: OrderType, default: 'POPULAR'
    option :date, type: types.Int, with: :apply_date_filter
    option :query, type: types.String, with: :apply_query_filter
    # ...
    private
    def apply_order_with_newest(scope)
    scope.order_by_date
    end
    def apply_order_with_popular(scope)
    scope.order_by_votes
    end
    def apply_date_filter(scope, value)
    scope.for_date(value)
    end
    def apply_query_filter(scope, value)
    scope.for_query(query)
    end
    # ...
    end

    View Slide

  50. Graph::Query = GraphQL::ObjectType.define do
    name 'Query'
    connection :posts, Graph::Types::PostType.connection_type,
    function: Graph::Resolvers::Posts::PostsResolver
    # ...
    end

    View Slide

  51. Monitoring

    View Slide

  52. View Slide

  53. View Slide

  54. View Slide

  55. View Slide

  56. Graph::Schema = GraphQL::Schema.define do

    #...
    use GraphQL::Tracing::NewRelicTracing
    #...
    end

    View Slide

  57. View Slide

  58. View Slide

  59. View Slide

  60. View Slide

  61. Which query causes
    this issue? )

    View Slide

  62. class Frontend::GraphqlController < Frontend::BaseController
    before_action :ensure_query
    def index
    render json: Graph::Schema.execute(query, variables: variables, context: context)
    rescue => e
    handle_error e
    end
    private
    def handle_error(e)
    if Rails.env.development?
    logger.error e.message
    logger.error e.backtrace.join("\n")
    response = {
    darta: nil
    errors: [{
    message: e.message,
    extensions: {
    backtrace: e.backtrace,
    }
    }],
    }
    render json: response, status: 500
    elsif Rails.env.test?
    p e.message
    p e.backtrace
    else
    Raven.capture_exception(e, extra: { query: query })
    end
    end
    # ...
    end

    View Slide

  63. class Frontend::GraphqlController < Frontend::BaseController
    before_action :ensure_query
    def index
    render json: Graph::Schema.execute(query, variables: variables, context: context)
    rescue => e
    handle_error e
    end
    private
    def handle_error(e)
    if Rails.env.development?
    logger.error e.message
    logger.error e.backtrace.join("\n")
    response = {
    darta: nil
    errors: [{
    message: e.message,
    extensions: {
    backtrace: e.backtrace,
    }
    }],
    }
    render json: response, status: 500
    elsif Rails.env.test?
    p e.message
    p e.backtrace
    else
    Raven.capture_exception(e, extra: { query: query })
    end
    end
    # ...
    end

    View Slide

  64. View Slide

  65. View Slide

  66. Authentication & Authorization

    View Slide

  67. View Slide

  68. gem 'omniauth'
    gem 'a-twitter'
    gem 'omniauth-angellist'
    gem 'omniauth-facebook'

    View Slide

  69. View Slide

  70. View Slide

  71. class Frontend::GraphqlController < Frontend::BaseController
    before_action :ensure_query
    def index
    render json: Graph::Schema.execute(query, variables: variables, context: context)
    rescue => e
    handle_error e
    end
    private
    # ...
    def context
    {
    current_user: current_user,
    request: request,
    }
    end
    # ...
    end

    View Slide

  72. Graph::Query = GraphQL::ObjectType.define do
    name 'Query'
    field :viewer, Graph::Types::ViewerType do
    resolve ->(_obj, _args, ctx) { ctx[:current_user] }
    end
    # ...
    end

    View Slide

  73. query {
    viewer {
    id
    username
    // ...
    }
    }

    View Slide

  74. View Slide

  75. Graph::Schema = GraphQL::Schema.define do
    # ...
    authorization Graph::Utils::AuthorizationStrategy
    instrument :field, Graph::Utils::FallbackInstrumenter
    # ...
    end

    View Slide

  76. class Graph::Utils::AuthorizationStrategy
    def initialize(ctx)
    @current_user = ctx[:current_user]
    end
    def allowed?(gate, value)
    Authorization.can? @current_user, gate.role, value
    end
    end

    View Slide

  77. Graph::Types::SurveyType = GraphQL::ObjectType.define do
    name 'Survey'
    authorize Authorization::READ
    field :id, !types.ID
    field :title, !types.String
    # ...
    field :questions, function: Graph::Resolvers::Surveys::QuestionsResolver.new
    field :answers, authorize: Authorization::MANAGE_FIELD, fallback: [], functio
    field :canManage, function: Graph::Resolvers::CanResolver.new(Authorization::
    end

    View Slide

  78. Graph::Types::SurveyType = GraphQL::ObjectType.define do
    name 'Survey'
    authorize Authorization::READ
    field :id, !types.ID
    field :title, !types.String
    # ...
    field :questions, function: Graph::Resolvers::Surveys::QuestionsResolver.new
    field :answers, authorize: Authorization::MANAGE_FIELD, fallback: [], functio
    field :canManage, function: Graph::Resolvers::CanResolver.new(Authorization::
    end

    View Slide

  79. Graph::Types::SurveyType = GraphQL::ObjectType.define do
    name 'Survey'
    authorize Authorization::READ
    field :id, !types.ID
    field :title, !types.String
    # ...
    field :questions, function: Graph::Resolvers::Surveys::QuestionsResolver.new
    field :answers, authorize: Authorization::MANAGE_FIELD, fallback: [], functio
    field :canManage, function: Graph::Resolvers::CanResolver.new(Authorization::
    end
    module Authorization
    READ = :read
    MANAGE = :manage
    MANAGE_FIELD = { parent_role: MAINTAIN }
    # ...
    end

    View Slide

  80. Graph::Types::SurveyType = GraphQL::ObjectType.define do
    name 'Survey'
    authorize Authorization::READ
    field :id, !types.ID
    field :title, !types.String
    # ...
    field :questions, function: Graph::Resolvers::Surveys::QuestionsResolver.new
    field :answers, authorize: Authorization::MANAGE_FIELD, fallback: [], functio
    field :canManage, function: Graph::Resolvers::CanResolver.new(Authorization::
    end

    View Slide

  81. Graph::Types::SurveyType = GraphQL::ObjectType.define do
    name 'Survey'
    authorize Authorization::READ
    field :id, !types.ID
    field :title, !types.String
    # ...
    field :questions, function: Graph::Resolvers::Surveys::QuestionsResolver.new
    field :answers, authorize: Authorization::MANAGE_FIELD, fallback: [], functio
    field :canManage, function: Graph::Resolvers::CanResolver.new(Authorization::
    end

    View Slide

  82. class Graph::Mutations::CollectionAddPost < Graph::Resolvers::Mutation
    input :post_id, !types.ID
    node :collection, type: Collection
    authorize: Authorization::MANAGE
    returns Graph::Types::PostType
    def perform
    post = Post.find inputs[:post_id]
    CollectionPosts.add collection, post
    post
    end
    end

    View Slide

  83. class Graph::Mutations::CollectionAddPost < Graph::Resolvers::Mutation
    input :post_id, !types.ID
    node :collection, type: Collection
    authorize: Authorization::MANAGE
    returns Graph::Types::PostType
    def perform
    post = Post.find inputs[:post_id]
    CollectionPosts.add collection, post
    post
    end
    end

    View Slide

  84. Performance

    View Slide

  85. View Slide

  86. View Slide

  87. query {
    posts(date: '2018-05-05') {
    id
    name
    tagline
    votesCount

    commentsCount

    topics {

    id
    name

    }
    isVoted
    }
    }

    View Slide

  88. query {
    posts(date: '2018-05-05') {
    id
    name
    tagline
    votesCount

    commentsCount

    topics {

    id
    name

    }
    isVoted
    }
    }

    View Slide


  89. posts(date: '2018-05-05') {
    id

    topics {

    id

    }
    isVoted
    }
    [{
    id: 1,
    topics: [{ id: 1 }],
    isVoted: true
    }, {
    id: 2,
    topics: [{ id: 2 }],
    isVoted: true
    }, {
    id: 3,
    topics: [{ id: 2 }],
    isVoted: true
    }]

    View Slide

  90. [{
    id: 1,
    topics: [{ id: 1 }],
    isVoted: true
    }, {
    id: 2,
    topics: [{ id: 2 }],
    isVoted: true
    }, {
    id: 3,
    topics: [{ id: 2 }],
    isVoted: true
    }]
    SELECT * FROM posts WHERE DATE(featured_at) = {date}

    SELECT * FROM topics JOIN post_topics WHERE post_id IN {post_id}
    SELECT * FROM votes WHERE post_id={post.id} and user_id={user_id}
    SELECT * FROM topics JOIN post_topics WHERE post_id IN {post_id}
    SELECT * FROM votes WHERE post_id={post.id} and user_id={user_id}
    SELECT * FROM topics JOIN post_topics WHERE post_id IN {post_id}
    SELECT * FROM votes WHERE post_id={post.id} and user_id={user_id}

    View Slide

  91. View Slide

  92. View Slide

  93. [{
    id: 1,
    topics: [{ id: 1 }],
    isVoted: true
    }, {
    id: 2,
    topics: [{ id: 2 }],
    isVoted: true
    }, {
    id: 3,
    topics: [{ id: 2 }],
    isVoted: true
    }]

    View Slide

  94. [...]

    View Slide

  95. [{
    id: 1,
    topics: [find topics for post 1],
    isVoted: [is post 1 voted]
    }, ...]

    View Slide

  96. [{
    id: 1,
    topics: [find topics for post 1],
    isVoted: [is post 1 voted]
    }, {
    id: 2,
    topics: [find topics for post 2],
    isVoted: [is post 2 voted]
    }, ...]

    View Slide

  97. [{
    id: 1,
    topics: [find topics for post 1],
    isVoted: [is post 1 voted]
    }, {
    id: 2,
    topics: [find topics for post 2],
    isVoted: [is post 2 voted]
    }, {
    id: 3,
    topics: [find topics for post 3],
    isVoted: [is post 3 voted]
    }]
    [find topics for post 1, 2, 3]
    [is post 1, 2, 3 voted]

    View Slide

  98. [{
    id: 1,
    topics: [find topics for post 1],
    isVoted: [is post 1 voted]
    }, {
    id: 2,
    topics: [find topics for post 2],
    isVoted: [is post 2 voted]
    }, {
    id: 3,
    topics: [find topics for post 3],
    isVoted: [is post 3 voted]
    }]
    SELECT * FROM topics JOIN post_topics WHERE post_id IN {post_ids}
    SELECT * FROM votes WHERE post_id IN {post_ids} and user_id={user_id}

    View Slide

  99. SELECT * FROM topics JOIN post_topics WHERE post_id IN {post_ids}
    SELECT * FROM votes WHERE post_id IN {post_ids} and user_id={user_id}
    [{
    id: 1,
    topics: [{ id: 1 }],
    isVoted: true
    }, {
    id: 2,
    topics: [{ id: 2 }],
    isVoted: true
    }, {
    id: 3,
    topics: [{ id: 2 }],
    isVoted: true
    }]

    View Slide

  100. gem "graphql-batch"

    View Slide

  101. Graph::Schema = GraphQL::Schema.define do
    # ...
    use GraphQL::Batch
    # ...
    end

    View Slide

  102. query {
    posts(date: '2018-05-05') {
    id
    name
    tagline
    votesCount
    commentsCount

    topics {
    id
    name
    }
    isVoted
    }
    }

    View Slide

  103. query {
    posts(date: '2018-05-05') {
    id
    name
    tagline
    votesCount
    commentsCount

    topics {
    id
    name
    }
    isVoted
    }
    }

    View Slide

  104. class Graph::Resolvers::Posts::TopicsResolver < GraphQL::Function
    type !types[!Graph::Types::TopicType]
    def call(post, _args, _ctx)
    TopicsLoader.for.load(post)
    end
    class TopicsLoader < GraphQL::Batch::Loader
    def perform(posts)
    ::ActiveRecord::Associations::Preloader.new.preload(posts, :topics)
    posts.each do |post|
    fulfill post, post.topics
    end
    end
    end
    end

    View Slide

  105. Graph::Types::PostType = GraphQL::ObjectType.define do
    name 'Post'
    field :id, !types.ID
    field :name, !types.String
    field :tagline, !types.String
    field :topics, function: Graph::Resolvers::Posts::TopicsResolver.new
    end

    View Slide

  106. Graph::Types::PostType = GraphQL::ObjectType.define do
    name 'Post'
    field :id, !types.ID
    field :name, !types.String
    field :tagline, !types.String
    field :topics, function: Graph::Resolvers::Posts::TopicsResolver.new
    end

    View Slide

  107. Graph::Types::PostType = GraphQL::ObjectType.define do
    name 'Post'
    field :id, !types.ID
    field :name, !types.String
    field :tagline, !types.String
    field :topics, function: Graph::Resolvers::Posts::TopicsResolver.new
    end

    View Slide

  108. https://github.com/Shopify/graphql-batch/blob/master/examples/association_loader.rb
    https://gist.github.com/RStankov/48070003a31d71a66f57a237e27d5865

    View Slide

  109. Graph::Types::PostType = GraphQL::ObjectType.define do
    name 'Post'
    field :id, !types.ID
    field :name, !types.String
    field :tagline, !types.String
    field :topics, function: Graph::Resolvers::AssociationResolver.new(:topics),
    end

    View Slide

  110. query {
    posts(date: '2018-05-05') {
    id
    name
    tagline
    votesCount
    commentsCount

    topics {
    id
    name
    }
    isVoted
    }
    }

    View Slide

  111. query {
    posts(date: '2018-05-05') {
    id
    name
    tagline
    votesCount
    commentsCount

    topics {
    id
    name
    }
    isVoted
    }
    }

    View Slide

  112. class Graph::Resolvers::Posts::HasVotedResolver < GraphQL::Function
    type !types.Boolean
    def call(post, _args, ctx)
    return false unless ctx[:current_user]
    VotesLoader.for(ctx[:current_user]).load(post)
    end
    class VotesLoader < GraphQL::Batch::Loader
    def initialize(user)
    @user = user
    end
    def perform(posts)
    voted_posts_ids = @user.votes.where(post_id: posts.map(&:id)).pluck(:post
    posts.each do |post|
    fulfill post, voted_posts_ids.include?(post.id)
    end
    end
    end
    end

    View Slide

  113. Graph::Types::PostType = GraphQL::ObjectType.define do
    name 'Post'
    field :id, !types.ID
    field :name, !types.String
    field :tagline, !types.String
    field :topics, function: Graph::Resolvers::Posts::TopicsResolver.new

    field :isVoted, function: Graph::Resolvers::Posts::HasVotedResolver.new
    end

    View Slide

  114. Graph::Types::PostType = GraphQL::ObjectType.define do
    name 'Post'
    field :id, !types.ID
    field :name, !types.String
    field :tagline, !types.String
    field :topics, function: Graph::Resolvers::Posts::TopicsResolver.new

    field :isVoted, function: Graph::Resolvers::Posts::HasVotedResolver.new
    end

    View Slide

  115. Conclusion

    View Slide

  116. View Slide

  117. View Slide

  118. Thanks *

    View Slide

  119. https://speakerdeck.com/rstankov/one-year-graphql-in-production

    View Slide