How to Use GraphQL with Ruby on Rails

GraphQL is a query language for APIs. The query language itself is universal and not tied to any frontend or backend technology. This characteristic makes it a great choice for many frameworks or patterns you or your company might follow.

Today, I’m going to build a basic API using GraphQL. This will only cover backend/api concepts. In the future I plan to add an additional tutorial on implementing a front-end around the same concepts. Look for that soon!

Download the source code

Create the app

For this app we will leverage the API mode Rails has baked in. This essentially eliminates the view layer and allows you to opt in to middleware as you see fit. Read more about the API mode here.

$ rails new graphql_fun --api --skip-test

GraphQL comes with some concepts that you need to understand to use it effectively. Here are those concepts and their definitions.

  • Queries – Fetch specific data from the API. Typically these are read-only like GET requests.
  • Mutations – Some type of modification of data on the API. e.g. CREATE, UPDATE, DESTROY.
  • Types – Used to define datatypes, or in our case, Rails models. A type contains fields and functions that respond with data based on what is requested. Types can also be static, like String or ID which come from the server side library.
  • Fields – Represent the attributes for a given type (like attributes on a model).
  • Functions – Supply the above fields with data (like methods on a model).

Create the models

We’ll use a basic blog concept without any form of authentication layer to keep things simple.

rails g model User email:string name: string
rails g model Post user:belongs_to title:string body:text
rails db:migrate

Update your user.rb model to have the following relation:

# app/models/user.rb
class User < ApplicationRecord
  has_many :posts
end

Since we are using API only mode this app won’t be using the latest Rails 6 release. In face we’ll be using gem 'rails', '~> 5.0.7', '>= 5.0.7.2' specifically. On top of this there’s one odd bug I ran into that requires an older version of the sqlite3 gem. Update your Gemfile to use

# Gemfile
gem 'sqlite3', '~> 1.3.13'

and then run:

bundle update

Doing this should get you unstuck and allow you to run those rails generations as outlined before.

Installing Gem dependencies

Since we need GraphQL itself, that means we need to install the ruby port of GraphQL as a gem. On top of the main ruby implementation of the language we’ll use a nice development utility that allows us to perform queries in the browser called GraphiQL.

I’ll also be using the popular gem called faker to save time seeding some data for us to use.

# Gemfile

gem 'graphql'

group :development do
  gem 'graphiql-rails'
  gem 'faker'
end

Be sure to run bundle install after adding these to your Gemfile.

Seeding data

Below I’ve added some dummy data in db/seeds.rb. You can user the Faker gem we added to generate filler content on the fly.

# db/seeds.rb

5.times do
  user = User.create(name: Faker::Name.name, email: Faker::Internet.email)
  5.times do
    user.posts.create(title: Faker::Lorem.sentence(word_count: 3), body: Faker::Lorem::paragraph(sentence_count: 3))
  end
end

We create 5 users with 5 posts for each user by running:

$ rails db:seed

Adding GraphQL files

In order to work with GraphQL we need to use different files not typically associated with a rails application. Thanks to the graphql gem we have access to new generators. You can see what generators are in your arsenal by running rails generate inside your application folder on the command line.

A new section should appear that looks similar to the following:

Graphql:
  graphql:enum
  graphql:install
  graphql:interface
  graphql:loader
  graphql:mutation
  graphql:object
  graphql:scalar
  graphql:union

We can generate the necessary files we need by running the following.

# install graphql
$ rails generate graphql:install
$ bundle install

# generate objects (similar to our typical rails model layer)
$ rails generate graphql:object user
$ rails generate graphql:object post

Running the install generator creates quite a few files as well as adds a new route:

$ rails generate graphql:install
Running via Spring preloader in process 30494
      create  app/graphql/types
      create  app/graphql/types/.keep
      create  app/graphql/graphql_fun_schema.rb
      create  app/graphql/types/base_object.rb
      create  app/graphql/types/base_argument.rb
      create  app/graphql/types/base_field.rb
      create  app/graphql/types/base_enum.rb
      create  app/graphql/types/base_input_object.rb
      create  app/graphql/types/base_interface.rb
      create  app/graphql/types/base_scalar.rb
      create  app/graphql/types/base_union.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"
Skipped graphiql, as this rails project is API only
  You may wish to use GraphiQL.app for development: https://github.com/skevy/graphiql-app

Our routes.rb file is as simple as the following:

# config/routes.rb
Rails.application.routes.draw do
  post "/graphql", to: "graphql#execute"
