November 13, 2019
•Last updated November 5, 2023
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!
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 into 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 data types, 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
orID
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 fact 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 oferrors
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.
I 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 to React + Apollo). Until then, thanks for following along!
Shameless plug time
I have a new course called Hello Rails. Hello Rails is 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. View the course!
Follow @hello_rails and myself @justalever on Twitter.
Categories
Collection
Part of the Ruby on Rails collection
Products and courses
-
Hello Hotwire
A course on Hotwire + Ruby on Rails.
-
Hello Rails
A course for newcomers to Ruby on Rails.
-
Rails UI
UI templates and components for Rails.