Domain Driven Design with GraphQL on Ruby on Rails API
Hello everyone,
For the past year, at A line, we had to build an ambitious project management and marketplace platform MVP from scratch. The frontend is built in TypeScript/React/Apollo (Charly Poly).
For the backend, Charly and I had three goals :
- Implementing DDD in Rails
- Implementing a GraphQL API with WebSocket subscriptions
- Having a well-tested, maintainable, and scalable codebase
FYI, today it’s all up and running in production on A Line, the Startup I work on.
The gems
Basic rails/puma/pg/sidekiq/redis. For authentication we choose to use a very pleasant API called Auth0, that provide JWT authentication. For search we use Algolia. Of course, for graphql, the graphql-ruby gem.
The core Architecture
Let’s get an example with the model Portfolio. I’ll not go into subscriptions, but it works great with ActionCable (next article ?).
app/
app/admin/ # ActiveAdmin
app/models/ # The models (User/Portfolio ....)
app/controllers/ # Only for the GraphQL controller
app/graphql/ # The GraphQL codeapp/graphql/aline_api_schema.rb # Main graphql schema
app/graphql/mutation_type.rb # Defines all mutations
app/graphql/query_type.rb # Defines all queries
app/graphql/subscriptions_type.rb # Defines the subscriptionsapp/graphql/resource_base_service.rb
# Base service object that exposes the index/show/create/update/destroy basic methods that will be called by the mutationsapp/graphql/portfolios/ # Portfolio domain
app/graphql/portfolios/type.rb # Portfolio GraphQL type
app/graphql/portfolios/mutations/
app/graphql/portfolios/mutations/create.rb # Create mutation
app/graphql/portfolios/mutations/update.rb # Update mutation
app/graphql/portfolios/mutations/accept.rb # Accept mutation
app/graphql/portfolios/mutations/input_type.rb # Portfolio inputapp/graphql/portfolios/service.rb
# Service Object that may surcharge the index/show/create... methods inherited by resource_base_service methods called by the mutations, and add the accept method.
The code
The controller
The GraphQL controller is pretty simple, just calling the graphql schema.
app/controllers/graphql_controller.rbclass GraphqlController < ApplicationController before_action :authenticate_user # GraphQL endpoint
def execute
result = AlineApiSchema.execute(
params[:query],
variables: ensure_hash(params[:variables]),
context: { current_user: current_user },
operation_name: params[:operationName]
)
render json: result
end
private
def ensure_hash(ambiguous_param)
case ambiguous_param
when String
ambiguous_param.present? ? ensure_hash(JSON.parse(ambiguous_param)) : {}
when Hash, ActionController::Parameters
ambiguous_param
when nil
{}
else
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
end
endend
The Service Object part
the ResourceBaseService (simplified) looks like that :
app/graphql/resource_base_service.rbclass ResourceBaseService attr_accessor :params, :object, :user def initialize(params: {}, object: nil, object_id: nil, user: nil)
@params = params.to_h.symbolize_keys
@object = object || model.find_by(id: object_id)
@user = user
end def self.graphql_call(resource, method)
lambda { |_obj, args, ctx|
params = args.try(:[], resource.to_sym) || args
service = "{resource.to_s.pluralize.camelize}::Service"
service.constantize.new(
params: params,
user: ctx[:current_user],
object_id: args[:id]
).send(method)
}
end def index
# Example of a index if ApplicationRecord defines the
# self.seeable_by method
model.seeable_by(user: user)
end def show
# Example of a show if ApplicationRecord defines the
# self.seeable_by method
return not_allowed unless model.seeable_by(user: user).include?(object)
object
end def create
# Creates the record regarding params and authorizations
model.create!(params)
end def destroy
# Destroys the record regarding authorizations
object.destroy!
end def update
# Updates the record regarding params and authorizations
object.update_attributes!(params)
end private
def singular_resource
resource_name.singularize
end def model
singular_resource.camelize.constantize
end def resource_name
self.class.to_s.split(':').first.underscore
endend
Then you have the Portfolios Service
app/graphql/portfolios/service.rbmodule Portfolios
class Service < ResourceBaseService # Custom Mutation
def accept
return unless model.writable_by(user: user).include?(object)
object.tap(&:accept!)
end # Add action after create
def create
super
# add fancy stuff
end # Rewritten index
def index
model.where(visible: true).seeable_by(user: user)
end end
end
The GraphQL part
The Portfolio Type
app/graphql/portfolios/type.rbPortfolios::Type = GraphQL::ObjectType.define do
name 'Portfolio' field :id, types.ID # types.String for uuid mapping
field :visible, types.Bool
field :user_id, types.ID
field :created_at, types.String
field :updated_at, types.String
end
And the mutations, first the input type, you can have many :
app/graphql/portfolios/mutations/input_type.rbPortfolios::Mutations::InputType = GraphQL::InputObjectType.define do name 'Portfolio input type' argument :visible, types.Bool
argument :user_id, types.IDend
and for example the update mutation :
app/graphql/portfolios/mutations/update.rbPortfolios::Mutations::Update = GraphQL::Field.define do description 'Updates a Portfolio' type Portfolios::Type
argument :id, !types.ID
argument :portfolio, Portfolios::Mutations::InputType # The input type defined earlier
resolve ResourceBaseService.graphql_call(:portfolio, :update)
end
Then simply add them to the mutations_type :
app/graphql/mutation_type.rbMutationType = GraphQL::ObjectType.define do name 'Mutation' field :update_portfolio, Portfolios::Mutations::Update
# ...
end
And add the queries to the query_type :
app/graphql/query_type.rbQueryType = GraphQL::ObjectType.define do field :portfolio do
type !Portfolios::Type
description 'Return a Portfolio'
argument :id, !types.ID
resolve ResourceBaseService.graphql_call(:portfolios, :show)
end field :portfolios do
type !types[!Portfolios::Type]
description 'Return the Portfolios'
resolve ResourceBaseService.graphql_call(:portfolios, :index)
end # ... Metaprog could be your friend here.end
finally the graphql schema
app/graphql/aline_api_schema.rbAlineApiSchema = GraphQL::Schema.define do
mutation(MutationType)
query(QueryType)
use GraphQL::Subscriptions::ActionCableSubscriptions
subscription(SubscriptionType)
end
OMG, that’s it, you have your graphql API. So fu**ing good.
Conclusion
The Service Objects part is maybe not the best thing for all projects, but here it works pretty well.
You can unit test all your services methods, and for me that’s the only real things to test.
For N+1 queries, I suggest you to tweak your index methods with model.includes()
regarding the context ast nodes, or take a look tographql-batch
, another wonderful gem made by Shopify ❤.
The performances are quite good for the moment. GraphQL is a wonderful tool to build APIs, and in ruby it’s pretty pleasant right now :)
Hope it helps, don’t hesitate to reach me for more details. By the way, if you are a talented freelance and look for quality projects, take a look at A line.