Building GraphQL APIs in Ruby on Rails
Introduction
Who am I?
Hello! My name is Joshua and I have been building solutions to business problems using Ruby on Rails for over four years now. I love finding ways to simplify complex processes and automate away the repetitive tasks that many people face every day.
The Problem
Let's say you are a restaurant owner who wants to create an easy way for customers to make reservations at your place of business. One way that you could do this would be to build a REST API for third party mobile applications to communicate with.
Then, you could build a front-end web application for your customers to use or integrate with an existing application for faster time to market. At first glance, this sounds like a great idea.
The great thing about REST APIs are that they are very specific to the data model of your back-end application and database. This makes it simple to build out the Create, Read, Update, and Delete actions for your entities, such as: "customers," "restaurants," and "reservations."
But, if you change the way that your data is modeled in the future, or if you choose to change how you display the data, then you could end up breaking your front-end application or your third party integrations.
One way to mitigate this problem in REST APIs is to version your client facing schema. However, this adds significantly to the complexity of your application, the maintainability, and documentation of it as well.
The GraphQL Solution
Here I propose using a different API model that puts control of the data into the hands of the mobile web application client or other third party clients like integrations. The model that I am recommending is called GraphQL and it takes a distinctly different approach to querying and displaying data than the more traditional REST model.
We will create a simple Rails application that exposes three entities to a client.
- Customers
- Locations
- Reservations
In our data model, Customers have a Reservation for a given Location.
So in other words:
- Customers have many Reservations
- Reservations belong to a Customer
- Reservations belong to a Location
We want to allow our client to specify how they want the data exposed to them, without having to conform to the way that we modeled the data and relationships in our database. To do that, we are going to use a Ruby library called 'graphql' along with our Rails applicaion back-end.
Let's get started.
Prerequisites
You will need a recent version of the programming language Ruby installed, as well as version 5.1+ of the Rails web application framework. You can install them with the instructions below:
Check the Ruby
$ ruby --version
ruby 2.4.2p198
Install Rails 5.2
$ gem install rails
=>
Fetching: activesupport-5.2.0.gem (100%)
Successfully installed activesupport-5.2.0
Fetching: erubi-1.7.1.gem (100%)
Successfully installed erubi-1.7.1
...
Create a New Rails App
The command below will generate all of the files that make up a basic Rails app.
$ rails new my-graphql
create
create README.md
create Rakefile
create .ruby-version
create config.ru
create .gitignore
create Gemfile
...
Adding the GraphQL Gem
Within the project Gemfile, you will need to add the line gem 'graphql', '~> 1.7'
somethere in the default block of the file.
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '2.4.2'
gem 'rails', '~> 5.2.0'
gem 'sqlite3'
gem 'puma', '~> 3.11'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.2'
gem 'turbolinks', '~> 5'
gem 'jbuilder', '~> 2.5'
gem 'bootsnap', '>= 1.1.0', require: false
gem 'graphql', '~> 1.7' # <= Add the gem here
Generating the GraphQL Files
This installs the neccessary files for GraphQL for us and adds a special route so that we can debug requests interactively.
$ rails g graphql:install
Running via Spring preloader in process 28224
create app/graphql/types
create app/graphql/types/.keep
create app/graphql/hello_graphql_schema.rb
create app/graphql/types/query_type.rb
add_root_type query
create app/graphql/mutations
create app/graphql/mutations/.keep
create app/graphql/types/mutation_type.rb
add_root_type mutation
create app/controllers/graphql_controller.rb
route post "/graphql", to: "graphql#execute"
gemfile graphiql-rails
route graphiql-rails
Gemfile has been modified, make sure you 'bundle install'
Somewhere in the bundle install
output, you should see the following lines.
Fetching graphiql-rails 1.4.10
Installing graphiql-rails 1.4.10
The GraphiQL Exploration Tool
Now that the graphql
gem is installed and initialized, you should be able to start the Rails server with bundle exec rails server
and see the graphiql
debugging tool at the URL http://localhost:3000/graphiql
.
It should look something like this:
Create the Customers, Locations, and Reservations
$ bundle exec rails g model Customer name:string
bundle exec rails g model Customer name:string
invoke active_record
create db/migrate/20180420021125_create_customers.rb
create app/models/customer.rb
invoke test_unit
create test/models/customer_test.rb
create test/fixtures/customers.yml
$ bundle exec rails g model Location name:string city:string seats:integer
invoke active_record
create db/migrate/20180420021644_create_locations.rb
create app/models/location.rb
invoke test_unit
create test/models/location_test.rb
create test/fixtures/locations.yml
$ bundle exec rails g model Reservation time:string seats:integer customer:references location:references
invoke active_record
create db/migrate/20180420021644_create_reservations.rb
create app/models/reservation.rb
invoke test_unit
create test/models/reservation_test.rb
create test/fixtures/reservations.yml
Create the GraphQL Customer, Location, and Reservation Type Objects
These commands will create both the types for the returned values from queries to the API and the query structure itself so that clients can simply ask for the entites and properties that they need.
bundle exec rails g graphql:object Customer name:String
create app/graphql/types/customer_type.rb
bundle exec rails g graphql:object Location name:String city:String seats:Int
create app/graphql/types/location_type.rb
bundle exec rails g graphql:object Reservation time:String seats:Int customer:Customer location:Location
create app/graphql/types/reservation_type.rb
Migrate the Database
Now that we have both the models and the GraphQL types needed for our API, let's migrate our database so that the tables are created for our entities to be persisted.
bundle exec rake db:migrate
== 20180420021125 CreateCustomers: migrating ====================================
-- create_table(:customers)
-> 0.0010s
== 20180420021125 CreateCustomers: migrated (0.0012s) ===========================
== 20180420021644 CreateLocations: migrating ======================================
-- create_table(:locations)
-> 0.0027s
== 20180420021644 CreateLocations: migrated (0.0029s) =============================
== 20180420021650 CreateReservations: migrating ======================================
-- create_table(:reservations)
-> 0.0027s
== 20180420021650 CreateReservations: migrated (0.0021s) =============================
What We Have Created
The application now has three models as well as three types, each for Customer, Location, and Reservation.
These types will serve as the schema for listing, displaying, and creating each of the entities within the application.
To use the API as it is now, we need three queries:
- List all of the Reservations in the database.
- Find and list an Customer by name.
- Find and list all the Reservations for a specific Customer.
Seeding the Database
Now let's get some values into the database so we can play with the API on real data.
Edit the content of the file within db/seeds.rb
josh = Customer.find_or_create_by(name: 'Joshua Burke')
kevin = Customer.find_or_create_by(name: 'Kevin Heart')
new_york = Location.find_or_create_by(name: 'Main Restaurant', city: 'New York', seats: 200 )
Reservation.find_or_create_by(time: '2018-04-18 12:00:00 UTC', seats: 2, customer_id: josh.id, location_id: new_york.id)
Reservation.find_or_create_by(time: '2018-04-18 12:15:00 UTC', seats: 6, customer_id: kevin.id, location_id: new_york.id)
Checking the GraphQL Types
You should now have several rows in the database as well as definitions for those types within app/graphql/types
. These can be explored in the GraphQL endpoint after we add them to the base types for the API.
Let's edit the app/graphql/types/query_type.rb
file to be the following:
Types::QueryType = GraphQL::ObjectType.define do
name "Query"
field :customer do
type Types::CustomerType
argument :id, !types.ID
description "Find a customer by ID"
resolve ->(obj, args, ctx) {
Customer.find_by(id: args[:id])
}
end
field :location do
type Types::LocationType
argument :id, !types.ID
description 'Find a Location by ID'
resolve ->(obj, args, ctx) {
Location.find_by(id: args[:name])
}
end
field :reservations_from_customer do
type GraphQL::ListType.new(of_type: Types::ReservationType)
argument :name, !types.String
description 'Find reservations from a specific Customer'
resolve ->(obj, args, ctx) {
Customer.find_by(name: args[:name]).reservations
}
end
end
Associations Between Models
In order to look up all of the reservations created by a given Customer, there will need to be some associations made between the Customer and Reservation models. We can also make associations between the Location model and Reservations.
Let's edit the Customer, Location, and Reservation models to look like the following:
class Customer < ApplicationRecord
has_many :reservations
end
class Location < ApplicationRecord
has_many :reservations
end
class Reservation < ApplicationRecord
belongs_to :customer
belongs_to :location
end
Querying the API with the GraphiQL Interface
You should now be able to query the API using the interactive query explorer located at http://localhost:3000/graphiql
on a running Rails server. Start up a Rails server now. If you do not have one running yet, give it a try.
$ bundle exec rails server
Now visit http://localhost:3000/graphiql
and enter this query on the left input to see the results:
{
reservations_from_customer(name: "Joshua Burke") {
seats
time
location
}
}
Conclusion and Benefits
What this is doing is fetching the types that are defined within app/graphql/types/
and serializing them as JSON for a response. Because the models and their types can expose any of their properties as data, and the GraphQL endpoint allows the client to define what it needs, this API is more flexible versus a REST endpoint.
The client in this case has the ability to form exactly the request and response that it needs, without being as limited to what the API provides by default. The client also may not need to make as many requests to the back-end, because several entities could be combined into a single response instead of needing several round trips.
This API was pretty simple to set up. It was about the same difficulty as if we had to build a standard REST API with an Index and Show action. However, this GraphQL version is much more flexible.
The next time you reach to create a REST API to fulfill a client's needs, consider whether a GraphQL API endpoint may be a simpler and better solution.
Cheers and good luck!
I got the error “Could not find generator ‘graphql:install’” when trying to run the generator. What am I doing wrong?