Building a GraphQL API in Rails — Part 3: Best Practice
This is the final post in a series covering three topics relating to GraphQL:
As I mentioned previously, I decided to write this series as Github recently released its GraphQL API alpha. Github has been at the forefront of API-driven development for some time now, and its RESTful API is an exemplary API design referenced by many developers. So it’s always pretty interesting to explore new Github releases.
This post is based on my talk at RoRoSyd Meetup. If you’d like to know more, you can check out my slides and an example repo.
Alright, let’s get started on part 3!
1. Interfaces
Say we have 2 types that share many fields, like id
, updated_at
, and created_at
basic ActiveRecord fields, for example. How do we clean these types? One way to do it is by using interfaces.
Let’s take a look.
Before refactoring, UserType
and PostType
look like this:
# app/graph/types/user_type.rb
UserType = GraphQL::ObjectType.define do
name "User"
description "A user"
field :id, types.Int
field :email, types.String
field :updated_at do
type types.Int
resolve -> (obj, args, ctx) {
obj.updated_at.to_i
}
end
field :created_at do
type types.Int
resolve -> (obj, args, ctx) {
obj.created_at.to_i
}
end
end# app/graph/types/post_type.rb
PostType = GraphQL::ObjectType.define do
name "Post"
description "A post"
field :id, types.Int
field :title, types.String
field :content, types.String
field :updated_at do
type types.Int
resolve -> (obj, args, ctx) {
obj.updated_at.to_i
}
end
field :created_at do
type types.Int
resolve -> (obj, args, ctx) {
obj.created_at.to_i
}
end
end
Now let’s create an active_record_interfaces.rb
file and define id
, updated_at
, and created_at
.
# app/graph/types/active_record_interfaces.rb
ActiveRecordInterface = GraphQL::InterfaceType.define do
name "ActiveRecord"
description "Active Record Interface"
field :id, types.Int
field :updated_at do
type types.Int
resolve -> (obj, args, ctx) {
obj.updated_at.to_i
}
end
field :created_at do
type types.Int
resolve -> (obj, args, ctx) {
obj.created_at.to_i
}
end
end
In doing so, our UserType
and PostType
become much clearer:
# app/graph/types/user_type.rb
UserType = GraphQL::ObjectType.define do
interfaces [ActiveRecordInterface]
name "User"
description "A user"
field :email, types.String
end# app/graph/types/post_type.rb
PostType = GraphQL::ObjectType.define do
interfaces [ActiveRecordInterface]
name "Post"
description "A post"
field :title, types.String
field :content, types.String
end
2. Service objects
We use resolve
blocks to handle each field’s behaviour. However if we add code logic into a resolve
block, it becomes fragile, and it’s also not easy to test. That’s why control flow logic should be abstracted to a Service Object and not placed in a resolve
block.
Here’s an example.
This code:
CreatePostMutation = GraphQL::Relay::Mutation.define do
# ...
resolve -> (object, inputs, ctx) {
post = ctx[:current_user].posts.create(title: inputs[:title], content: inputs[:content])
{
post: post
}
}
end
Could be:
CreatePostMutation = GraphQL::Relay::Mutation.define do
# ...
resolve -> (object, inputs, ctx) {
Graph::CreatePostService.new(inputs, ctx).perform!
}
end
In the second case, we only need to test Graph::CreatePostService.
It’s more testable and easier to maintain.
3. Use the Relay module
In graphql-ruby gem, there are a couple of built in modules with the Relay namespace. If you want your API to look like GitHub GraphQL API, or you just want to integrate it with Relay, it will save you tons of time.
Check out the modules below to get a taste:
- GraphQL::Relay::ConnectionType
- GraphQL::Relay::Node
- GraphQL::Relay::Edge
These are all my experiences, and things I’ve learned. Do you have any more tips to share? Leave a comment here. Thanks!
By Wayne Chu