end

And our User and Post object are new files that get created inside a new folder to the app called graphql/types.

The current structure looks like the following:

graphql
├── graphql_fun_schema.rb
├── mutations
└── types
    ├── base_argument.rb
    ├── base_enum.rb
    ├── base_field.rb
    ├── base_input_object.rb
    ├── base_interface.rb
    ├── base_object.rb
    ├── base_scalar.rb
    ├── base_union.rb
    ├── mutation_type.rb
    ├── post_type.rb
    ├── query_type.rb
    └── user_type.rb

Visualizing queries

If you recall a bit before we added a second gem to our development environment that lets you perform and visualize GraphQL queries on the fly. We need to extend our routes.rb file to only load this in the development environment of the app. That would look like the following:

# config/routes.rb

Rails.application.routes.draw do
  if Rails.env.development?
    mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "graphql#execute"
  end
  post "/graphql", to: "graphql#execute"
end

Think of GraphiQL as a GUI for GraphQL. GraphQL only has a single endpoint unlike more conventional RESTful routing which generates quite a few absolute paths for different request types.

Now, visiting localhost:3000/graphiql on your local rails server will give you a nice UI to use.

Small gotcha

Because we are in API mode there’s no concept of an asset pipeline in play. That framework has been commented out by default in config/application.rb If you tried to boot your server right now it would boot but visiting the GraphiQL path would result in an error. To fix this we need to un-comment a line in application.rb and restart your server.

# config/application.rb

require_relative 'boot'

require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "action_cable/engine"
require "sprockets/railtie" # <- uncomment this line!
# require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module GraphqlFun
  class Application < Rails::Application
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

    # Only loads a smaller set of middleware suitable for API only apps.
    # Middleware like session, flash, cookies can be added back manually.
    # Skip views, helpers and assets when generating a new resource.
    config.api_only = true
  end
end

On top of this issue we need to provide a manifest.js file within app/assets/config/manifest.js. You’ll need to create those files and folders for this to work.

assets
└── config
    └── manifest.js

Inside the manifest file add the following:

//= link graphiql/rails/application.css
//= link graphiql/rails/application.js

Boot your server once more and you should hopefully be back in action.

GraphQL Types

For the User and Post models we need to create types so that GraphQL knows what kind of data to send back. Here I can specify what columns, methods, and more that will return to the app.

Starting with the UserType let’s ammend the user_type.rb file to include the following.

# app/graphql/types/user_type.rb
module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: true
    field :email, String, null: true
    field :posts, [Types::PostType], null: true
    field :posts_count, Integer, null: true


    def posts_count
      object.posts.size
    end
  end
end

Each field gets an object “type” and a null option of whether or not it needs to be present for the query to be consider successful. Passing types and null booleans tells GraphQL what to expect so it knows how to parse data on both the backend and client side parts of the app.

I added a method called posts_count that simply grabs the amount of posts in the database. This doesn’t exist on the model directly so we invented it. In these type of methods we get the word object for free. It refers to the Rails model in mention. In this case, User.

You may notice id and name don’t have functions tied to them. These are already mapped thanks to the models we generated with Rails before hand.

The PostType file is a bit simpler.

# app/graphql/types/post_type.rb

module Types
  class PostType < Types::BaseObject
    field :id, Integer, null: false
    field :title, String, null: false
    field :body, String, null: false
  end
end

The Main Query Type

With GraphQL there are two types of requests that get routed to query_type.rb and mutation_type.rb. These have already been referenced when we ran the install generator. That file is called your_appname_schema.rb. You can think of this file like a routing type of file. Mine looks like the following:

# app/graphql/graphql_fun_schema.rb
class GraphqlFunSchema < GraphQL::Schema
  mutation(Types::MutationType)
  query(Types::QueryType)
end

We need to amend app/graphql/types/query_type.rb to accomodate for our new UserType. Doing so looks like the following:

# app/graphql/types/query_type.rb 
module Types
  class QueryType < Types::BaseObject
    # /users
    field :users, [Types::UserType], null: false

    def users
      User.all
    end

    # /user/:id
    field :user, Types::UserType, null: false do
      argument :id, ID, required: true
    end

    def user(id:)
      User.find(id)
    end
  end
end

Here I’m defining what users and user bring back for us. We need to define these fields and their appropriate ruby methods for determining the response. The users field returns an array of UserType objects (multiple users) and can never be empty (nil). The user field accepts an id argument and returns a single user. It to, can never be nil.

