GraphQL APIs in Rails

GraphQL is a flexible, strongly-typed query language. It's useful because it gives front-end developers the ability to query the database without many changes to the back-end. In this article, David Sanchez shows us how to design and build our own GraphQL APIs in Rails.

Software developers need a way to make applications communicate with each other and share necessary data, which is where APIs come into play. An application programming interface (API) is an interface for building and integrating applications.

Currently, the most common way to create an API is by using REST, so when you're creating an API that uses this architecture, you can say that you are building a RESTful API, but what does it mean? What is REST?

REST

Representational state transfer (REST) is an architectural style for standardizing applications to facilitate communication between them. Thus, a RESTful API is just an API based on REST that uses HTTP requests, such as GET, POST, PUT, PATCH, or DELETE, to access and use the data of a resource.

The requests and responses of an API consist of three parts: the status code, headers, and the body. For example, if you search for something using Google, go to the network tab and click on the first element, you will see the headers subtab, which looks something like this:

Request Method: GET
Status Code: 200
...

The response body is the HTML that you see on the browser; you can see it on the response subtab.

Headers allow us to specify some options, including the content type of the endpoint, such as JSON or XML, and authorization tokens. The status code indicates the result status of a request. If you attempt to load a page that doesn't exist, you often see a 404 page, which is a status code. The Google example above returned a 200 status, which means the request was successful.

Resources are an essential component of REST; they are an abstraction of your application domains, just like OOP. For example, you can have a resource called users and access it through a URL by performing some actions called resource methods; however, these methods are different from the HTTP methods.

Thus, your RESTful API for users may look like this:

  • GET /users -> Fetch all Users
  • GET /users/:id -> Fetch a specific User by its ID
  • POST /users -> Create a new User
  • PUT /users/:id -> Update a User
  • DELETE /users/:id -> Delete a User

Like any other architecture, REST has constraints:

  • Uniform Interface: This means that if a developer becomes familiar with one endpoint, he or she should be able to become familiar with the others.
  • Client-server: Both sides are independent but must use the same interface to communicate.
  • Stateless: The client is responsible for managing the state of the application, not the server.
  • Cacheable: Caching can help improve performance.

Advantages

  • Scalability: The product can scale quickly since the client and server are separate; you can run them on separate servers by different teams.
  • Flexibility: REST APIs accept diverse data formats, such as JSON and XML, so you can change them and experiment without much effort.
  • Independence: Stateless communication helps to make the API less complex by focusing on their concerns.

Disadvantages

  • Over-fetching: Sometimes, you need less data than the endpoint gives you, but there is no way to inform the REST API, and retrieving all the data can be expensive in many ways.
  • Under-fetching: In the opposite scenario, you sometimes need more data than the endpoint gives you, so it is necessary to make more requests.
  • Versioning: If you want to avoid breaking changes to your clients, it's essential to create an API version, but it's difficult to maintain.

REST is an option, but a recent alternative called GraphQL allows us to construct flexible APIs.

GraphQL

GraphQL was created by Facebook to build APIs. They define it as a Query Language for APIs and a runtime for fulfilling queries with your existing data. Most importantly, it gives clients the power to ask for exactly what they need.

A GraphQL query is very descriptive; you specify which fields you from of a resource, and you only get what you asked for. For example, this is a query where you want the first name, last name, and the email of a user:

GraphQL Query

The main difference between GraphQL and REST is that GraphQL is a language and a technology, while REST is an architecture pattern. In most cases, GraphQL is used for teams whose data retrieval needs are not met by traditional REST APIs.

We need to understand some concepts before continuing, and they are essential to understanding how GraphQL works. The first one is schema; it is the core concept, and it defines the functionality available to the client applications.

GraphQL is a strongly typed language, so it has a type system that helps us define a contract between the client and server; these are some of its types:

  • Scalar types (Int, Float, String, Boolean, and ID)
  • Object
  • Query (Used to fetch data, basically your GET requests)
  • Mutation (Used to modify, create, and delete data, basically your POST, PUT, and DELETE requests)

Advantages

  • No Over and Under fetching: You ask for the data you need.
  • Single Source Truth: You only have an endpoint, so there’s no need to call many endpoints for each resource.
  • Strong Typing: You need to define the type of each resource attribute; it reduces errors significantly, and it's easier to debug if the endpoint fails.
  • No Versioning: GraphQL deprecates APIs on a field level; you can remove aging fields from the schema without impacting the existing queries.

