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. theInvoice
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)
- object - the object provided from the query
- 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 invoiceTypes::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
toInvoice.find_by(id: id)
and simply return null if the id is not found - create a standardized response object with fields like
success
,payload
, anderrors
. 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.