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

3 Practices about
Service-to-Service GraphQL Ruby Client

qsona
September 13, 2023
450

3 Practices about
Service-to-Service GraphQL Ruby Client

GraphQL Tokyo #21 - Guest talk and lightning talks

qsona

September 13, 2023
Tweet

More Decks by qsona

Transcript

  1. 3 Practices about

    Service-to-Service GraphQL


    Ruby Client
    GraphQL Tokyo #21 - Guest talk and lightning talks
    @qsona

    View Slide

  2. Introduction

    View Slide

  3. whoami
    • @qsona


    • Working at SHE, Inc.


    • Ex- StudySapuri


    • GraphQL / Ruby on Rails / Node.js / Microservices

    View Slide

  4. 4 years ago...
    • Urigo (The Guild) came to Japan, and GraphQL Tokyo was held


    • I talked about Service-to-service GraphQL

    View Slide

  5. My argument:
    • GraphQL is very useful as a service-to-service protocol


    • not only as frontend-to-server protocol


    • REST, gRPC are more popular but GraphQL is better for me


    • Several months later -


    • We decided to build a new product from scratch

    View Slide

  6. GraphQL-based Architecture

    View Slide

  7. Today I'll talk about...

    View Slide

  8. Practice 1 -


    GraphQL Client on Backend Ruby

    View Slide

  9. On web frontend app
    • It's almost mandatory to use some kind of GraphQL client library


    • apollo-client, urql, ...


    • graphql-code-generator

    View Slide

  10. On ruby app
    • Are there any ruby GraphQL client libraries?


    • github/graphql-client


    • Latest update: 1.5 years ago (Not actively maintained)


    • But... are we really need GraphQL client libraries?

    View Slide

  11. In web frontend, GraphQL client library provides:
    • caching fetched data


    • generating static code/types (e.g. TypeScript code / d.ts)


    • generating codes which work with React, etc


    • like `useQuery`, `useFragment`, ...

    View Slide

  12. Does the ruby app need those?
    • caching fetched data => unnecessary


    • Network cost is low


    • It's far more dif
    fi
    cult to cache in backend

    because multiple user's data is mixed


    • generating static code/types => unnecessary


    • Ruby is dynamically-typed and duck-typing language


    • generating codes which work with React, etc => unnecessary

    View Slide

  13. Tips
    • You might not need GraphQL client library


    • Just make a thin wrapper of HTTP client (like Net::HTTP, Faraday, etc)

    View Slide

  14. Practice 2 -


    GraphQL Requests


    in complex command logics

    View Slide

  15. Compare to frontend apps...
    • Backend app often has more complicated domain logics


    • Especially, Command (CUD) operations


    • Sometimes the operations are nested, or executed concurrently

    View Slide

  16. Example
    class FooOperation
    def self.execute!(baz_id)
    ActiveRecord::Base.transaction do
    Foo.create!
    BarOperation.execute!(baz_id)
    end
    end
    end
    class BarOperation
    def self.execute!(baz_id)
    Bar.create!
    # more complicated logics...
    end
    end

    View Slide

  17. FooOperation and BarOperation might need to


    fetch data from other services
    class FooOperation
    QUERY = "query(bazId: $ID!) { baz(id: $bazId) { a } }"
    def self.execute!(baz_id)
    data = client.execute(query: QUERY, variables: { bazId: baz_id })
    ActiveRecord::Base.transaction do
    Foo.create!(a: data.baz.a)
    BarOperation.execute!(baz_id)
    end
    end
    end
    class BarOperation
    QUERY = "query(id: $ID!) { baz(id: $id) { b { c, d } } }"
    def self.execute!(baz_id)
    data = client.execute(query: QUERY, variables: { bazId: baz_id })
    Bar.create!(c: data.baz.b.c, d: data.baz.b.d)
    # more complicated logics...
    end
    end

    View Slide

  18. Problems?
    • Two GraphQL Requests are sent


    • It's mottainai!


    • The second request is sent in the DB transaction block


    • It can make the transaction longer

    View Slide

  19. The two requests should be merged!
    class FooOperation
    QUERY = "query(bazId: $ID!) { baz(id: $bazId) { a, b { c, d } } }"
    def self.execute!(baz_id)
    data = client.execute(query: QUERY, variables: { bazId: baz_id })
    ActiveRecord::Base.transaction do
    Foo.create!(a: data.baz.a)
    BarOperation.execute!(data.baz.b)
    end
    end
    end
    class BarOperation
    def self.execute!(b)
    Bar.create!(c: b.c, d: b.d)
    # more complicated logics...
    end
    end

    View Slide

  20. 💯
    • It becomes simpler!


    • Problems?

    View Slide

  21. Problems
    class FooOperation
    QUERY = "query(bazId: $ID!) { baz(id: $bazId) { a, b
    { c, d } } }"
    def self.execute!(baz_id)
    data = client.execute(query: QUERY, variables:
    { bazId: baz_id })
    ActiveRecord::Base.transaction do
    Foo.create!(a: data.baz.a)
    BarOperation.execute!(data.baz.b)
    end
    end
    end
    class BarOperation
    def self.execute!(b)
    Bar.create!(c: b.c, d: b.d)
    # more complicated logics...
    end
    end
    `FooOperation` class needs
    to know what
    fi
    elds
    `BarOperation` uses


    It violates “Single-
    responsibility principle”

    View Slide

  22. Solution: Fragment colocation

    View Slide

  23. Solution: Fragment colocation
    class FooOperation
    QUERY = <<~EOS
    query(bazId: $ID!) {
    baz(id: $bazId) {
    a
    ...BarOperationFragment
    }
    }
    #{BarOperation::Fragment}
    EOS
    def self.execute!(baz_id)
    data = client.execute(query: QUERY, variables: { bazId: baz_id })
    ActiveRecord::Base.transaction do
    Foo.create!(a: data.baz.a)
    BarOperation.execute!(data.baz)
    end
    end
    end
    class BarOperation
    FRAGMENT = <<~EOS
    fragment BarOperationFragment on Baz {
    b { c, d }
    }
    EOS
    def self.execute!(baz)
    Bar.create!(c: baz.b.c, d: baz.b.d)
    # more complicated logics...
    end
    end

    View Slide

  24. Tips
    • Fragment Colocation is useful technique

    not only for frontend but also for backend!


    • Sometimes you should colocate GraphQL fragment with

    the operation (command, service, interaction...) class

    View Slide

  25. Practice 3 -


    Testing

    View Slide

  26. Testing
    • To test the app, we need to stub/mock the GraphQL Requests


    • In web frontend apps


    • mswjs/msw


    • In Ruby apps


    • bblimke/webmock

    View Slide

  27. webmock
    stub_request(:post, "another-backend.com/graphql").
    with(body: { query: FooOperation::QUERY, variables: { bazId: "1" } }).
    to_return(body: {
    data: {
    a: 1,
    b: {
    c: 2,
    d: 3
    }
    }
    }.to_json)

    View Slide

  28. This is sooooo unmaintainable!
    • It’s hard to manage this mock data


    • no static types


    • We need to stub for same request in several
    fi
    les


    • e.g. api test (request spec) and unit test


    • => Please use qsona/webmock-graphql


    • Sorry time is up 😉

    View Slide

  29. Conclusion

    View Slide

  30. Conclusion
    • GraphQL client library is not necessary on Backend Ruby


    • Colocate Fragments with operation classes/functions


    • Use (and contribute) qsona/webmock-graphql 😉

    View Slide