Introduction to GraphQL

Hi everyone,

This is going to be an introduction to GraphQL that will explain what it is and how to implement it in a Rails application.

GraphQL

The first thing to understand is what is GraphQL.

GraphQL is a query language for your API that allows you to specify the data structure that you need at the client-side instead of the server-side, for example:

{
  product(id: 42) {
    id
    name
    price
    comments {
      id
      author
      body
    }
  }
}

This query, written at the client-side, is specifying that you want the product 42 containing its id, name, price, and comments with id, author, and body.

The returned data will be similar to:

{
  "data": {
    "product": {
      "id": "1",
      "name": "Wallet",
      "price": 4.99,
      "comments": [
        {
          "id": "1",
          "author": "John Doe",
          "body": "Cool wallet"
        }
      ]
    }
  }
}

This is an inversion from what we are used to comparing to REST APIs, where the server is who decides what attributes are going to be rendered instead of the client, regardless of it being used or not.

GraphQL in Rails

Configure

All you need is to install the graphql-ruby gem:

gem 'graphql-ruby'  

And then generate the boilerplate using a generator:

bin/rails g graphql:install  

This will create a schema, a query object type, an HTTP endpoint and will install graphiql.

For the sake of simplicity, you may learn about these concepts later as we are going to be more straightforward here.

If you want to understand them, I recommend you to walk through the getting started guide.

Schema

To understand the basics, we are going to create a standard schema:

bin/rails g model product name:string price:float  
bin/rails g model comment product:belongs_to author:string body:text  

Do not forget to migrate the database and to add the has many association on Product:

class Product < ApplicationRecord  
  has_many :comments
end  

And to seed your database like that:

wallet = Product.create!(name: 'Wallet', price: 4.99)  
tshirt = Product.create!(name: 'T-Shirt', price: 16.90)

Comment.create!(product: book, author: 'John Doe', body: 'Cool wallet')  
Comment.create!(product: p2, author: 'Jane Doe', body: 'Cool t-shirt')  

Object Types

What you need to know right now is that Object Types performs a similar role of a JSON serializer, pretty much like a ActiveModel::Serializer.

You specify them to say "hey, these are the attributes that I want to expose for this kind of data".

You can generate them using the graphql:object generator but there are some pitfalls there, I recommend you to implement them by yourself for now:

# app/graphql/types/comment_type.rb
Types::CommentType = GraphQL::ObjectType.define do  
  name "Comment"

  field :id, types.ID
  field :author, types.String
  field :body, types.String
end  
# app/graphql/types/product_type.rb
Types::ProductType = GraphQL::ObjectType.define do  
  name "Product"

  field :id, types.ID
  field :name, types.String
  field :price, types.Float
  field :comments, types[Types::CommentType]
end  

You are in essence saying that a comment is allowed to render the id, author and body and product is allowed to render the id, name, price and comments (note the difference in the syntax for specifying an array attribute).

Now, all you need is to specify the API endpoints on your query object type:

# app/graphql/types/query_type.rb
Types::QueryType = GraphQL::ObjectType.define do  
  name "Query"

  field :products do
    type types[Types::ProductType]
    resolve -> (object, args, ctx) {
      Product.all
    }
  end

  field :product do
    type Types::ProductType
    argument :id, !types.ID
    resolve -> (obj, args, ctx) {
      Product.find(args[:id])
    }
  end
end  

You are saying that you have an object type called products that returns a collection of Types:: ProductType and an object type called product that has a required attribute called id and that it returns a Types::ProductType.

Think that you are implementing the /products and /products/{id} endpoints.

Observations

Pros

What are the benefits, after all?

Well, if you have different clients that consumes your API in different ways, that's a way to go.

Imagine that you have a SPA that renders all products with its comments at the products page.

You can specify a full query like that:

{
  products {
    id
    name
    price
    comments {
      id
      author
      body
    }
  }
}

Now imagine that you have a mobile application that the products page only renders the product name and when you tap on it, you fetch the price and the comments.

You can hit the products like that:

{
  products {
    id
    name
  }
}

And when the user taps the product, you make another query like this:

{
  product(id: 42) {
    price
    comments {
      id
      author
      body
    }
  }
}

That will save server resources, bandwidth resources, and client resources.

This is a contrived example but as you dive into GraphQL, you will understand that you may have much more complex queries like this:

{
  recentProducts: products(filter: 'recent') {
    ...products

    comments(limit: 10) {
      id
      author
      body
    }
  }

  trendingProducts: products(filter: 'trending') {
    ...products
  }
}

fragment products on Product {  
  id
  name
  small: picture(size: '300x300')
  large: picture(size: '600x600')
}

That basically extracts a common definition to a fragment, does two queries with different arguments and by hitting the GraphQL endpoint just once, name these two queries into different keys and query the comments only for recent products and limits it to 10.

This is just a small piece of what can be done, I will explore more on that in another post.

Cons

Is important to understand that all queries are sent to one HTTP endpoint.

This means that standard profiling tools like New Relic and Skylight won't help, at least not yet.

That will be also left for another post but it is possible to profile the performance using specialized tools like Apollo Optics.

Another point is that will increase the learning curve of your application a lot and that may not be desired.

TL;DR

IMO, only makes sense to use if:

  1. You have a lot of endpoints being hit at once as they will be collapsed into one hit
  2. You need different response shapes for different cases (SPA vs mobile)
  3. You need to improve the performance of your API
    1. You may use graphql-batch or dataloader to fix N+1 queries in a bigger context than AR solves natively
    2. You may use client-side caching by using Relay or a similar tool

If you don't have any of these problems, probably GraphQL isn't for your application.