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

3 Practices about
Service-to-Service GraphQL Ruby Client

qsona
September 13, 2023
690

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. whoami • @qsona • Working at SHE, Inc. • Ex-

    StudySapuri • GraphQL / Ruby on Rails / Node.js / Microservices
  2. 4 years ago... • Urigo (The Guild) came to Japan,

    and GraphQL Tokyo was held • I talked about Service-to-service GraphQL
  3. 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
  4. On web frontend app • It's almost mandatory to use

    some kind of GraphQL client library • apollo-client, urql, ... • graphql-code-generator
  5. 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?
  6. 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`, ...
  7. 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
  8. Tips • You might not need GraphQL client library •

    Just make a thin wrapper of HTTP client (like Net::HTTP, Faraday, etc)
  9. Compare to frontend apps... • Backend app often has more

    complicated domain logics • Especially, Command (CUD) operations • Sometimes the operations are nested, or executed concurrently
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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”
  15. 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
  16. 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
  17. Testing • To test the app, we need to stub/mock

    the GraphQL Requests • In web frontend apps • mswjs/msw • In Ruby apps • bblimke/webmock
  18. 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 😉
  19. Conclusion • GraphQL client library is not necessary on Backend

    Ruby • Colocate Fragments with operation classes/functions • Use (and contribute) qsona/webmock-graphql 😉