Disadvantages

  • Data Caching: The cache in GraphQL is performed on the client-side, but doing so requires adding a lot of extra data or use some external libraries.
  • Complexity: The learning curve is more extended than REST because you need to learn a language, types, and how to make requests.
  • Performance issues: Sometimes, you can abuse nested attributes on requests, and if your server is not prepared for it, you could encounter N+1 problems.
  • File uploading: GraphQL doesn't support this out of the box, so if you want to use images, you will have to use Base64 encoding or a library like Apollo.
  • Status Code 200 for everything: You only get a Status 200 (Ok) for all your requests, even if they fail; this is not helpful for the developer experience.

Now that you know a few things about GraphQL, let's code to understand more about it!

Creating a GraphQL API with Ruby on Rails

Setting Up

First, create a Rails API project by running the following command:

> rails new graphql_example --api

For this example, we'll create an application in which a user can create movies in a helpful but straightforward manner. Let's make our models:

> rails g model User email first_name last_name
> rails g model Movie user:references title year:integer genre

Don't forget to run the migrations:

> rails db:migrate

Open your user model and add the relationship to the movie model by using something like this:

# app/models/user.rb

class User < ApplicationRecord
  has_many :movies, dependent: :destroy
end

Installing GraphQL

The next step is to add the GraphQL gem to our Gemfile; you can visit its page, graphql-ruby, for more details; now, open your Gemfile and add this line:

# Gemfile

gem 'graphql'

group :development do
  gem 'graphiql-rails'
end

As you can see, we are adding two gems. The first one is the GraphQL implementation, and the second one is the graphic interface (graphiql) to test our endpoints in the development environment. That's so cool!

Install these gems by running the following in the console:

> bundle install

Finally, set up the necessary for graphql by executing the following instruction:

> rails generate graphql:install

After the installation, you'll see the message "Skipped graphiql, as this rails project is API only" in the console because we created the project with the API flag. However, if you have a full Rails project, is not necessary to add the graphiql-rails gem, as the previous command will do it for you.

We now have GraphQL in our project; you can see we have a folder called graphql within the app and a graphql controller:

> tree app/graphql
app/graphql
β”œβ”€β”€ graphql_example_schema.rb
β”œβ”€β”€ mutations
β”‚Β Β  β”œβ”€β”€ base_mutation.rb
└── 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
    β”œβ”€β”€ query_type.rb

In previous sections, I mentioned that GraphQL only has an endpoint; if you go to your routes file, you'll see something like this:

# config/routes.rb

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

The endpoint is responsible for managing all our requests; note that this POST endpoint points to the graphql_controller and the execute action. In general, you can see that the controller processes all requests, context, variables, etc. and matches the types, either queries or mutations.

Creating the Object Types

It's time to create our first object types. As you saw previously, we have a type called object, and you can relate objects like a resource talking about REST API. In this case, user and movie are object types, and you can create them by running the following:

> rails generate graphql:object user
> rails generate graphql:object movie

These commands create the user and movie types within the graphql/types folder; this is the user type:

# app/graphql/types/user_type.rb

module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :email, String, null: true
    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

Each field is an attribute of the model, and we are defining the type for each one. For example, If we say that the user id is an ID type or that the user’s email is a string type, then you'll get an error if you send an integer for the email. The null option tells us which field needs to be present in the query.

We can also define custom fields; suppose that you want the number of movies for each user, you can add the following:

# app/graphql/types/user_type.rb

module Types
  class UserType < Types::BaseObject
    ...
    field :movies_count, Integer, null: true

    def movies_count
      object.movies.size
    end
  end
end

In these methods, the object refers to the Rails model, which is the user model in this case.

We can also refer to other types. For example, if we want to show a list of movies that each user has created, add this line:

# app/graphql/types/user_type.rb

module Types
  class UserType < Types::BaseObject
    ...
    field :movies_count, Integer, null: true
    field :movies, [Types::MovieType], null: true

    def movies_count
      object.movies.size
    end
  end
end

As you can see, we are just adding an array of MovieType, and we can add an element by removing the brackets.

Creating Queries

There are two types of requests, query type and mutation type. Queries are all the requests you use to fetch data (using GET), Mutations are all the requests where you modify data (POST, PUT and DELETE). These types are defined in our schema:

# app/graphql/graphql_example_schema.rb

class GraphqlExampleSchema < GraphQL::Schema
  mutation(Types::MutationType)
  query(Types::QueryType)
  ...

