GraphQL Performance Testing (Part 3): Up and Running with Rails GraphQL

Now that our Rails models are in order we can finally get to creating our first GraphQL API.

Step 1: Set up the gem

graphql-ruby has great documentation for doing setup. But I will describe how to get it up and running for our test.

Generate the installation

$ rails g graphql:install

Generate our models

$ rails g graphql:object  User first_name:String last_name:String email:String invoices:\\[Invoice\\]
$ rails g graphql:object InvoiceItem amount:Int description:String price:String price_cents:Int invoice:Invoice
$ rails g graphql:object Invoice number:String date:String total_cents:Int total:String invoice_items:\\[InvoiceItem\\] creator:User

This should create our initial GraphQL types. But there's still some modification to do.

Update the models

We told the generators to treat the total fields for Invoices and InvoiceItems as strings. But, they are currently returning Money objects. To fix this, we can override the total methods.

# app/graphql/types/invoice
def total  
  object.total.format
end  

A few quick things to observe:

  • creating a method on a graphql type will intercept the call of the method on the object. This is great if you need to modify it (as we are doing here) or if you need to add additional calculated fields that don't make sense on the rails model.
  • our graphql implementation provides a few variables that we can interact with
    • object - the object provided from the query
      (i.e. the Invoice we just instantiated)
    • context - an object created in our rails controller where we might want to persist data across queries
      (e. g. the current user or user authorization details)
  • the money object provides a format option that

Step 2: Installing GraphiQL

It's almost time to start up the server, but we need to test types first.

We can install the graphiql gem to get an easy way to interact with and iterate over our schema as we create it.

Since we started rails in api mode, we need to make a few changes to get everything working. This issue has more details if these instructions don't work for you.

  • Check that we have the gem
# Gemfile
gem \"graphiql-rails\"
# we need sprockets now to load css on js

Add the routes

# config/routes.rb
if Rails.env.development?  
    mount GraphiQL::Rails::Engine, at: \"/graphiql\", graphql_path: \"/graphql\"
end  

Add in the sprockets railtie

# config/application.rb
require 'sprockets/railtie'  

create a manifest.js file

/* app/assets/config/manifest.js */
//= link graphiql/rails/application.css
//= link graphiql/rails/application.js

This should ensure we have access to graphiql, but only in a development setting.

try starting the rails server and seeing if it works.

cd rails_api  
rails server  

Then, open localhost:3000/graphiql to see if it works.

Step 3: Setting up the Query Type

Now that rails is back up to speed. We can create the QueryType.

The QueryType determines the root of our api. A good api design should determine which objects should be present at the root and which should only be access through their relationship to other objects. In a more advanced app we could show or hide fields based on user permissions or limit the results returned to those a user is able to access.

For our test, we will create a way to select a list of invoices and a single invoice using its ID.

Creating a list of Invoices

# app/graphql/types/query_type.rb
  field :invoices, [Types::InvoiceType], null: false, description: 'A list of invoices'
  def invoices
    #  
    Invoice.all.limit(5)
  end

By default the rails class will call the method of the same name. When using a regular rails object, this means calling a method on the associated object or trying to access a hash key with that name.

Methods can be overridden, or custom methods can be added.

Here we'll just pull the first five invoices to make sure everything is working properly.

Just refresh graphiql and see the updated api.

Something like the following should pull up a list of invoices, creators, and invoice items without error.

query {  
  invoices {
    date
    number
    creator {
      lastName
      firstName
      email
    }
    invoiceItems {
      amount
      priceCents
    }
  } 
}

Note: the graphql-ruby gem will follow naming conventions both ruby and javascript, so methods in snake_case in ruby will automatically translated to camelCase in graphql and javascript.

Updating the field

A listing of all fields is great. But, it would be better to tell the system how many records we would like returned. This can easily be done by adding an argument to the field

# app/graphql/types/query_type.rb
  field :invoices, [Types::InvoiceType], null: false, description: 'A list of invoices' do
      argument :count, Integer, required: false, default_value: 5
  end
  def invoices(count:)
    Invoice.all.limit(count)
  end

Arguments appear as keyword arguments in the resolver method. By adding a default value, the initial response will remain the same but now we can specify the maximum number of returned records.

query {  
  invoices(count: 50) {
    date
    number
    creator {
      lastName
      firstName
      email
    }
    invoiceItems {
      amount
      priceCents
    }
  } 
}

Since we have already specified the type of the argument, graphql-ruby also takes care of converting user input to an Integer for us.

query {  
  invoices(count: \"Muffins\") {
    date
    number
   ...
}

Adding an incorrect value will return a helpful error instead of being passed to the application code.

Add a way to lookup a single Invoice

In order to look up a single invoice, we can add another field to the QueryType. We'll keep to the rails naming conventions, so we end up with the plural invoices for a list and the singular invoice when looking up a single record.

First, we should expose the id attribute on the InvoiceType

# app/graphql/types/invoice_type.rb
field :id, ID, null: false  

Now we add another field to the query type

    field :invoice, Types::InvoiceType, null: false, description: 'A list of invoices' do
      argument :id, ID, required: false, default_value: 10
    end
    def invoice(id:)
      Invoice.find id
    end

This is similar to the last method, with a few quick changes:

  • Instead of returing an array of invoices [Types::InvoiceType], it's now only returning a single invoice Types::InvoiceType
  • the id argument is provided as a keyword argument
  • the ID type is a special scalar type. At the moment it just returns the stringified version of whatever we pass to the query. Though it is possible (and good security practice) to implement methods to cryptographically convert to and from a UUID. More on that here.

Now something like this should work:

query {  
  invoice(id: 12345) {
    date
    number
   ...
}

... with one exception, if an id is provided that isn't in the database, this will cause a server error and cause the query to hang. How to handle this probably depends on the nature of the site.

We could:

  • change Invoice.find to Invoice.find_by(id: id) and simply return null if the id is not found
  • create a standardized response object with fields like success, payload, and errors. This approach tends to work better on Mutations, where we need to distinguish between network errors and issues with performing the update
  • catch the error and raise a GraphQL::ExecutionError by adding the following to the end of the invoice method:
rescue_from(ActiveRecord::RecordNotFound) do |_err, _obj, _args, _ctx, _field|  
  raise GraphQL::ExecutionError, 'Invoice not found'
end  

The last option is the closest to returning a simple 404 error. We will stick with that option, in order to allow the user to deal with providing an incorrect id.

The docs recommend adding generalized version of this to the schema file to catch similar errors across all fields.

Conclusion

We now have a fully functioning GraphQL api and a way to test our queries!

The final code at this point of the article can be found in the Part3-end branch of the article's repository.