has_many :codes

Using GraphQL with Rails

Published  

Until recently whenever I had to implement an API in Ruby/Rails I would go for the typical REST implementation. Then I learnt about GraphQL and wow… I wish I had known about it earlier. REST works fine in most cases but it does have some issues when implementing a not-so-trivial API (not to say GraphQL doesn’t, as we’ll see later). In particular, with REST it is the server that defines what data can be made available to clients, and how; clients do not have a say in this and they may end up fetching more data from the API than actually necessary or, in the opposite case, requiring multiple HTTP requests to multiple API endpoints just to get all the data they need for a particular purpose. This is bad, because each HTTP request is expensive and time consuming, which can be a problem especially with slow or unstable connections.

GraphQL solves these issues in a very clever way that lets clients tell the API/server exactly what data they need, with a single HTTP request. Data can be nested, for example when you need data for a Rails model together with data for a belongs_to/has_one/has_many association. Because of the way GraphQL works, loading data for an association with GraphQL can lead to issues with N+1 SQL queries on the server, but as we’ll see later even these can be circumvented quite easily. Another advantage of GraphQL is that because the client can define what data it needs, the versioning of the API as it’s typically done with REST is not really required with GraphQL.

GraphQL was open sourced by Facebook in 2012 and was created to solve the aforementioned issues with mobile clients in particular. It is essentially a “query language” for APIs, not to be confused with SQL though. A query typically looks like the following:

query {
 categoryGroups {
  id
  name

  categories {
    id
    name
  }
 }
}

In the example above, we are requesting data for a collection of category groups - the model in Rails would be CategoryGroup - together with the categories for each category group - model would be “Category” - according to the has_many association defined in CategoryGroup. As you can see, we are requesting all the data in one single request in a nested format. The response for such a request would be JSON as usual and would look like the following:

{
  "data": {
    "categoryGroups": [
      {
        "id": 113,
        "name": "Food",
        "categories": [
          {
            "id": 358,
            "name": "Groceries"
          },
          [...]
        ]
      },
      {
        "id": 114,
        "name": "Housing",
        "categories": [
          {
            "id": 277,
            "name": "Household goods"
          },
          ...
        ]
      },
      [...]
    ]
  }
}

Using GraphQL with Rails is actually quite easy thanks to the graphql gem, although I must admit I had to spend a little time at first figuring out how to properly use it because the documentation is IMO a bit confusing, in that it shows two different “styles” (depending on where you look) in which you can define the classes etc. required. So I thought I’d write a few notes here for my future reference, hoping they may also help others.

If you follow the README on the gem’s Github page, it tells you to run a generator (rails generate graphql:install) after adding the gem to the Gemfile, and this generator will create some base classes plus a schema, which defines which queries are available to fetch data, and which mutations are available to modify data. The schema and base classes generated are however in a format slightly different from the format compatible with the current version of an accessory gem, graphql-preload, which I am using to solve the N+1 issue with associations. So for now I prefer to create the schema and the classes required manually using the format compatible with this gem. I guess I will have to update that code once this gem is also updated to use the same format as the main graphql gem.

So to get started, first add the graphql-preload gem to your Gemfile (it will also add the graphql gem and another gem by Shopify, graphql-batch, which it uses behind the scenes). I recommend you also add the graphiql-rails gem making sure it’s only available in development:

gem 'graphql-preload'
gem 'graphiql-rails', group: :development

The graphiql-rails gem adds a cool UI which lets you test queries and mutations in the browser and see what’s available in your GraphQL API. I absolutely love it as it makes development a lot easier. This way I can test queries and mutations before using them within a Vue.js app for example. Besides adding the gem to the Gemfile you’ll also need to mount its engine in the routes.rb file:

if Rails.env.development?
  authenticated do
    mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
  end
end

authenticated is a Devise method to ensure that the user is authenticated in order to access GraphiQL’s UI. If you don’t use Devise you will have to adapt your code.

All the queries and mutations will be handled by the single /graphql endpoint, which expects POST requests even to just fetch data because the body of the requests can at times be larger than what is allowed with GET requests. A controller is also required to handle the requests to this endpoint. If you use the graphql gem’s generator this controller will be generated automatically, otherwise create the file app/controllers/graphql_controller.rb and paste the following in it:

class GraphqlController < ApplicationController
  def execute
    variables = ensure_hash(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]
    context = {
      current_user: current_user,
    }
    result = MyAppSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
    render json: result
  end

  private

  # Handle form data, JSON body, or a blank value
  def ensure_hash(ambiguous_param)
    case ambiguous_param
    when String
      if ambiguous_param.present?
        ensure_hash(JSON.parse(ambiguous_param))
      else
        {}
      end
    when Hash, ActionController::Parameters
      ambiguous_param
    when nil
      {}
    else
      raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
    end
  end
end

current_user assumes Devise or other authentication mechanism sets the user currently logged in; the context defined in the execute action will be available in the query/mutation classes we’ll define in a moment.

Next, we need to define the GraphQL schema, so create the file app/graphql/my_app_schema.rb (the example assumes the app’s name is “MyApp”) and paste the following in it:

MyAppSchema = GraphQL::Schema.define do
  use GraphQL::Batch
  enable_preloading

  mutation(Types::MutationType)
  query(Types::QueryType)
end

GraphQL::Batch is provided by Shopify’s graphql-batch gem while enable_preloading is provided by the graphql-preload gem, and both together handle the N+1 issue with associations.

Queries

As you can see, the schema expects queries to be defined in the Types::QueryType class and mutations in the Types::MutationType class. So let’s define the first one by creating the file app/graphql/types/query_type.rb and pasting the following into it:

