Getting Started GraphQL Using Ruby on Rails

Published: Mar 12, 2023

GraphQL is an API standard to provide resources on the server-side to various types of clients. GraphQL itself is a specification and doesn’t provide an implementation. For Ruby on Rails, GraphQL Ruby https://graphql-ruby.org/ is the most popular library.

Not like REST API, GraphQL has only one endpoint. While REST API uses different endpoints (paths or URL) to access multiple types of resources, GraphQL uses schema based queries. The REST API clients receives all fields server returned. However, GraphQL clients can specify what fields they want to receive from the API server.

For REST API, the swagger UI https://swagger.io/tools/swagger-ui/ is often used as a documentation and testing tool. In general, the swagger UI is provided by the REST API server.

In contract, the GraphQL server doesn’t provide such UI tool. Instead, GraphQL desktop apps are out there, which provides UI based documentation and testing tools. The blog post “Top 10 GraphQL clients” lists desktop apps with some explanations. Such apps have ability to pull the schema from GraphQL server and show that on UI. Also, those have features to write GraphQL queries, send those and receive results.

Since Ruby on Rails is designed for REST API, we need additional steps to use Rails as a GraphQL server. This is a memo how to get started GraphQL using Ruby on Rails.

Setting Up

Create Rails App

This memo focuses on creating GraphQL API server. The App will be the API only and have a smaller stack with sqlite3 as a database. Ruby and Rails versions are:

  • Ruby: 3.2.1
  • Rails: 7.0.4.2

Create .railsrc file with the content below:

--api
--skip-action-mailer
--skip-action-mailbox
--skip-action-cable
--skip-action-text
--skip-active-job
--skip-active-storage
-T

The default location of .railsrc is a home directory just like .zshrc or .bashrc. If it is created in another directory and/or another filename, use --rc=RC option to specify the file.

Create the Rails app named “mini-blog”, for example:

$ rails new mini-blog --rc=./.railsrc

Test the app is created correctly:

$ cd mini-blog
$ rails s

Then, hit http://localhost:3000/ on a web browser. If you see Rails logo and Ruby/Rails versions, the server could start successfully.

Install GraphQL Ruby gem

