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

3 Years GraphQL at Product Hunt

3 Years GraphQL at Product Hunt

Radoslav Stankov

February 21, 2020
Tweet

More Decks by Radoslav Stankov

Other Decks in Programming

Transcript

  1. Three+ Years GraphQL
    in
    Product Hunt
    Radoslav Stankov 21/02/2020

    View Slide

  2. View Slide

  3. Radoslav Stankov
    @rstankov
    blog.rstankov.com

    twitter.com/rstankov

    github.com/rstankov

    speakerdeck.com/rstankov

    View Slide

  4. View Slide

  5. View Slide

  6. 9 MAY | SOFIA | 2020 EDITON

    View Slide

  7. View Slide

  8. https://speakerdeck.com/rstankov

    View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. 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. April 2019 Your Stack

    View Slide

  17. View Slide

  18. View Slide

  19. GraphQL First

    View Slide

  20. Have good defaults
    Have good code organization
    Isolate dependencies
    Extensibility and reusability
    Make common operations easy

    View Slide

  21. Have good defaults
    Have good code organization
    Isolate dependencies
    Extensibility and reusability
    Make common operations easy

    View Slide

  22. Frontend Stack

    View Slide

  23. Backend Stack

    View Slide

  24. View Slide

  25. GraphQL
    GraphQL
    (ruby)
    Apollo
    React
    Ruby
    on
    Rails
    Business
    Logic
    Business
    Logic
    frontend backend

    View Slide

  26. View Slide

  27. View Slide

  28. GraphQL
    GraphQL
    (ruby)
    Apollo
    React
    Ruby
    on
    Rails
    Business
    Logic
    Business
    Logic
    frontend backend
    Custom
    Utilities

    View Slide

  29. Library Your Utilities Application

    View Slide

  30. https://www.apollographql.com/

    View Slide

  31. https://relay.dev

    View Slide

  32. Library
    Specification for
    how to design you GraphQL
    schema
    https://relay.dev

    View Slide

  33. Library
    Specification for
    how to design you GraphQL
    schema
    https://relay.dev/docs/en/graphql-server-specification.html

    View Slide

  34. Node interface
    Connections
    Mutations
    GraphQL Relay Specification
    https://relay.dev/docs/en/graphql-server-specification.html

    View Slide

  35. Node interface
    Connections
    Mutations
    GraphQL Relay Specification
    https://relay.dev/docs/en/graphql-server-specification.html

    View Slide

  36. Connections
    Mutations
    GraphQL Relay Specification
    https://relay.dev/docs/en/graphql-server-specification.html

    View Slide

  37. Connections
    Mutations
    GraphQL Relay Specification
    https://relay.dev/docs/en/graphql-server-specification.html

    View Slide

  38. Connections
    query Name($cursor: String) {
    someField(first: 20, after: $cursor) {

    edges {
    cursor

    node {

    ...Node

    }

    }

    pageInfo {

    hasNextPage
    hasPreviousPage
    startCursor
    endCursor

    }
    }
    }
    https://relay.dev/docs/en/graphql-server-specification#connections

    View Slide

  39. Connections
    query Name($cursor: String) {
    someField(last: 20, before: $cursor) {

    edges {
    cursor

    node {

    ...Node

    }

    }

    pageInfo {

    hasNextPage
    hasPreviousPage
    startCursor
    endCursor

    }
    }
    }
    https://relay.dev/docs/en/graphql-server-specification#connections

    View Slide

  40. Connections + Total Count
    query Name($cursor: String) {
    someField(first: 20, after: $cursor) {

    totalCount

    edges {
    cursor

    node {

    ...Node

    }

    }

    pageInfo {

    hasNextPage
    hasPreviousPage
    startCursor
    endCursor

    }
    }
    }

    View Slide

  41. View Slide

  42. import HomepageSidebarFragment from './Sidebar/Fragment';
    import StackCardFragment from '~/components/StackCard/Fragment';
    import gql from 'graphql-tag';
    export default gql`
    query HomePage($cursor: String, $loadMore: Boolean!) {
    homepageFeed(first: 20, after: $cursor) {
    edges {
    node {
    id
    ...StackCardFragment
    }
    }
    pageInfo {
    endCursor
    hasNextPage
    }
    }
    ...@skip(if: $loadMore) {
    ...HomepageSidebarFragment
    }

    }
    ${StackCardFragment}
    ${HomepageSidebarFragment}
    `;

    View Slide

  43. import HomepageSidebarFragment from './Sidebar/Fragment';
    import StackCardFragment from '~/components/StackCard/Fragment';
    import gql from 'graphql-tag';
    export default gql`
    query HomePage($cursor: String, $loadMore: Boolean!) {
    homepageFeed(first: 20, after: $cursor) {
    edges {
    node {
    id
    ...StackCardFragment
    }
    }
    pageInfo {
    endCursor
    hasNextPage
    }
    }
    ...@skip(if: $loadMore) {
    ...HomepageSidebarFragment
    }

    }
    ${StackCardFragment}
    ${HomepageSidebarFragment}
    `;

    View Slide

  44. import HomeSidebarFragment from './Sidebar/Fragment';
    import StackCardFragment from '~/components/StackCard/Fragment';
    import gql from 'graphql-tag';
    export default gql`
    query HomePage($cursor: String, $loadMore: Boolean!) {
    homepageFeed(first: 20, after: $cursor) {
    edges {
    node {
    id
    ...StackCardFragment
    }
    }
    pageInfo {
    endCursor
    hasNextPage
    }
    }
    ...@skip(if: $loadMore) {
    ...HomepageSidebarFragment
    }

    }
    ${StackCardFragment}
    ${HomeSidebarFragment}
    `;

    View Slide

  45. export default createPage({
    query: QUERY,
    queryVariables: { cursor: null, loadMore: false },
    title: 'YourStack',
    titleNoPrefix: true,
    component({ data, fetchMore }) {
    return (
    }>
    connection={data.homepageFeed}
    connectionPath="homepageFeed"
    fetchMore={fetchMore}>
    {(use, i) => }


    );
    },
    });

    View Slide

  46. wraps Apollo query
    handles page state (loading, loaded, error)
    handles errors
    handles authentication
    handles authorization
    handles SEO tags
    handles feature flags
    type safe
    createPage
    createPage({
    component:
    query:
    queryVariables:
    requireLogin:
    requirePermissions:
    requireFeature:
    tags:
    title:
    titleNoPrefix:
    );

    View Slide

  47. export default createPage({
    query: QUERY,
    queryVariables: { cursor: null, loadMore: false },
    title: 'YourStack',
    titleNoPrefix: true,
    component({ data, fetchMore }) {
    return (
    }>
    connection={data.homepageFeed}
    connectionPath="homepageFeed"
    fetchMore={fetchMore}>
    {(use, i) => }


    );
    },
    });

    View Slide

  48. knows how to map connection
    knows how to read "pageInfo"
    fetches more pages
    type safe
    InfiniteScroll
    connection={...}
    connectionPath={...}
    cursorName={...}
    fetchMore={...}
    variables={...}>
    {(item) => ...}

    View Slide

  49. How many SQL queries are performed here?

    View Slide

  50. How many SQL queries are performed here?
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1

    View Slide

  51. How many SQL queries are performed here?
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1 + N * 4

    View Slide

  52. How many SQL queries are performed here?
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1 + N * 4

    View Slide

  53. View Slide

  54. [{
    id: 1,
    isLiked: true
    }, {
    id: 2,
    isLiked: false
    }, {
    id: 3,
    isLiked: false
    }]

    View Slide

  55. [{
    id: 1,
    isLiked: "SELECT * FROM likes WHERE stack_item_id=1 and user_id={user_id}"
    }, {
    id: 2,
    isLiked: "SELECT * FROM likes WHERE stack_item_id=2 and user_id={user_id}"
    }, {
    id: 3,
    isLiked: "SELECT * FROM likes WHERE stack_item_id=3 and user_id={user_id}"
    }]

    View Slide

  56. gem "graphql-batch"

    View Slide

  57. SELECT * FROM likes WHERE stack_item_id IN {ids} and user_id={user_id}
    [{
    id: 1,
    isLiked: [Promise to check(1)]
    }, {
    id: 2,
    isLiked: [Promise to check(2)]
    }, {
    id: 3,
    isLiked: [Promise to check(3)]
    }]

    View Slide

  58. module Types
    class StackItemType < BaseRecord
    field :note, String, null: false
    field :is_liked, resolve: Graph::Resolvers::IsLiked
    field :profile, ProfileType, null: false

    field :product, ProductType, null: false
    end
    end

    View Slide

  59. module Types
    class StackItemType < BaseRecord
    field :note, String, null: false
    field :is_liked, resolve: Graph::Resolvers::IsLiked
    field :profile, ProfileType, null: false

    field :product, ProductType, null: false
    end
    end

    View Slide

  60. class Graph::Resolvers::IsLiked < 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(items)
    ids = @user.likes.where(stack_item_id: items.map(&:id)).pluck(:stack_item_id)
    items.each do |items|
    fulfill item, ids.include?(item.id)
    end
    end
    end
    end

    View Slide

  61. class Graph::Resolvers::IsLiked < 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(items)
    ids = @user.likes.where(stack_item_id: items.map(&:id)).pluck(:stack_item_id)
    items.each do |items|
    fulfill item, ids.include?(item.id)
    end
    end
    end
    end

    View Slide

  62. How many SQL queries are performed here?
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1 + N * 4

    View Slide

  63. View Slide

  64. How many SQL queries are performed here?
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1 + N * 3

    View Slide

  65. module Types
    class StackItemType < BaseRecord
    field :note, String, null: false
    field :is_liked, resolve: Graph::Resolvers::IsLiked
    field :profile, ProfileType, null: false

    field :product, ProductType, null: false
    end
    end

    View Slide

  66. module Types
    class StackItemType < BaseRecord
    field :note, String, null: false
    field :is_liked, resolve: Graph::Resolvers::IsLiked
    field :profile, ProfileType, null: false

    field :product, ProductType, null: false
    end
    end

    View Slide

  67. module Types
    class StackItemType < BaseRecord
    field :note, String, null: false
    field :is_liked, resolve: Graph::Resolvers::IsLiked
    field :profile, ProfileType, null: false

    field :product, ProductType, null: false
    end
    end
    SELECT * FROM product WHERE id={id}

    View Slide

  68. module Types
    class StackItemType < BaseRecord
    field :note, String, null: false
    field :is_liked, resolve: Graph::Resolvers::IsLiked
    field :profile, ProfileType, null: false

    field :product, ProductType, null: false
    end
    end
    SELECT * FROM product WHERE id={id}

    View Slide

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

    View Slide

  70. module Types
    class StackItemType < BaseRecord
    field :note, String, null: false
    field :is_liked, resolve: Graph::Resolvers::IsLiked
    field :profile, ProfileType, resolve: Graph::Resolvers::Association.new(:profil
    field :product, ProductType, resolve: Graph::Resolvers::Association.new(:produc
    end
    end

    View Slide

  71. module Types
    class StackItemType < BaseRecord
    field :note, String, null: false
    field :is_liked, resolve: Graph::Resolvers::IsLiked

    association :profile, ProfileType

    association :product, ProductType
    end
    end

    View Slide

  72. How many SQL queries are performed here?

    View Slide

  73. How many SQL queries are performed here?
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1
    1 + N * 3

    View Slide

  74. How many SQL queries are performed here?
    1
    1 ‼

    View Slide

  75. http://blog.rstankov.com/dealing-with-n-1-with-graphql-part-1
    http://blog.rstankov.com/dealing-with-n-1-in-graphql-part-2

    View Slide

  76. Connections
    Mutations
    GraphQL Relay Specification
    https://relay.dev/docs/en/graphql-server-specification.html

    View Slide

  77. View Slide

  78. mutation ProductLikeCreate($input: ProductLikeCreateInput!) {
    productLikeCreate(input: $input) {
    node {
    id
    ...LikeButtonFragment
    }
    errors {
    name
    messages
    }
    }
    }
    Mutations
    Mutations
    https://relay.dev/docs/en/graphql-server-specification.html#mutations

    View Slide

  79. mutation ProductLikeCreate($input: ProductLikeCreateInput!) {
    productLikeCreate(input: $input) {
    node {
    id
    ...LikeButtonFragment
    }
    errors {
    name
    messages
    }
    }
    }
    Mutations
    Mutations
    https://relay.dev/docs/en/graphql-server-specification.html#mutations

    View Slide

  80. mutation ProductLikeCreate($input: ProductLikeCreateInput!) {
    productLikeCreate(input: $input) {
    node {
    id
    ...LikeButtonFragment
    }
    errors {
    name
    messages
    }
    }
    }
    Mutations
    Mutations

    View Slide

  81. mutation ProductLikeCreate($input: ProductLikeCreateInput!) {
    productLikeCreate(input: $input) {
    node {
    id
    ...LikeButtonFragment
    }
    errors {
    name
    messages
    }
    }
    }
    Mutations
    Mutations

    View Slide

  82. [namespace][object][action]
    Mutation Naming Convention

    View Slide

  83. [namespace][object][action]


    BookmarkCreate

    BookmarkDestroy

    ProductLikeCreate

    ProductLikeDestroy
    TeamInviteAccept
    TeamInviteCreate
    TeamInviteDecline

    UserSettingsUpdate
    Mutation Naming Convention

    View Slide

  84. module Mutations
    class ProductLikeCreate < BaseMutation
    argument_record :product, Product
    authorize
    returns Types::ProductType
    def perform(product:)
    Like.find_or_create_by!(
    product: product,
    current_user: current_user
    )
    end
    end
    end
    Mutations::BaseMutation

    View Slide

  85. Mutations::BaseMutation
    module Mutations
    class ProductUpdate < BaseMutation
    argument_record :product, Product, authorize: :manage
    argument :name, String, required: true

    argument :tagline, String, required: true
    argument :topic_ids, String, required: true

    returns Types::ProductType
    def perform(product:, **attributes)
    Products::Form.submit(

    current_user: current_user,
    product: product,
    attributes: attributes
    )
    end
    end
    end

    View Slide

  86. Mutations::BaseMutation
    ensures proper mutation format - node and errors
    handles loading of records
    handles authenication
    handles authorization
    handles model validations
    handles error case
    handles server validation

    View Slide

  87. View Slide

  88. import gql from 'graphql-tag';
    export default gql`
    fragment ProductLikeButtonFragment on Product {
    id
    isLiked
    likesCount
    }
    `;
    ~/components/ProductLikeButton/Fragment.ts

    View Slide

  89. import FRAGMENT from './Fragment';
    export const CREATE_MUTATION = gql`
    mutation ProdutLikeCreate($input: ProductLikeCreateInput!) {
    productLikeCreate(input: $input) {
    node {
    ...ProductLikeButtonFragment
    }
    }
    }
    ${FRAGMENT}
    `;
    export const DESTROY_MUTATION = gql`
    mutation ProductLikeDestroy($input: ProductLikeDestroyInput!) {
    productLikeDestroy(input: $input) {
    node {
    ...ProductLikeButtonFragment
    }
    }
    }
    ${FRAGMENT}
    `;
    ~/components/ProductLikeButton/Mutation.ts

    View Slide

  90. interface IProps {
    product: ProductLikeButtonFragment;
    }
    export default function ProductLikeButton({ product }: IProps) {
    return (
    className={styles.likeButton}
    requireLogin="like"
    mutation={product.isLiked ? DESTROY_MUTATION : CREATE_MUTATION}
    input={{ productId: product.id }}
    optimisticResponse={optimisticResponseFor(product)}>
    {product.likesCount}

    );
    }
    ~/components/ProductLikeButton/index.tsx

    View Slide

  91. Button.Mutation
    wraps our internal Button component
    wraps Apollo mutation
    handles authentication
    handles confirms
    handles mutation responses
    className={...}
    mutation={...}
    mutationInput={...}

    onMutate={...}
    updateCache={...}
    refetchQueries={...}
    optimisicResponse={...}

    requireLogin={...}

    confirm={...}
    />

    View Slide

  92. View Slide

  93. View Slide










  94. View Slide

  95. wraps a form library (FinalForm)
    wraps Apollo mutation
    ensures consistent form look
    support for custom inputs
    executes mutation
    handles success case
    handles error case
    handles cache updates
    className={...}

    initialValues={...}

    mutation={...}
    mutationInput={...}

    onMutate={...}
    updateCache={...}
    refetchQueries={...}
    optimisicResponse={...}>



    Form.Mutation

    View Slide

  96. Conclusion

    View Slide

  97. GraphQL can help you write
    both cleaner frontend and backend code
    integrate your backend and frontend
    don't be afraid to build custom tools
    solve core concerns on top level
    naming conventions
    page lifecycle
    performance
    validations
    auth
    [...]

    View Slide

  98. View Slide

  99. View Slide

  100. https://speakerdeck.com/rstankov
    Thanks

    View Slide