GraphQL is a query language for APIs, as well as a server side runtime for executing said queries. The query language itself is universal and not tied to any frontend or backend technology. However, the server side implementations come in many flavors; in our case we're going to use the graphql-ruby gem (with rails) to parse incoming queries, make database calls, and respond with JSON. It's a full-on replacement for REST APIs, rails API controllers, and JSON serializers.
First, let's define some vocabulary I'll be using through this tutorial.
- Queries - Fetch specific data from the API. It's best practice to make queries read-only, like you would a
GET
request in REST. But queries are much more than just simpleGET
s! - Mutations - Any modification of data on the API. Think
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's requested in a query / mutation. Types can also be static, like
String
orID
; these are built into 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).
These 5 Things all work together to fetch, create, mangle, and destroy data in an incredibly readable and intuitive way — If you can read JSON or Yaml, you can read and write GraphQL!
Setting up a Rails API
First, we're going to create a new api-only Rails app for our backend. I'm gonna skip testing for now for the sake of this tutorial. Next, create a couple models to test data with.
rails new graphql_api --skip-test --api
rails g model User email:string name:string
rails g model Book user:belongs_to title:string
rails db:migrate
Open app/models/user.rb
and add the has_many :books
association.
Optionally, create some seed data using the faker
gem in seeds.rb
, then run rake db:seed
.
Installing dependencies
# Gemfile
# The ruby implementation of the GraphQL language.
gem 'graphql'
group :development do
# A development utility to test GraphQL queries.
gem 'graphiql-rails'
# Seed data generator
gem 'faker'
end
Generating the GraphQL files
rails generate graphql:install
bundle
rails generate graphql:object user
rails generate graphql:object book
These generators create a graphql
directory with types, mutations, and a schema. We also want to generate new custom types for our User
and Book
models we created above.
├─ controllers
+ │ └─ graphql_controller.rb
+ ├─ graphql
+ │ ├─ mutations
+ │ ├─ rails_graphql_demo_schema.rb
+ │ └─ types
+ │ ─ base_enum.rb
+ │ ─ base_input_object.rb
+ │ ─ base_interface.rb
+ │ ─ base_object.rb
+ │ ─ base_scalar.rb
+ │ ─ base_union.rb
+ │ ─ book_type.rb
+ │ ─ mutation_type.rb
+ │ ─ query_type.rb
+ │ ─ user_type.rb
The generator adds a new POST
endpoint to our routes that's mapped to app/controllers/graphql_controller.rb#execute
— this method serves as our main API entrypoint and is ready to go. For development, we need the additional endpoint for graphiql-rails
.
# 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
Testing queries with Graphiql
The final step to get graphiql
running is to uncomment require "sprockets/railtie"
in application.rb
. Boot up your rails server with rails s
and navigate to https://localhost:3000/graphiql
to see the interface. Here we can run the following query to get a test response from the API.
Types
For the User
and Book
models, we need to create a series of types so GraphQL knows what kind of data to send back in the event of a request. Somewhat similar to Rails' activemodelserializers or JBuilder, these types make up the structure of our models from the API's point of view. Here we'll specifiy what columns, model methods, and more return to the client application. More info on declaring types can be found here.
User and Book Types
Open up the generated types and add the following fields. Notice each field gets an "object type" and a null
option of whether or not it needs to be present for the query to succeed (e.g. an :id
field should never be nil
, but :name
might be). This tells graphql what to expect from incoming and outgoing data, and gives us peace of mind in knowing exactly how to parse data on both the front and back end.
Also notice we didn't have to define functions for :id, :name
, etc; Those are automatically mapped to the Rails model's attributes we created earlier. Then, we added a custom field, books_count
. This method doesn't exist on the Rails model, so we define it below the list of fields. In these methods object
refers to the Rails model, so we must call object.books.size
.
# 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 :books, [Types::BookType], null: true
field :books_count, Integer, null: true
def books_count
object.books.size
end
end
end
# app/graphql/types/book_type.rb
module Types
class BookType < Types::BaseObject
field :title, String, null: false
end
end
The Main Query Type
There are two main types that incoming requests are routed to: query_type.rb
and mutation_type.rb
. They are both already refrerenced in our schema file, and behave somewhat similarly to Rails routes & resources.
# app/graphql/RAILS_APP_NAME_schema.rb
class GraphqlApiSchema < GraphQL::Schema
mutation(Types::MutationType)
query(Types::QueryType)
end
In our main query type file, we define :users
and :user
fields, along with users
and user
functions. The users
field returns an array of UserType
objects, and can never be nil
(but can be empty). The user
field accepts a required argument :id
that is of the type ID
, and returns a single UserType
object. (ID
is a built-in type that acts just the same as the above User
and Book
type.)
# app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
field :users, [Types::UserType], null: false
def users
User.all
end
field :user, Types::UserType, null: false do
argument :id, ID, required: true
end
def user(id:)
User.find(id)
end
end
end
Querying the User Fields
Visit https://localhost:3000/graphiql
in your browser and paste in the following for the users
and user
query fields we added above. Here we specify exactly what we want the API to respond with; in this case, we only want a list of user names, emails, and the number of books they own.
query {
users {
name
email
booksCount
}
}
We can also query a single user, along with all of their books, and each book's title.
query {
user(id: 1) {
name
email
books {
title
}
}
}
Mutations
Mutations allow for creating, updating, and destroying data. More info on them can be found here. Let's set up a base class from which to extend a CreateUser
mutation.
# app/graphql/mutations/base_mutation.rb
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
end
- Arguments - Here we specify which arguments to accept as params, which are required, and what object types they are. This is somewhat similar to defining strong params in a Rails controller, but with more fine grained control of what's coming in.
- Fields - Same concept as Query fields above. In our case, we accepted arguments to create a user, and we want to return a
user
field with our new model accompanied with an array oferrors
if present. - Resolver - The
resolve
method is where we execute our ActiveRecord commands. It returns a hash with keys that match the above field names.
# 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
# Successful creation, return the created object with no errors
{
user: user,
errors: [],
}
else
# Failed save, return the errors to the client
{
user: nil,
errors: user.errors.full_messages
}
end
end
end
Then finally, add the new mutation to the main 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
To test, open up https://localhost:3000/graphiql
and paste in the following query. Notice we pass in an input: {}
object to createUser
; this maps to the :create_user
field which accepts a single input
argument. Learn more about this design in graphql-ruby's documentation.
mutation {
createUser(input: {
name: "Matt Boldt",
email: "me@mattboldt.com"
}) {
user {
id
name
email
}
errors
}
}
Success! We just created our first model via GraphQL; no extra routes, controllers, or serializers needed. What's more, we only returned exactly the data we needed from the newly created model.
The Frontend
Click here for Part 2, which features a React & Apollo frontend app that connects to our Rails API.