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

Two+ Years GraphQL at ProductHunt

Two+ Years GraphQL at ProductHunt

Experience and learnings from running GraphQL for two-plus years in production at Product Hunt.

Main focus is on the Ruby on Rails backend. The talk explains how GraphQL is implemented in Product Hunt. There are a lot of code samples streight out from the real codebase.

Gems mentioned in the talk:

- graphiql-rails
- graphql
- graphql-cache
- graphql-cache
- search_object
- search_object_graphql

Links from the talk:

- https://www.apollographql.com/
- https://facebook.github.io/relay/
- https://facebook.github.io/relay/docs/graphql-relay-specification.html
- https://facebook.github.io/relay/docs/graphql-connections.html
- https://facebook.github.io/relay/docs/graphql-mutations.html
- https://github.com/Shopify/graphql-batch/blob/master/examples/association_loader.rb
- https://gist.github.com/RStankov/48070003a31d71a66f57a237e27d5865

Radoslav Stankov

June 10, 2019
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. Two Years GraphQL
    in
    Production
    Radoslav Stankov 10/06/2018

    View Slide

  2. Radoslav Stankov
    @rstankov

    blog.rstankov.com

    github.com/rstankov

    twitter.com/rstankov

    View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. https://speakerdeck.com/rstankov

    View Slide

  8. Agenda

    View Slide

  9. ! Basics
    " Structure
    # Authentication & Authorization
    $ Performance

    View Slide

  10. Basics

    View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. View Slide

  15. posts {
    id
    name
    tagline
    votesCount
    commentsCount
    thumbnailUrl
    state
    isVoted
    }

    View Slide

  16. posts(date: "2019-06-10") {
    id
    name
    tagline
    votesCount
    commentsCount
    thumbnailUrl
    state
    isVoted
    }

    View Slide

  17. query {
    posts(date: "2019-06-10") {
    id
    name
    tagline
    votesCount
    commentsCount
    thumbnailUrl
    state
    isVoted
    }
    }
    POST /graphql

    View Slide

  18. POST /graphql
    {
    "data": {
    "posts": {
    "id": "1",
    "name": "Playdate",
    "tagline": "A new handheld gaming system
    "votesCount": 1352,
    "commentsCount": 25,

    "thumbnailUrl": "https://example.com/C22
    "state": "pre_launch",
    "isVoted": true,
    }
    }
    }

    query {
    posts(date: "2019-06-10") {
    id
    name
    tagline
    votesCount
    commentsCount
    thumbnailUrl
    state
    isVoted
    }
    }

    View Slide

  19. View Slide

  20. productRequests(first: 1, order: FEATURED)
    { ... }
    goalsWorkedToday(first: 12)
    { .,. }
    radioEpisodes(first: 1)
    { ... }
    newsletters(first: 1)
    { ... }
    stories(first: 3)
    { ... }
    posts(first: 1, order: FEATURED, date: $date)
    { ... }
    viewer { .. }

    View Slide

  21. query Homepage {
    viewer { .. }
    productRequests(first: 1, order: FEATURED) { ... }
    goalsWorkedToday(first: 12) { .. }
    radioEpisodes(first: 1) { ... }
    posts(first: 1, order: FEATURED, date: $date) { ... }
    newsletters(first: 1) { ... }
    stories(first: 3) { ... }
    ...
    }
    POST /graphql

    View Slide

  22. View Slide

  23. View Slide

  24. gem "graphql"

    View Slide

  25. query {
    posts(date: "2019-06-10") {
    id
    name
    tagline
    }
    }

    View Slide

  26. module Graph::Types
    class PostType < BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :tagline, String, null: false
    end
    end
    query {
    posts(date: "2019-06-10") {
    id
    name
    tagline
    }
    }

    View Slide

  27. module Graph::Types
    class PostType < BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :tagline, String, null: false
    end
    end
    query {
    posts(date: "2019-06-10") {
    id
    name
    tagline
    }
    }

    View Slide

  28. module Graph::Types
    class BaseRecord < BaseObject
    field :id, ID, null: false
    end
    end
    query {
    posts(date: "2019-06-10") {
    id
    name
    tagline
    }
    }

    View Slide

  29. module Graph::Types
    class PostType < BaseRecord
    field :name, String, null: false
    field :tagline, String, null: false
    end
    end
    query {
    posts(date: "2019-06-10") {
    id
    name
    tagline
    }
    }

    View Slide

  30. module Graph::Types
    class PostType < BaseRecord
    field :name, String, null: false
    field :tagline, String, null: false
    field :votes_count, Integer, null: false
    end
    end
    query {
    posts(date: "2019-06-10") {
    id
    name
    tagline
    votesCount
    }
    }

    View Slide

  31. module Graph::Types
    class QueryType < BaseObject
    field :posts, [PostType], null: true do
    argument :date, String, required: true
    end
    def posts(date:)
    Post.for_date(date)
    end
    end
    end
    query {
    posts(date: "2019-06-10") {
    id
    name
    tagline

    votesCount
    }
    }

    View Slide

  32. class Graph::Schema < GraphQL::Schema
    query Graph::Types::QueryType
    # ...
    end
    query {
    posts(date: "2019-06-10") {
    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. View Slide

  36. View Slide

  37. View Slide

  38. View Slide

  39. Structure

    View Slide

  40. % 83 fields in query type
    & 197 types
    ' 190 mutations
    # 172 resolvers
    ( 6 developers
    Stats

    View Slide

  41. class Graph::Query < Graph::Types::BaseObject
    field :posts, [PostType], null: true do
    argument :date, String, required: true
    end
    def posts(date:)
    Post.for_date(date)
    end
    end

    View Slide

  42. class Graph::Query < Graph::Types::BaseObject
    field :posts, [PostType], null: true do
    argument :date, String, required: false
    argument :name, String, required: false
    end
    def posts(date: nil, name: nil)
    scope = Post.featured
    scope = scope.for_date(date) if date
    scope = scope.for_name(name) if name
    scope
    end
    end

    View Slide

  43. class Graph::Query < Graph::Types::BaseObject
    field :posts, function: Graph::Resolvers::Posts::Search
    end

    View Slide

  44. gem 'search_object'
    gem 'search_object_graphql'

    View Slide

  45. class Graph::Resolvers::Posts::Search < Graph::Resolvers::BaseSearch
    scope { Post.featured }
    type [Graph::Types::PostType]
    option :date, type: types.Int, with: :apply_date_filter
    option :name, type: types.String, with: :apply_name_filter
    def apply_date_filter(scope, value)
    scope.for_date(value)
    end
    def apply_query_filter(scope, value)
    scope.for_name(value)
    end
    end

    View Slide

  46. https://www.apollographql.com/

    View Slide

  47. https://facebook.github.io/relay/

    View Slide

  48. https://facebook.github.io/relay/
    Library
    Specification for
    how to design you
    GraphQL schema

    View Slide

  49. https://facebook.github.io/relay/
    Library
    Specification for
    how to design you
    GraphQL schema

    View Slide

  50. % Node interface
    & Connections
    ' Mutations
    GraphQL Relay Specification
    https://facebook.github.io/relay/docs/graphql-relay-specification.html

    View Slide

  51. ) Node interface
    & Connections
    ' Mutations
    GraphQL Relay Specification
    https://facebook.github.io/relay/docs/graphql-relay-specification.html

    View Slide

  52. & Connections
    ' Mutations
    GraphQL Relay Specification
    https://facebook.github.io/relay/docs/graphql-relay-specification.html

    View Slide

  53. https://facebook.github.io/relay/docs/graphql-connections.html
    Connections
    query Homepage {
    posts {
    id
    name
    tagline
    }
    }

    View Slide

  54. https://facebook.github.io/relay/docs/graphql-connections.html
    Connections
    query Homepage($cursor: String) {
    posts(first: 10, after: $cursor) {

    edges {

    node {

    id
    name
    tagline

    }

    }

    pageInfo {

    hasNextPage

    }
    }
    }

    View Slide

  55. class Graph::Resolvers::Posts::Search < Graph::Resolvers::BaseSearch
    scope { Post.featured }
    type Graph::Types::PostType.connection_type
    option :date, type: types.Int, with: :apply_date_filter
    option :query, type: types.String, with: :apply_query_filter
    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

  56. & Connections
    ' Mutations
    GraphQL Relay Specification
    https://facebook.github.io/relay/docs/graphql-relay-specification.html

    View Slide

  57. mutation CollectionPostAdd($input: CollectionPostAddInput!) {
    response: collectionPostAdd(input: $input) {
    node {
    ...SomeFragment
    }
    errors {
    name
    messages
    }
    }
    }
    Mutations

    View Slide

  58. https://facebook.github.io/relay/docs/graphql-connections.html
    Mutations
    https://facebook.github.io/relay/docs/graphql-mutations.html
    mutation CollectionPostAdd($input: CollectionPostAddInput!) {
    response: collectionPostAdd(input: $input) {
    node {
    ...SomeFragment
    }
    errors {
    name
    messages
    }
    }
    }

    View Slide

  59. Mutations
    mutation CollectionPostAdd($input: CollectionPostAddInput!) {
    response: collectionPostAdd(input: $input) {
    node {
    ...SomeFragment
    }
    errors {
    name
    messages
    }
    }
    }

    View Slide

  60. Mutations
    mutation CollectionPostAdd($input: CollectionPostAddInput!) {
    response: collectionPostAdd(input: $input) {
    node {
    ...SomeFragment
    }
    errors {
    name
    messages
    }
    }
    }

    View Slide

  61. Mutations
    mutation CollectionPostAdd($input: CollectionPostAddInput!) {
    response: collectionPostAdd(input: $input) {
    node {
    ...SomeFragment
    }
    errors {
    name
    messages
    }
    }
    }

    View Slide

  62. Mutations
    mutation CollectionPostAdd($input: CollectionPostAddInput!) {
    response: collectionPostAdd(input: $input) {
    node {
    ...SomeFragment
    }
    errors {
    name
    messages
    }
    }
    }

    View Slide

  63. [module][object][action]
    Mutations

    View Slide

  64. [module][object][action]


    CommentCreate

    CollectionPostAdd
    PostVoteCreate
    PostVoteDestroy
    PostSubmissionCreate
    ShipContactCreate

    ShipContactDestroy
    Mutations

    View Slide

  65. [module][object][action]


    CommentCreate
    CollectionPostAdd
    PostVoteCreate
    PostVoteDestroy
    PostSubmissionCreate
    ShipContactCreate

    ShipContactDestroy
    Mutations

    View Slide

  66. class Graph::Schema < GraphQL::Schema
    mutation Graph::Types::MutationType
    # ...
    end

    View Slide

  67. module Graph::Types
    class MutationType < Types::BaseObject
    def self.mutation_field(mutation)
    field mutation.name.demodulize.underscore, mutation: mutation
    end
    mutation_field Graph::Mutations::CommentCreate

    mutation_field Graph::Mutations::CollectionPostAdd
    mutation_field Graph::Mutations::PostVoteCreate
    mutation_field Graph::Mutations::PostVoteDestroy
    mutation_field Graph::Mutations::PostSubmissionCreate
    mutation_field Graph::Mutations::ShipContactCreate
    mutation_field Graph::Mutations::ShipContactDestroy
    # ...
    end
    end

    View Slide

  68. module Graph::Mutations
    class CollectionPostAdd < BaseMutation
    argument_record :collection, Collection
    argument_record :post, Post
    argument :description, String, required: false
    returns Graph::Types::CollectionPostType
    def perform(collection:, post:, description: nil)
    Collections.collect(
    current_user: current_user,
    collection: collection,
    post: post,
    description: description
    )
    end
    end
    end

    View Slide

  69. module Graph::Mutations
    class CollectionPostAdd < BaseMutation
    argument_record :collection, Collection
    argument_record :post, Post
    argument :description, String, required: false
    returns Graph::Types::CollectionPostType
    def perform(collection:, post:, description: nil)
    Collections.collect(
    current_user: current_user,
    collection: collection,
    post: post,
    description: description
    )
    end
    end
    end

    View Slide

  70. Authentication & Authorization

    View Slide

  71. View Slide

  72. View Slide

  73. View Slide

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

  75. module Graph::Query < Graph::Types::BaseObject
    field :viewer, ViewerType, null: true
    def viewer
    context.current_user
    end
    end

    View Slide

  76. query {
    viewer {
    email
    user {
    id
    username
    // ...

    }
    // ...
    }
    }

    View Slide

  77. View Slide

  78. class Graph::Schema < GraphQL::Schema
    # ...
    authorization Graph::Utils::AuthorizationStrategy
    instrument :field, Graph::Utils::FallbackInstrumenter
    # ...
    end

    View Slide

  79. class Graph::Utils::AuthorizationStrategy
    def initialize(context)
    @current_user = context.current_user
    end
    def allowed?(gate, value)
    ApplicationPolicy.can? @current_user, gate.role, value
    end
    end

    View Slide

  80. class Graph::Utils::AuthorizationStrategy
    def initialize(context)
    @current_user = context.current_user
    end
    def allowed?(gate, value)
    ApplicationPolicy.can? @current_user, gate.role, value
    end
    end

    View Slide

  81. module Graph::Types
    class PostType < BaseRecord
    authorize :read
    field :submission, SubmissionType, authorize: :edit, fallback: nil
    field :can_edit, resolve: Graph::Resolvers::Can.for(:edit)
    # ...
    end
    end

    View Slide

  82. module Graph::Types
    class PostType < BaseRecord
    authorize :read
    field :submission, SubmissionType, authorize: :edit, fallback: nil
    field :can_edit, resolve: Graph::Resolvers::Can.for(:edit)
    # ...
    end
    end

    View Slide

  83. module Graph::Types
    class PostType < BaseRecord
    authorize :read
    field :submission, SubmissionType, authorize: :edit, fallback: nil
    field :can_edit, resolve: Graph::Resolvers::Can.for(:edit)
    # ...
    end
    end

    View Slide

  84. module Graph::Mutations
    class CollectionPostAdd < BaseMutation
    argument_record :collection, Collection
    argument_record :post, Post
    argument :description, String, required: false
    authorize :edit, :collection
    returns Graph::Types::CollectionPostType
    def perform(collection:, post:, description: nil)
    Collections.collect(
    current_user: current_user,
    collection: collection,
    post: post,
    description: description
    )
    end
    end
    end

    View Slide

  85. module Graph::Mutations
    class CollectionPostAdd < BaseMutation
    argument_record :collection, Collection
    argument_record :post, Post
    argument :description, String, required: false
    authorize :edit, :collection
    returns Graph::Types::CollectionPostType
    def perform(collection:, post:, description: nil)
    Collections.collect(
    collection: collection,
    post: post,
    description: description
    )
    end
    end
    end

    View Slide

  86. Performance

    View Slide

  87. module Graph::Types
    class UserType < BaseRecord
    field :activity_streak, Integer, null: false
    def activity_streak
    # NOTE: Very, very expensive calculation *
    Users::ActivityStreak.for(object)
    end
    end
    end

    View Slide

  88. gem "graphql-cache"

    View Slide

  89. class Graph::Schema < GraphQL::Schema
    # ...
    use GraphQL::Cache
    # ...
    end

    View Slide

  90. module Graph::Types
    class UserType < BaseRecord
    field :activity_streak, Integer, null: false, cache: true
    def activity_streak
    # NOTE: Very, very expensive calculation *
    Users::ActivityStreak.for(object)
    end
    end
    end

    View Slide

  91. module Graph::Types
    class UserType < BaseRecord
    field :activity_streak, Integer, null: false, cache: true
    def activity_streak
    # NOTE: Very, very expensive calculation
    Users::ActivityStreak.for(object)
    end
    end
    end

    View Slide

  92. View Slide

  93. View Slide

  94. query {
    posts(date: "2019-06-10") {
    id
    name
    tagline
    votesCount
    commentsCount
    thumbnailUUID
    state
    isVoted
    }
    }

    View Slide

  95. query {
    posts(date: "2019-06-10") {
    id
    isVoted
    }
    }
    [{
    id: 1,
    isVoted: true
    }, {
    id: 2,
    isVoted: true
    }, {
    id: 3,
    isVoted: true
    }]

    View Slide

  96. [{
    id: 1,
    isVoted: true
    }, {
    id: 2,
    isVoted: true
    }, {
    id: 3,
    isVoted: true
    }]
    SELECT * FROM posts WHERE DATE(featured_at) = {date}
    SELECT * FROM votes WHERE post_id={post.id} and user_id={user.id}
    SELECT * FROM votes WHERE post_id={post.id} and user_id={user.id}
    SELECT * FROM votes WHERE post_id={post.id} and user_id={user.id}

    View Slide

  97. View Slide

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

    View Slide

  99. [...]

    View Slide

  100. [{
    id: 1,
    isVoted: [is post 1 voted by current user?]
    }, ...]

    View Slide

  101. [{
    id: 1,
    isVoted: [is post 1 voted by current user?]
    }, {
    id: 2,
    isVoted: [is post 2 voted by current user?]
    }, ...]

    View Slide

  102. [{
    id: 1,
    isVoted: [is post 1 voted by current user?]
    }, {
    id: 2,
    isVoted: [is post 2 voted by current user?]
    }, {
    id: 3,
    isVoted: [is post 3 voted by current user?]
    }]
    [is post 1, 2, 3 voted for current user]

    View Slide

  103. [{
    id: 1,
    isVoted: [is post 1 voted?]
    }, {
    id: 2,
    isVoted: [is post 2 voted?]
    }, {
    id: 3,
    isVoted: [is post 3 voted?]
    }]
    SELECT * FROM votes WHERE post_id IN {post_ids} and user_id={user_id}

    View Slide

  104. gem "graphql-batch"

    View Slide

  105. class Graph::Schema < GraphQL::Schema
    # ...
    use GraphQL::Batch
    lazy_resolve Promise, :sync
    # ...
    end

    View Slide

  106. module Graph::Types
    class PostType < BaseRecord
    field :name, String, null: false
    field :tagline, String, null: false
    field :votes_count, Integer, null: false
    field :is_voted, resolve: Graph::Resolvers::Posts::IsVoted
    end
    end

    View Slide

  107. module Graph::Types
    class PostType < BaseRecord
    field :name, String, null: false
    field :tagline, String, null: false
    field :votes_count, Integer, null: false
    field :is_voted, resolve: Graph::Resolvers::Posts::IsVoted
    end
    end

    View Slide

  108. class Graph::Resolvers::Posts::IsVoted < Resolvers::Base
    type Boolean, null: false
    def resolve
    user = context.current_user
    return false if user.nil?
    Loader.for(user).load(object)
    end
    class Loader < GraphQL::Batch::Loader
    def initialize(user)
    @user = user
    end
    def perform(posts)
    voted_ids = @user.votes.where(post_id: posts.map(&:id)).pluck(:post_id)
    posts.each do |post|
    fulfill post, voted_ids.include?(post.id)
    end
    end
    end
    end

    View Slide

  109. class Graph::Resolvers::Posts::IsVoted < Resolvers::Base
    type Boolean, null: false
    def resolve
    user = context.current_user
    return false if user.nil?
    Loader.for(user).load(object)
    end
    class Loader < GraphQL::Batch::Loader
    def initialize(user)
    @user = user
    end
    def perform(posts)
    voted_ids = @user.votes.where(post_id: posts.map(&:id)).pluck(:post_id)
    posts.each do |post|
    fulfill post, voted_ids.include?(post.id)
    end
    end
    end
    end

    View Slide

  110. module Graph::Types
    class PostType < BaseRecord
    field :name, String, null: false
    field :tagline, String, null: false
    field :votes_count, Integer, null: false
    field :is_voted, resolve: Graph::Resolvers::Posts::IsVoted
    field :topics, [TopicType], null: false
    end
    end

    View Slide

  111. module Graph::Types
    class PostType < BaseRecord
    field :name, String, null: false
    field :tagline, String, null: false
    field :votes_count, Integer, null: false
    field :is_voted, resolve: Graph::Resolvers::Posts::IsVoted
    field :topics, [TopicType], null: false
    end
    end
    SELECT *
    FROM topics
    JOIN post_topics
    WHERE post_id IN {post_id}

    View Slide

  112. module Graph::Types
    class PostType < BaseRecord
    field :name, String, null: false
    field :tagline, String, null: false
    field :votes_count, Integer, null: false
    field :is_voted, resolve: Graph::Resolvers::Posts::IsVoted
    field :topics, [TopicType], null: false
    end
    end
    SELECT *
    FROM topics
    JOIN post_topics
    WHERE post_id IN {post_id}
    +

    View Slide

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

    View Slide

  114. module Graph::Types
    class PostType < BaseRecord
    field :name, String, null: false
    field :tagline, String, null: false
    field :votes_count, Integer, null: false
    field :is_voted, resolve: Graph::Resolvers::Posts::IsVoted
    field :topics, function: Graph::Resolvers::Association.new(:topics, TopicsT
    end
    end

    View Slide

  115. module Graph::Types
    class PostType < BaseRecord
    field :name, String, null: false
    field :tagline, String, null: false
    field :votes_count, Integer, null: false
    field :is_voted, resolve: Graph::Resolvers::Posts::IsVoted
    association :topics, [TopicType]
    end
    end

    View Slide

  116. Conclusion

    View Slide

  117. View Slide

  118. Thanks ,

    View Slide

  119. https://speakerdeck.com/rstankov

    View Slide