apidays LIVE Paris - Responding to the New Normal with APIs for Business, People and Society
December 8, 9 & 10, 2020
Augmenting a Legacy REST API with GraphQL
Clément Villain, Software Engineer at Fabernovel
2. ● Started in 2015
● Back end with Ruby on Rails, two mobiles app (Android
and iOS), website
● Good tests coverage but not perfect
● Some functional documentation for critical features
(like payment)
● Small evolutions and dependencies upgrade during the
years
● End of 2019 new big feature: Beehive for Business
Fast and independent
Coworking with self check-in
2
9. Ask front end if they can use GraphQL API
Start with queries
Added-value is here, we can do mutation later
Write tests if missing
Extract business code from controller into interactor
Write the GraphQL query and types
10. With an interactor
Extract business code from controller
def index
offers = Offer.all
filtered_offers = offers.available.not_hidden.not_meeting_room
scoped_offers = policy_scope(filtered_offers)
render json: scoped_offers, root: 'offers', serializer: OfferSerializer
end
def index
offers = GetOffers.new.call(current_user: current_user).value!
render json: offers, root: 'offers', serializer: OfferSerializer
end
11. Query is the entry point
class QueryType < Types::BaseObject
# Add root-level fields here.
# They will be entry points for queries on your schema.
field :offers, [Types::OfferType], null: false
def offers
GetOffers.new.call(
current_user: context[:current_user]
).value!
end
end
Query
FIELDS
offers [Offer!]!
12. Type is the equivalent of a REST serializer
class OfferType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
field :has_access_to_private_offices, Boolean, null: false
field :hidden, Boolean, null: false
field :highlighted, Boolean, null: false
field :ht_price, String, null: false
field :kind, OfferKind, null: false
field :marketing_name, String, null: true
field :multiplier_for_durations, [GraphQL::Types::JSON], null: false
def multiplier_for_durations
object.possible_multipliers.map do |i|
{
string: I18n.t("offers.period.#{object.period}", count: i),
multiplier: i
}
end
end
end
Offer
FIELDS
hasAccessToPrivateOffices Boolean!
hidden Boolean!
highlighted Boolean!
htPrice String!
id ID!
kind OfferKind!
marketingName String!
multiplierForDurations [JSON!]!
name String!
13. Ask front end if they can use GraphQL API
Start with queries
Added-value is here, we can do mutation later
Write tests if missing
Extract business code from controller into interactor
Write the GraphQL query and types
Adapt your existing tools
Postman => Altair
16. In the REST world it is solved by adding .preload(:offers)
Enterprise.preload(:offers).all
An innocent query…
# SELECT * FROM enterprises (+ 1 query)
Enterprise.all.each do |enterprise|
# SELECT * FROM offers WHERE enterprise_id = ? (n queries)
enterprise.offers.map(&:id)
end
query {
enterprises {
id
offers {
id
}
}
}
...will result in n + 1 queries being made
Result in bad performance
17. ● Batchloading is a generic lazy batching mechanism
● Idea: Delay evaluation to load data in once
https://github.com/exAspArk/batch-loader & https://github.com/graphql/dataloader
class EnterpriseType < Types::BaseObject
field :offers, [Types::OfferType], null: false
def offers
BatchLoader::GraphQL.for(object).batch do |enterprises, loader|
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload(enterprises, :offers)
enterprises.each do |enterprise|
loader.call(enterprise, enterprise.offers)
end
end
end
end
18. With a custom helper
GraphQL
class EnterpriseType < Types::BaseObject
preload_field :offers, [Types::OfferType], null: false, associations: :offers
end
{REST}
Enterprise.preload(:offers).all