Once the Rails app is created, the next step is to install GraphQL Ruby (https://graphql-ruby.org/) gem. Run two commands below:

$ bundle add graphql
$ rails g graphql:install

The second command generates GraphQL basic types, default query/mutation types, and a default schema. The schema name is [APP_NAME]_schema.rb. All are located under app/graphql directory. Also, the command adds a route to /graphql. Have a look at config/route.rb, which looks like:

# config/routes.rb
Rails.application.routes.draw do
  post "/graphql", to: "graphql#execute"
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Defines the root path route ("/")
  # root "articles#index"
end

Lastly, the second command generates a default GraphQL controller, app/controllers/graphql_controller.rb.

Now, app directory looks like:

app
├── controllers
│   ├── application_controller.rb
│   ├── concerns
│   └── graphql_controller.rb
├── graphql
│   ├── mini_blog_schema.rb
│   ├── mutations
│   │   └── base_mutation.rb
│   └── types
│       ├── base_argument.rb
│       ├── base_connection.rb
│       ├── base_edge.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
│       ├── node_type.rb
│       └── query_type.rb
└── models
    ├── application_record.rb
    └── concerns

Try How GraphQL Looks Like

For now, GraphQL has started working with the minimal setting. The GraphQL gem’s generator already created a very simple API. We can test that.

To test GraphQL, we need a GraphQL client. Previously, Graphiql gem (notice, the name has “i” before ql) was often installed along with GraphQL Ruby gem. However, once the Rails 7 app is configured as API only server, adding UI became not easy. Besides, many free-to-use GraphQL clients are out there as mentioned at the beginning.

Among those desktop apps, below might be the choice.

This memo is going to use GraphiQL desktop app. If you are on MacOS and have brew, try below to install the app.

$ brew install --cask graphiql

If you have other OS, you can install the desktop app by npm command as well. Additional installations are on the GitHub repo.

When the GraphiQL window is firstly opened, it should look like this:

default graphiql window

Input http://localhost:3000/graphql in the GraphQL Endpoint field, which is defined in config/routes.rb Also, check the Method choice. It is defined in config/routes.rb as POST. Then, GraphiQL app connects to Rails and pulls the schema defined in the app/graphql/types/query_type.rb and app/graphql/types/mutation_type.rb. Those schemas show up when “< DOCS” button on the right upper area of GraphiQL window is clicked.

default graphiql window

Both Query and Mutation schemas are displayed when the links get clicked.

Let’s make a query to testField. On the leftmost pane, input GraphQL query.

{
  testField
}

Hit the right arrow button above the query input pane. The result is on the center pane. The message of “Hello World!” is from app/graphql/types/query_type.rb file.

default graphiql window

The mutation query can be sent, however, nothing changes since all are static just a test implementation.

Create Models and GraphQL Types

GraphQL itself is independent from the active record world. However, API connects to a database to retrieve or update resources almost always. On Ruby on Rails, creating an active record model is the way to connect to the database.

Well, a GraphQL type is the first? Or, an active record model should come before the GraphQL type? The GraphQL generator can create a type without a model definition. But, certainly, the type definition is almost empty. When the active record model exists already, GraphQL generator looks at the database schema and generates the type. By this, we will get a usable GraphQL type definition.

Define Models

Let’s imagine we will create a blog site. The minimum models would be users and posts whose relation is: a user has multiple posts. The user’s email should be unique so that the same email user won’t be created more than once.

The first thing is to generate user and post models as in below:

$ rails g model User email:string:uniq first_name:string last_name:string
$ rails g model Post user:references title:string{50} content:text

We want to add some more constraints to models. The user model’s email is implicitly non-null since it is an indexed unique value. However, for the graphql generator, it’s better to have non-null constraint explicitly. Additionally, we want post model’s title and content to be non-null fields also. No need to say, we need migrations.

$ rails g migration ChangeEmailNullOnUsers
$ rails g migration ChangeTitleContentNullOnPosts

Then, edit migration files:

#  db/migrate/[DATE TIME]_change_email_null_on_users.rb
class ChangeEmailNullOnUsers < ActiveRecord::Migration[7.0]
  def change
    change_column_null :users, :email, false
  end
end

#  db/migrate/[DATE TIME]_change_title_content_null_on_posts.rb
class ChangeTitleContentNullOnPosts < ActiveRecord::Migration[7.0]
  def change
    change_column_null :posts, :title, false
    change_column_null :posts, :content, false
  end
end

Create tables on the sqlite3 database.

$ rails db:create db:migrate

We should update models as well.

Edit app/models/user.rb to add a has_many association and uniqueness validation.

# app/models/user.rb
class User < ApplicationRecord
  has_many :posts, dependent: :destroy

  validates :email, uniqueness: true
end

Edit app/models/post.rb to add presence validations.

# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user

  validates :title, presence: true
  validates :content, presence: true
end

The model definitions are ready. It’s time to add some seed data, so that we can see some results.

# db/seeds.rb
user1 = User.create(
  email: "john.doe@example.com",
  first_name: "John",
  last_name: "Doe"
)

user2 = User.create(
  email: "jane.smith@example.com",
  first_name: "Jane",
  last_name: "Smith"
)

Post.create(
  [{
     user: user1,
     title: "Accidentally, I created a Rails app!",
     content: "My days are this, that and blah. Wait! I accidentally created a Rails app!!"
   },
   {
     user: user2,
     title: "Blogging",
     content: "I like blogging. It's fun. One day, I started writing a blog post..."
   },
   {
     user: user2,
     title: "Spring!",
     content: "It's not about a web framework here. I'm talking about a season. Yes, Spring!"
   }
  ]
)

Run the command below to add seed data to the database.

$ rails db:seed

To check above worked correctly, Rails console is one of ways to test it.

$ rails c
Loading development environment (Rails 7.0.4.2)
irb(main):001:0> User.all
...(user query result here)...
irb(main):002:0> Post.all
...(post query result here)...
...

Generate GraphQL Types

For now, active record models are ready. The next step is to generate GraphQL types. Run the command below.

$ rails g graphql:object user
$ rails g graphql:object post

The generator captures the database schema definition in db/schema.rb to create user and post types. Mostly, we can use generated types as those are.

The generated user_type.rb and post_type.rb look like this:

# app/graphql/types/user_type.rb
module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :email, String, null: false
    field :first_name, String, null: true
    field :last_name, String, null: true
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
  end
end
# app/graphql/types/post_type.rb
module Types
  class PostType < Types::BaseObject
    field :id, ID, null: false
    field :user_id, Integer, null: false
    field :title, String, null: false
    field :content, String, null: false
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
  end
end

Make GraphQL Queries

So far, we have some seed data in the database. The next step is to make GraphQL queries.

GraphQL query schema is defined in app/graphql/types/query_type.rb, which has been generated when GraphQL Ruby gem was installed. As we see “testField” in the above section, all query definitions should be in the query_type.rb.

GraphQL examples out there often write every query implementation in the query_type.rb. It’s OK if the API is as simple as examples. However, that might end up in adding bunch of methods in a single file in an actual application. A concern is, the code will have poor readability and/or maintainability. As far as I searched online, it looks no decisive solution exists for this. What I found is a GraphQL Ruby’s Resolver (https://graphql-ruby.org/fields/resolvers.html). Although GraphQL Ruby team wants people to avoid Resolver, it is a good way to decouple each type’s query implementation. For that reason, the Resolver is used here.

Define Resolvers

Queries to be implemented are:

  • a single user query, parameter: user id
  • all users query, parameter: none
  • multiple posts query, parameter: none – all posts, user id – one user’s all posts

The implementations of resolvers for those queries are defined:

# app/graphql/resolvers/user_resolver.rb
module Resolvers
  class UserResolver < GraphQL::Schema::Resolver
    type Types::UserType, null: false

    argument :id, Int, required: true

    def resolve(id:)
      User.find(id)
    end
  end
end
# app/graphql/resolvers/user_collection_resolver.rb
module Resolvers
  class UserCollectionResolver < GraphQL::Schema::Resolver
    type [Types::UserType], null: false

    def resolve(**kwargs)
      User.all
    end
  end
end
# app/graphql/resolvers/post_collection_resolver.rb
module Resolvers
  class PostCollectionResolver < GraphQL::Schema::Resolver
    type [Types::PostType], null: false

    argument :user_id, Int, required: false

    def resolve(**kwargs)
      if kwargs[:user_id]
        Post.where(user: kwargs[:user_id]).all
      else
        Post.all
      end
    end
  end
end

Above three resolvers should be referenced in the query_type.rb. Now query_type.rb becomes below:

# app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
    # Add `node(id: ID!) and `nodes(ids: [ID!]!)`
    include GraphQL::Types::Relay::HasNodeField
    include GraphQL::Types::Relay::HasNodesField

    # Add root-level fields here.
    # They will be entry points for queries on your schema.
    field :user, resolver: Resolvers::UserResolver
    field :users, resolver: Resolvers::UserCollectionResolver
    field :posts, resolver: Resolvers::PostCollectionResolver
  end
end

Queries on GraphiQL

We are ready to test newly defined queries. On GraphiQL desktop app, Command + w will delete the tab and initialize UI. Input http://localhost:3000/graphql in the Endpoint text field and hit the return key. The updated schema is pulled, which is displayed in the Docs pane.

To get all users, the query is simple.

{
	users {
    id
    email
    firstName
    lastName
  }
}

graphiql users query

To get one user using user’s id, the query needs a parameter. The simple form is to write id in the query.

{
  user(id: 1) {
    id
    email
  }
}

Another form is to use a query variable. In this case, the query needs additional info about the variable.

query user($uid: Int!) {
  user(id: $uid) {
    id
    email
  }
}

Then, pass the variable in the query variables pane.

{
  "uid": 1
}

graphiql user with id query

For post queries, with/without user id are defined. If the user id is not provided, the query gets all posts.

{
  posts {
    id
    userId
    title
    content
  }
}

If the user id is given, the query retrieves the specified user’s all posts.

query posts($uid: Int) {
  posts(userId: $uid) {
    id
    userId
    title
    content
  }
}
{
  "uid": 2
}

graphiql posts query

Create a New Resource by Mutation

GraphQL calls “mutation” to create/update/delete operations over resources. Like queries, mutations need a definition for each types.

User and Post Mutations

GraphQL Ruby provides a mutation generator. It still needs to edit, however, the generator cuts down the amount of coding. Run the command below to create user and post mutation definitions:

$ rails g graphql:mutation_create user
$ rails g graphql:mutation_create post

After editing two mutations, those look like below:

# app/graphql/mutations/user_create.rb
module Mutations
  class UserCreate < BaseMutation
    description "Creates a new user"

    field :user, Types::UserType, null: false

    argument :email, String, required: true
    argument :first_name, String, required: false
    argument :last_name, String, required: false

    def resolve(**kwargs)
      user = ::User.new(**kwargs)
      raise GraphQL::ExecutionError.new "Error creating user", extensions: user.errors.to_hash unless user.save

      { user: user }
    end
  end
end
# app/graphql/mutations/post_create.rb
module Mutations
  class PostCreate < BaseMutation
    description "Creates a new post"

    field :post, Types::PostType, null: false

    argument :user_id, Integer, required: true
    argument :title, String, required: true
    argument :content, String, required: true

    def resolve(**kwargs)
      post = ::Post.new(**kwargs)
      raise GraphQL::ExecutionError.new "Error creating post", extensions: post.errors.to_hash unless post.save

      { post: post }
    end
  end
end

GraphQL Ruby generator has already updated app/graphql/types/mutation_type.rb, so we don’t need to edit this file. For now, mutation type definition is like this:

# app/graphql/types/mutation_type.rb
module Types
  class MutationType < Types::BaseObject
    field :post_create, mutation: Mutations::PostCreate
    field :user_create, mutation: Mutations::UserCreate
  end
end

Mutations on GraphiQL

Everything is ready. It’s time to create a user and post. The mutations look like below:

mutation {
  userCreate(input: {
    email: "alice.jones@example.com",
    firstName: "Alice",
    lastName: "Jones"
  }) {
    user {
      id
      email
    }
  }
}
mutation {
  postCreate(input: {
    userId: 3,
    title: "Hello World!",
    content: "Hey, I made it to this world!"
  }) {
    post {
      id
      userId
      title
      content
    }
  }
}

After executing create user and post mutations, check the result by GraphQL queries we tried above. A new user, posts should be created. The Rails console is another way to test newly added user and post.

Latest Posts

Ruby on Rails Secrets Management

A web application needs various kinds of values, params and etc which should not be revealed to the public, say GitHub repo. For example, API keys, tokens, passwords, and endpoints, all those should be kept secret. Another important factor is that such secrets should be shared among the team members. Additionally, all those secrets should come along with the deployment.

OAuth2 PKCE With Rails 7, React/Redux and esbuild

Logging in to a web site is what users do quite a lot. Suppose it is a blog site. Once a user completes a log-in process, the user is allowed to create a new post, update contents and delete a post. The blog site might have a feature to leave comments by logged in users.

Rails 7 React/Redux Development with esbuild

Rails 7 provides a couple of approaches to bundle a rich JavaScript application such as SPA. To create the JavaScript application, we should specify j|--javascript option with importmap (default), webpack, esbuild or rollup when rails new command gets run. Although webpack is still among the choices, it has been retired as describe in the https://github.com/rails/webpacker/blob/master/README.md. The choice here is esbuild since it is friendly to JavaScript development, for example, starting from yarn create react-app .... The esbuild is gaining popularity and known to run very fast with its Go-lang implementation.