We can test out our work in at localhost:3000/graphiql. A GraphQL query for users looks like the following:

query {
  users {
    name
    email
    postsCount
  }
}

If all goes swimmingly you should see a response back of all the users.

And if querying a specific user you should return only one:

query {
  user(id: 2) {
    name
    email
    posts {
      title
    }
  }
}

Based on the relationship between our data we can nest posts within the user query and return all the posts of the given user whose ID equals 2.

Mutations

Mutating data is exactly how it sounds. In the RESTful world this is your UPDATE, PUT, POST, DELETE responses.

We can setup a base class and extend it for each future mutation we create. Create a new file in app/graphql/mutations/ called base_mutation.rb

Inside that file add the following.

# app/graphql/mutations/base_mutation.rb
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
end

Consider this a shell of all our future mutations.

Some terminology is in order when it comes to understanding mutations

  • Arguments – arguments to accept as params, which are required, and what object types they are. This is similar to defining strong params in a Rails controller, but with more fine grained control of what’s coming in.
  • Fields – Same concept as our Query fields from before. In my case, I accepted arguments to create a new user. I want to return a user field with our new model accompanied with an array of errors if any exist.
  • Resolver – The resolve method is where we execute our ActiveRecord commands. It returns a hash with keys that match the above field names.

Putting those to work looks like this:

# app/graphql/mutations/create_user.rb

class Mutations::CreateUser < Mutations::BaseMutation
  argument :name, String, required: true
  argument :email, String, required: true

  field :user, Types::UserType, null: false
  field :errors, [String], null: false 

  def resolve(name:, email:)
    user = User.new(name: name, email: email)
        if user.save
      {
        user: user,
        errors: []
      }
    else
      {
        user: nil,
        errors: user.errors.full_messages
      }
    end
  end
end

A few notes:

  • Notice how we are inheriting the base_mutation class.
  • Much like a controller we check if a user saved and return a response. The same is true if there are errors during creation.

Adding the CreateUser mutation type

With our mutation response work out of the way we can add the new mutation to the mutation type class so it’s exposed to our API

# app/graphql/types/mutation_type.rb

module Types
  class MutationType < Types::BaseObject
    field :create_user, mutation: Mutations::CreateUser
  end
end

Creating a user

Now we can build a query to create a user and return the same user and/or errors if present.

mutation {
 createUser(input: {
   name: "Andy Leverenz",
   email: "[email protected]"
 }) {
   user {
     id,
     name,
     email
   }
   errors
 }
}

In my logs I can see that a user was indeed created:

Started POST "/graphql" for ::1 at 2019-11-03 15:22:20 -0600
Processing by GraphqlController#execute as */*
  Parameters: {"query"=>"mutation {\n  createUser(input: {name: \"Andy Leverenz\", email: \"[email protected]\"}) {\n    user {\n      id\n      name\n      email\n    }\n    errors\n  }\n}\n", "variables"=>nil, "graphql"=>{"query"=>"mutation {\n  createUser(input: {name: \"Andy Leverenz\", email: \"[email protected]\"}) {\n    user {\n      id\n      name\n      email\n    }\n    errors\n  }\n}\n", "variables"=>nil}}
   (0.0ms)  begin transaction
  SQL (0.7ms)  INSERT INTO "users" ("email", "name", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["email", "[email protected]"], ["name", "Andy Leverenz"], ["created_at", "2019-11-03 21:22:20.095509"], ["updated_at", "2019-11-03 21:22:20.095509"]]
   (0.6ms)  commit transaction
Completed 200 OK in 16ms (Views: 0.1ms | ActiveRecord: 1.7ms)

Pretty slick stuff!

Part 2 featuring the front-end coming soon!

For now we have our GraphQL/Rails set up working great. We didn’t need extra routes, controllers, or serializers to achieve the same work done here. What’s great is that we are only returning the data way ask for and type-checking at the same time. GraphQL is very powerful and I’m beginning to see what all the fuss is about.

Look forward to a front-end follow up to this tutorial coming soon. We’ll use similar queries to construct a view layer with a front-end framework (mostly likely React + Apollo). Until then, thanks for following along!

Shameless plug time

I have a new course called Hello Rails. Hello Rails is a modern course designed to help you start using and understanding Ruby on Rails fast. If you’re a novice when it comes to Ruby or Ruby on Rails I invite you to check out the site. The course will be much like these builds but a super more in-depth version with more realistic goals and deliverables. Download your copy today!!

Follow @hello_rails and myself @justalever on Twitter.