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

Avatar for Radoslav Stankov

Radoslav Stankov

May 23, 2018
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. early 2014 jQuery spaghetti October 2014 Backbone February 2015 React

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

    & Rails May 2015 custom Flux December 2015 Redux January 2016 React-Router
  3. 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
  4. 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
  5. 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
  6. February 2015 React & Rails May 2015 custom Flux December

    2015 Redux January 2016 React-Router April 2016 Redux Ducks Febuary 2017 GraphQL
  7. 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
  8. POST /graphql query { posts(date: '2018-05-05') { id name tagline

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

    votesCount commentsCount topics { id name } isVoted } }
  10. 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 } }
  11. 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 } }
  12. 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 } }
  13. 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
  14. 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
  15. " 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)
  16. " 51 root query fields # 136 types $ 132

    mutations & 130 resolvers ( 7 developers Stats
  17. 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
  18. 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
  19. 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
  20. 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
  21. Graph::Query = GraphQL::ObjectType.define do name 'Query' field :viewer, Graph::Types::ViewerType do

    resolve ->(_obj, _args, ctx) { ctx[:current_user] } end # ... end
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 
 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 }]
  30. [{ 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}
  31. [{ id: 1, topics: [{ id: 1 }], isVoted: true

    }, { id: 2, topics: [{ id: 2 }], isVoted: true }, { id: 3, topics: [{ id: 2 }], isVoted: true }]
  32. [{ 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] }, ...]
  33. [{ 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]
  34. [{ 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}
  35. 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 }]
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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