Table of contents
GraphQL describes itself as a query language for your API and at its core lies a schema describing the underlying data in the form of a directed graph. GraphQL execution engine - runtime executes queries according to the schema and perhaps some general rules from the specification.
Previously, I shared an introduction to GraphQL alongside the comparison of GraphQL vs REST.
In this article, we will set up a Rails API-only application with graphql for our blog app. The blog app will include the following model.
- User model
- Post model and
- Comments model
Let’s begin by running installing our rails API-only application using
rails new blog-graphql -T -d postgresql
The -T
skips unit test, -d
specifies PostgreSQL as the database engine since by default Rails uses SQLite.
Now let’s add GraphQL to our application, at the time of this article, the current version is gem 'graphql', '~> 2.0', '>= 2.0.2'
. See rubygems page for latest version.
Let's add it to our Gemfile and run
bundle install.
Next, run
rails generate graphql:install
This command generates the following:
- Set up a folder structure in app/graphql/
- Add schema definition
- Add base type classes
- Add a Query type definition
- Add a Mutation type definition with a base mutation class
- Add a route and controller for executing queries
- Set up a folder structure in app/graphql/
- Add schema definition
- Add base type classes
- Add a Query type definition
- Add a Mutation type definition with a base mutation class
- Add a route and controller for executing queries
- Install graphiql-rails
Run bundle install
again to install the added graphiql-rails
gem added.
Now, that you’ve successfully installed your GraphiQL, let's set up our blog models.
rails generate model User first_name last_name address postcode city country
rails generate model Post user:references body
rails generate model Comment user:references post:references body
rails db:create db:migrate
Let’s make sure our associations are in place within the models. Update User model associations to
# app/models/user
class User < ApplicationRecord
has_many :posts
has_many :comments
end
# app/models/post
class Post < ApplicationRecord
belongs_to :user
has_many :comments
end
To make sure that everything works correctly on your end, fire up your rails server using
rails server
Open http://localhost:3000/graphiql
on your rails server, you should get the GraphQL playground.
Go ahead and query the example testField.
If you’re probably wondering, where does the testField
come from? Well, wonder no more, it came from #app/graphql/types/query_type.rb
, one of the files generated when we did rails generate graphql:install
Great job! So far.
Let’s generate sample data to play with.
Add the code below to #db/seeds.rb
users = User.create([
{
first_name: "John",
last_name: "Doe",
address: "123 Main Street",
postcode: "AB12 3CD",
city: "London",
country: "UK"
},
{
first_name: "Jane",
last_name: "Doe",
address: "24 High Street",
postcode: "AB12 45CD",
city: "New York",
country: "US"
}
])
posts = Post.create!([
{
body: "This is a post",
user: users.first
},
{
body: "This is another post",
user: users.first
},
{
body: "This is yet another post",
user: users.last
}
])
Comment.create!([
{
body: "This is a comment",
post: posts.first,
user: users.first
},
{
body: "This is another comment",
post: posts.first,
user: users.last
}
])
Run
rails db:seed
Implementing the R (Read) from CRUD Operations
Let’s see how we can read or query our users as well as posts and comments. If you pay attention to the test_field in #app/graphql/types/query_type.rb
field :test_field, String, null: false, description: "An example field added by the generator"
The word field is followed by a name, the type then its nullability. This means the return type should be a String and cannot be null. The description allows you to document your field types as you develop, giving API users to understand the purpose/function of each field. Just below it, there is a method that matches the field name, it has to be the same, this gives the return value when the query is executed.
Let’s create a field to return all users, in our case we would like to return a user object containing all user properties. GraphQL comes with a generator to easily achieve that.
rails generate graphql:object user
This should generate user_type.rb
under #app/graphql/types
for you
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :first_name, String
field :last_name, String
field :address, String
field :postcode, String
field :city, String
field :country, String
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
end
end
Now, let’s add the code below to our
#app/graphql/types/query_type.rb
file.
field :users, [UserType], null: false, description: 'List all users'
def users
User.all
end
Navigate back to http://localhost:3000/graphiql
, you should have the same output.
As stated earlier with the test_field, the GraphQL execution engine resolves fields using the following algorithm:
- First, it looks for the method with the same name defined in the type class itself (like we did earlier in the
QueryType
forusers
); we can access theobject
being resolved using the object method. - If no such method is defined, it tries to call the method with the same name on the
object
itself.
Let’s demonstrate this example by adding the full_name and full_address methods to the user model.
def full_name
"#{first_name} #{last_name}"
end
def full_address
"#{address}, #{postcode}, #{city}, #{country}"
end
Then let’s quickly update our user_type.rb with defined fields. Your #app/graphql/types/user_type.rb
should now be looking like this
module Types
class UserType < Types::BaseObject
field :full_name, String
field :full_address, String
[...]
end
end
Navigate back to http://localhost:3000/graphiql
, let’s make sure we can query the defined method in our model as part of the user field.
The beautiful thing about GraphQL is there is no over/under fetching which is a concept describing how we often get less or more data than we need with REST because there is only so much that has been made available by the backend, this is avoided by GraphQL, all the data are always available but you can just select the field you need at any point in time in any particular context so that you don’t ever ask for more than you actually need and you always have access to what you need.
Before we wrap up, let’s quickly generate post and comment types, and also establish relationships between user posts and comments.
rails generate graphql:object post
rails generate graphql:object comment
Now that we have our PostType and CommentType let’s include that in our UserType and PostType respectively.
#app/graphql/types/user_type.rb
module Types
class UserType < Types::BaseObject
field :posts, [Types::PostType]
[...]
end
end
#app/graphql/types/post_type.rb
module Types
class PostType < Types::BaseObject
field :comments, [Types::CommentType]
[...]
end
end
Navigate back to http://localhost:3000/graphiql
, let’s make sure our associations work properly.
Finally, we will add fields for posts and and associated comments.
#app/graphql/types/query_type.rb
file.
field :posts, [PostType], null: false, description: "List all posts"
def posts
Post.all
end
Navigate back to http://localhost:3000/graphiql
and test your query
We can add a few tweaks, by including author of the posts, also handling N+1 query by preloading each request for posts in users and comments in posts request.
The easiest way to avoid N+1 queries is to use eager loading. In our case, we need to preload users when making a query to fetch items in QueryType
: This may not be the most efficient solution, however, if you’re keen on the solution look at the following.
- lazy eager loading (e.g., using the ar_lazy_preload gem)
- batch loading (e.g., using the graphql-batch gem)
#app/graphql/types/query_type.rb
def users
User.preload(:posts, :comments)
end
def posts
Post.preload(:comments, :user)
end
#app/models/post.rb
def author
user.full_name
end
Then update our PostType
#app/graphql/types/post_type.rb
module Types
class PostType < Types::BaseObject
field :author, String
end
end
We have learned a lot about GraphQL, created our blog API models, add GraphQL to our Rails only-API application, and implemented read operations for Users, Posts, and Comments.
In our next article, we will talk about how to manipulate data using GraphQL mutations and keep it up-to-date with subscriptions.
Source code: https://github.com/acushlakoncept/blog-graphql