DEV Community

loading...

A Complete Guide to Setting up a GraphQL Server on Rails

harigopal profile image Hari Gopal Originally published at blog.harigopal.in ・8 min read

Introduction

"Convention over configuration" is one of the reasons Rails became as popular as it did. Conventions allow developers to be productive without bike-shedding. However, introducing new concepts to Rails involves a period of experimentation during which there are no answers to troublesome questions.

Setting up a GraphQL server on Rails is one of those tasks.

This post presents a method for setting up a GraphQL server, while also tackling two issues that are generally glossed over by existing documentation surrounding the use of the graphql-ruby gem:

  1. How do I handle authorization?
  2. How do I avoid N+1 queries?

Start with graphql-ruby

We'll be using the well-documented graphql-ruby gem to get started with setting up a GraphQL server on Rails. If you're someone who's already familiar with graphql-ruby, feel free to skip this section.

  1. Add gem 'graphql-ruby', '~> 1.9' to your Gemfile.
  2. Run bundle install on the command line.
  3. Run the gem's installation generator: rails generate graphql:install.
    • If Rails complains that it can't find graphql:install, try again after stopping spring, with bin/spring stop.
  4. The previous step will have updated the Gemfile, so you'll need run bundle install again.

At this point, graphql-ruby is all set up. Let's take a quick look at what the gem added to our Rails application, so that the following steps are easier to understand.