So, let's create two queries; the first one retrieves all users, and the second one retrieves a user by providing his or her ID. To do so, open the query_type.rb file and add the following:

# app/graphql/types/query_type.rb

module Types
  class QueryType < Types::BaseObject
    # Get all users
    field :users, [Types::UserType], null: false
    def users
      User.all
    end

    # Get a specific user
    field :user, Types::UserType, null: false do
      argument :id, ID, required: true
    end
    def user(id:)
      User.find(id)
    end
  end
end

We have defined two fields, users and user, with their respective methods. The users field returns a users array and can't be null, while the user field accepts an id as the argument and is required; note that the id is an ID type and returns a single user object.

To test these queries, we need to use the graphiql tool, but first, it's necessary to add the following to our routes.rb file:

# config/routes

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

Before testing our queries in the development environment, open your config/application.rb and uncomment this line:

# config/application.rb

require "sprockets/railtie"

Sprockets is necessary at this point to compile and serve web assets for Graphiql, and finally, create this file: app/assets/config/manifest.js:

// app/assets/config/manifest.js

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

Run the server with rails s and go to localhost:3000/graphiql, and voila! You'll see Graphiql in action:

Graphiql

Next, let's add some users. To use the Faker gem to generate random data, open your db/seeds.rb file and add the following:

# db/seeds.rb

10.times do
  user = User.create(
    email: Faker::Internet.email,
    first_name: Faker::Name.first_name,
    last_name: Faker::Name.last_name
  )

  Movie.create(
    user: user,
    title: Faker::Movie.title,
    year: Faker::Date.between(from: '2000-01-01', to: '2021-01-01').year,
    genre: Faker::Book.genre
  )
end

Don't forget to run rails db:seed to load our seed file, then you can paste the following code in the browser and click the play button to see the API working:

query {
  users {
    firstName
    lastName
    email
    moviesCount
  }
}

With this, you'll see a list of users defined in the users field. You can specify the user attributes you want and experiment with it:

Users Query

Now, you can run the following code to get a single user:

query {
  user(id: 1) {
    firstName
    lastName
    email
    moviesCount
    movies {
      title
      year
      genre
    }
  }
}

User Query

Creating a Mutation

As we said previously, mutations allow us to create, modify, or delete data. We're going to make some users, and to do this, create a app/graphql/mutations/create_user.rb file with the following:

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

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

  def resolve(first_name:, last_name:, email:)
    user = User.new(first_name: first_name, last_name: last_name, email: email)

    if user.save
      { user: user, errors: [] }
    else
      { user: nil, errors: user.errors.full_messages }
    end
  end
end

We receive an argument to create the user, and you can set the type and whether it's required. We return a field, which, in this case, is the user or the errors. The resolve method receives the arguments as params, and it contains the logic to create the user.

Finally, add the mutation to the mutation_type.rb file to be used in the API:

# app/graphql/types/mutation_type.rb

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

We are ready to test it, so go to the browser, paste the following code, and run it:

mutation {
  createUser(input: {
    firstName: "Peter",
    lastName: "Parker",
    email: "spiderman@mail.com"
  }) {
    user {
      id
      firstName
      email
    }
    errors
  }
}

It returns the created User! 🀟🏼

User Mutation

You are ready to create your own GraphQL API with Rails! πŸŽ‰

Considerations

  • N+1 problems

Be careful when defining associations. They are now described at the runtime because your API is dynamic, so N+1 problems are more difficult to detect.

You can avoid them by using includes in your queries. For example, if you want to load the users with their respective movies and the movies with their actors (suppose you have an actors table for this example), you can do this:

User.includes(movies: :actors)

If you go to the Rails documentation you'll see the following: With includes, Active Record ensures that all of the specified associations are loaded using the minimum possible number of queries.

  • Prevent deeply nested queries

Deeply nested queries can generate N+1 problems and add complexity, but we can prevent them by defining a variable at the schema level:

class MySchema < GraphQL::Schema
  # ...
  max_depth 10
end

References

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup β€” No credit card required
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring communityβ€”exclusively for developers like you.
    author photo

    David Sanchez

    Software Engineer and full-time Dad. I love dogs, soccer, and Ruby/Rails also I'm passionate about coding, learning, and teaching others new things.

    More articles by David Sanchez
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errorsβ€”so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do bestβ€”release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup β€” No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    β€” Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial
    Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
    Start free trial
    Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
    Start free trial
    “Wow — Customers are blown away that I email them so quickly after an error.”
    Chris Patton, Founder of Punchpass.com
    Start free trial