Codementor Events

Building GraphQL APIs in Ruby on Rails

Published Apr 20, 2018Last updated Oct 17, 2018
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.

  1. Customers
  2. Locations
  3. 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:

How to install Ruby on MacOS

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:

Screenshot 2018-04-19 18.42.26.png

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:

  1. List all of the Reservations in the database.
  2. Find and list an Customer by name.
  3. 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!

Discover and read more posts from Joshua Burke
get started
post comments1Reply
Mathieu Bertin
6 years ago

I got the error “Could not find generator ‘graphql:install’” when trying to run the generator. What am I doing wrong?