app/
├─ config/routes.rb          (updated to include /graphql, and /graphiql)
├─ controllers/
|  └─ graphql_controller.rb  (all requests are handled by the execute action)
└─ graphql/
   ├─ my_rails_app_schema.rb (the web of interconnected types starts here)
   ├─ types/
   |  ├─ query_type.rb       (base type for all queries)
   |  ├─ mutation_type.rb    (base type for all mutations)
   |  └─ base_*.rb           (many other base types - object, enum, etc.)
   └─ mutations/
      └─ .keep               (empty - we haven't made any mutations yet)

If we take a look inside the routes.rb file, we can see that a new /graphql path is handled by the GraphqlController#execute method:

Notice how the incoming query is passed onto the schema (along with context) for execution. The schema defines only two things right now:

The schema says that mutations can be found in Types::MutationType and queries in Types::QueryType. If we take a look in either, we'll see a dummy field that we can play around with.

One thing that I glossed over in routes.rb is that the gem also mounted the awesome GraphiQL app on the /graphiql path. Visit the /graphiql path in your browser, and try running the following query there:

query {
  testField
}

You should get this response:

{
  "data": {
    "testField": "Hello World!"
  }
}

There you go! Now that we're done with our whirlwind tour of the graphql-ruby gem, let's start digging a bit deeper.

Here Be Dragons

While the graphql-ruby gem has added a ton of functionality to our app, it doesn't really go into detail as to what the best practices are for actually using the gem. Specifically, as mentioned at the beginning of this guide, two issues that are glossed over are:

  1. How to handle authorization, and...
  2. How to efficiently query data.

Just like the official documentation about authorization in GraphQL, the gem's documentation also suggests pushing the responsibility for authorization into business logic, specifically into model methods that accept context and decide what kind of relation or data is accessible for that user.

As for the potential for N+1 queries, it's pretty much ignored altogether - I'm guessing that you're expected to handle this on a case-by-case basis.

I'd like to suggest an alternative: new conventions.

Resolvers authorize and fetch data

Let's start by adding an ApplicationQuery class that'll act as the base class for resolvers and mutators:

With that in place, we can start writing resolver objects that will help us retrieve properly authorized data for GraphQL queries. Let's start by creating a query that asks for a list of users:

Notice how the users method simply hands over the responsibility for loading the data to a UsersResolver object:

What you're looking at is the essence of this approach.

  1. All requests are individually authorized.
  2. There is an assumption that once a query is authorized, all data returned by the resolver (or mutator) can be accessed by the authenticated user.
  3. Avoid N+1-s by making sure that the resolver method includes all necessary data for the response.

Before we move onto mutators, let's also look at how we deal with queries that have arguments, using a variation of what we've done above:

Only a few things are different here:

  1. args is passed to the resolver in addition to context.
  2. There's a property :id in the resolver class defining what data the query will work with.
  3. Instead of a relation, the user method in the resolver returns a User object, since the type for the query is a single object.

Mutators authorize, modify, and supply a response

GraphQL mutations aren't really all that different from queries. Mutations are queries that are, by convention, allowed to modify data. And just like queries, they too can return structured data.

As with queries, let's start with a simple example that shows just how similar mutators are to resolvers.

Notice how there's a call to .valid? before the .create_comment is called. This triggers validations that can be configured in the mutator class:

Again, there's very little that's new here.

  1. Because ApplicationQuery includes ActiveModel::Model, we have access to all of the validation methods that we're familiar with.
  2. The property helper simply combines validates and attr_accessor into a single-step, and helps avoid bugs because the former depends on the latter.
  3. We can either process the request in the mutator directly in the create_comment method, or pass it onto a service as shown in the example.

As with queries, there is an assumption that the create_comment method will return an object that responds to the fields mentioned in the mutation class. In this case, that's id, and as long as the service returns a Comment object, everything should work as expected.

Create types for complex returns

While GraphQL unsubtly suggests the use of relations in your response types, there is no need to follow that pattern. Often, it's much more straight-forward to create a custom type that fits exactly the data that you want to return:

Here, the custom UpdatePostType is used to compose exactly what the UI requires in this imaginary app, when a post is updated.

What are the advantages of this approach?

On the server-side:

  1. You have a self-documenting API. Testing it is a breeze thanks to GraphiQL.
  2. The Rails server will crash with a useful error message if your code ever disobeys the type specification.
  3. Pagination of resources is simple and straight-forward, thanks to built-in, well-thought-out conventions that cover a large variety of pagination-use-cases.
  4. Avoids a lot of bike-shedding. PUT vs PATCH? 400 vs 422? How to handle deprecation? These questions, and more, are no longer concerns.
  5. The server's response can be extended to include more standardized behavior.

On the client-side (assuming that you're using a typed language):

  1. Your API is integrated with the editor - it'll suggest names, arguments, and return values - writing correct queries is much simpler.
  2. Your compiler will prevent the application from generating code with invalid queries.

About extensibility

Your server always supplies a JSON response. This means that you can add more fields to it if you'd like.

In PupilFirst, we've expanded the response object to include a notifications field. If present, the response handler in the client automatically converts them into flash notifications that are shown to the user. This helps us preserve a Rails-like experience in our mutators, and keeps notifications DRY:

The query superclass has simple methods that inject notifications into the context...

...which then gets placed in the response by the GraphQL controller:

However, concerns still exist

This isn't what GraphQL promised

One of the stated advantages of GraphQL is that it solves the problem of over-fetching by allowing the client to specify exactly what data it needs, leading to the server fetching only the asked-for data.

This approach definitely ignores that goal. We're taking this approach because of two reasons:

  1. Over-fetching is not a problem for us. It might become a problem at scale, but we're not at that size yet. It's generally better to tackle problems that exist now (ease of API usage, and avoiding clerical mistakes), instead of one that might happen in the future.
  2. GraphQL doesn't actually do anything to solve over-fetching - it just specifies how to deliver the data once you've retrieved it. However, retrieving data correctly is still up to your business logic, which is always vaguely defined in all documentation that I've come across.

Arbitrarily loading relational data and incurring huge performance hits is one of the easiest mistakes to make with GraphQL, and it's not a problem whose solution is clear. At this point, I think it's appropriate to mention that Shopify has released a graphql-batch gem that claims to tackle this issue. Unfortunately, I think it's poorly documented, and I couldn't really make sense of how it's supposed to work, but it may be worth looking at if you're already at scale, and dealing with issues like over-fetching.

Why not authorize fields?

The simple answer is that it's much easier to think about authorizing requests rather than fields. Requests always have a context which can be used to determine whether this user is allowed to access some data or make a change.

However, if the fields that a client can request are unbounded, i.e., the type allows the client to dig deeper into relationships and ask for distant data, then field-level authorization is your only option. This is why we suggest creating response types specific to queries if the requested data is complicated. Yes, this is restrictive, but requires only one authorization, and ensures that we're limiting the response to a selection of data that we know the client is definitely allowed to access.

How is this any different from REST?

First, I'd like to point you to the list of advantages written above.

You'll notice that the process I've suggested is very similar to how REST works. And you know what? REST has some really good ideas about how to manage communication - it's just that some of its requirements don't make sense anymore when building APIs. REST has an uncomplicated approach to authorization and data-delivery that I think we should adopt even when we're using GraphQL.

A real-world example

If you'd like to take a look at a Rails application that uses this approach, take a look at codebase for the PupilFirst LMS. The patterns described here were created as our team gradually switched to using ReasonML and ReasonReact on the front-end, and adopted GraphQL in order to leverage the presence of types and a compiler.

Discussion (0)

pic
Editor guide