Types::QueryType = GraphQL::ObjectType.define do
  name "Query"

  field :categoryGroups, !types[Types::CategoryGroupType], description: "Category groups for the current user" do
    resolve ->(obj, args, context) {
      context[:current_user].category_groups
    }
  end
end

In this class, we make it possible for clients to request data for the collection of category groups for the current user. This requires a type for the items in this collection, so we need to create the Types::CategoryGroupType class. Create the file app/graphql/types/category_group_type.rb and paste the following into it (of course I’m still assuming “CategoryGroup has many Categories” in this example, but it should be easy to adapt your code for your actual models):

Types::CategoryGroupType = GraphQL::ObjectType.define do
  name "CategoryGroup"

  field :id, !types.Int
  field :name, !types.String

  field :categories do
    type -> { !types[!Types::CategoryType] }

    preload :categories

    resolve ->(obj, args, ctx) { obj.categories }
  end
end

In this class, we first define the fields id and name which are equivalent to the attributes by the same names in the CategoryGroup model. Fields in GraphQL are typed, and GraphQL will validate the type of fields as well as arguments if any (we’ll see an example of arguments when we look into mutations). We then define a categories field which is for the “has_many :categories” association. As you can see resolve makes three things available: the object, which basically in this case is an instance of CategoryGroup, arguments passed to fetch the field categories, if any (not in this example, but you may need arguments to filter/sort/paginate results), and the context which is the context defined in GraphqlController#execute. The lambda passed to resolve will then fetch the categories for the instance of CategoryGroup. preload is provided by the graphql-preload gem and is very important here, because without it the API request will result in a SQL query to fetch the category groups, plus a query for each category group to fetch its categories, so we’ll basically have an N+1 issue here. Using preload will ensure that all the categories for all the category groups are loaded at once in a single SQL query, so we’ll have in total two SQL queries only (one for the category groups and one for all the categories). So that’s why I recommended adding the graphql-preload gem as well.

Of course we also need to create a type for the Category model, so create the file app/graphql/types/category_type.rb and paste the following into it:

Types::CategoryType = GraphQL::ObjectType.define do
  name "Category"

  field :id, !types.Int
  field :name, !types.String
end

This class should be self explanatory at this point. Now go ahead and open the /graphiql page in the browser (note the “i”). You will see something like this:

Paste the following query as you see in the screenshot and if all went well you’ll see a JSON response in the right column similar to what I showed at the beginning of this post:

{
 categoryGroups {
  id
  name

  categories {
    id
    name
  }
 }
}

Mutations

Mutations in GraphQL are executed in a similar way to how queries are executed. They usually expect some arguments, perform some action that updates/creates/deletes some data, and return some response. Let’s see an example of mutation that allows renaming a category group. First, create the file app/graphql/types/mutation_type.rb and paste the following into it:

Types::MutationType = GraphQL::ObjectType.define do
  name "Mutation"

  field :renameCategoryGroup, field: Mutations::RenameCategoryGroup.field
end

Now it’s time to create the actual mutation that will let us rename a category group. Create app/graphql/mutations/rename_category_group.rb and paste the following into it:

Mutations::RenameCategoryGroup = GraphQL::Relay::Mutation.define do
  name "RenameCategoryGroup"

  return_field :categoryGroup, !Types::CategoryGroupType
  return_field :validationErrors, !types[!Types::ValidationErrorType]

  input_field :id, !types.Int
  input_field :name, !types.String

  resolve ->(obj, args, context) do
    current_user = context[:current_user]
    category_group = current_user.category_groups.find(args[:id])

    if category_group.update(name: args[:name])
      {
        categoryGroup: category_group,
        validationErrors: []
      }
    else
      errors = category_group.errors.map do |attribute, message|
        {
          attribute: attribute,
          attributeHumanized: attribute.to_s.humanize,
          message: message,
        }
      end

      {
        categoryGroup: category_group,
        validationErrors: errors
      }
    end
  end
end

There’s a bit more code in this class but it’s pretty straightforward. First, we define what is going to be returned in the response once the mutation has been performed. In this case, we’ll return the updated category group and an empty validationErrors array if the mutation was successful, otherwise validationErrors will contain a collection of validation errors raised by the CategoryGroup model, in a friendly format. So we also need to create the ValidationErrorType class. Create app/graphql/types/validation_error.rb and paste the following into it:

Types::ValidationErrorType = GraphQL::ObjectType.define do
  name "ValidationError"

  field :message, !types.String do
    resolve ->(obj, args, context) {
      obj[:message]
    }
  end

  field :attribute, !types.String do
    resolve ->(obj, args, context) {
      obj[:attribute]
    }
  end

  field :attributeHumanized, !types.String do
    resolve ->(obj, args, context) {
      obj[:attributeHumanized]
    }
  end
end

In this case, we also call resolve with a lambda that resolves the fields using the properties defined in Mutations::RenameCategoryGroup in the category_group.errors.map do… block.

Back to the GraphiQL interface, paste the following into the left textarea making sure you set the ID of an existing category group or equivalent model in your case:

mutation {
  renameCategoryGroup(input: {id: 113, name: "New name"}) {
    categoryGroup {
      id
      name
    }
    validationErrors {
      attribute
      attributeHumanized
      message
    }
  }
}

If you execute this mutation and all went well, the category group will have the name set to “New name”.

Conclusions

This is just a quick introduction on how to use GraphQL in Rails and avoid N+1 issues, so I may add more posts as I learn - including how to test the API. But there’s A LOT about GraphQL you should look into. A good start could be the website https://www.howtographql.com which contains a lot of useful information. GraphQL is a very cool solution to common issues with APIs, so I definitely recommend you consider it over REST for your next API, especially if you need to build web apps with Vue.js/React/etc or mobile clients.

© Vito Botta