Logo of AppSignal

Menu

An instrumental intro to GraphQL with Ruby

Peter Ohler on

You may have heard developers sing the praises of the wonders of GraphQL. In this series we like to learn technologies by using them, and this article will go through an example application that uses GraphQL.

What is GraphQL

GraphQL is a query language and runtime that can be used to build APIs. It holds a similar position in the development stack as a REST API but with more flexibility. Unlike REST, GraphQL allows response formats and content to be specified by the client. Just as SQL SELECT statements allow query results to be specified, GraphQL allows returned JSON data structures to be specified. Following the SQL analogy, GraphQL does not provide a WHERE clause but identifies fields on application objects that should provide the data for the response.

GraphQL, as the name suggests, models APIs as though the application is a graph of data. While this description may not be how you view your application, it is a model used in most systems. Data that can be represented by JSON is a graph since JSON is just a directed graph. Thinking about the application as presenting a graph model through the API will make GraphQL much easier to understand.

Using GraphQL in an Application

Now that we’ve described GraphQL in the abstract, let’s get down to actually building an application that uses GraphQL by starting with a definition of the data model or the graph. Last year I picked up a new hobby. I’m learning to play the electric upright bass as well as learning about music in general, so a music-related example came to mind when coming up with a demo app.

The object types in the example are Artist and Song. Artists have multiple Song and a Song is associated with an Artist. Each object type has attributes such as a name.

Define the API

GraphQL uses SDL (Schema Definition Language) which is sometimes referred to as “type system definition language” in the GraphQL specification. GraphQL types can, in theory, be defined in any language but the most common agnostic language is SDL so let’s use SDL to define the API.

1
2
3
4
5
6
7
8
9
10
11
12
type Artist {
  name: String!
  songs: [Song]
  origin: [String]
}

type Song {
  name: String!
  artist: Artist
  duration: Int
  release: String
}

An Artist has a name that is a String. The exclamation mark means the field is non-null. songs is an array of Song objects, and origin which is a String array. Song is similar but with one odd field. The release field should be a time or date type but GraphQL doesn’t have that type defined as a core type. For complete portability between different GraphQL implementations, a String is used. The GraphQL implementation we will use does have the Time type added so let’s change the Song definition so that the release field is a Time type. The returned value will be a String, but by setting the type to Time we document the API more accurately.

1
  release: Time

The last step is to describe how to get one or more of the objects. This is referred to as the root or, for queries, the query root. Our root will have just one field or method called artist and will require an artist name.

1
2
3
type Query {
  artist(name: String!): Artist
}

Writing the Application

Let’s look at how we would use this in an application. There are several implementations of a GraphQL server for Ruby. Some approaches require the SDL above to be translated into a Ruby equivalent. Agoo, an HTTP server I’ve built, uses the SDL definition as it is and the Ruby code is plain vanilla Ruby, so that is what we’ll use.

Note that the Ruby classes match the GraphQL types. By having the Ruby class names match the GraphQL type names, we don’t add any unneeded complexity.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Artist
  attr_reader :name
  attr_reader :songs
  attr_reader :origin

  def initialize(name, origin)
    @name = name
    @songs = []
    @origin = origin
  end

  # Only used by the Song to add itself to the artist.
  def add_song(song)
    @songs << song
  end
end

class Song
  attr_reader :name     # string
  attr_reader :artist   # reference
  attr_reader :duration # integer
  attr_reader :release  # time

  def initialize(name, artist, duration, release)
    @name = name
    @artist = artist
    @duration = duration
    @release = release
    artist.add_song(self)
  end
end

Methods match fields in the Ruby classes. Note that the methods either have no arguments or args={}. This is what GraphQL APIs expect and our implementation here follows suit. The initialize methods are used to set up the data for the example, as we will see shortly.

The query root class also needs to be defined. Note the artist method that matches the SDL Query root type. An attr_reader for artists was also added. That would be exposed to the API simply by adding that field to the Query type in the SDL document.

1
2
3
4
5
6
7
8
9
10
11
class Query
  attr_reader :artists

  def initialize(artists)
    @artists = artists
  end

  def artist(args={})
    @artists[args['name']]
  end
end

The GraphQL root (not to be confused with the query root) sits above the query root. GraphQL defines it to optionally have three fields. The Ruby class, in this case, implements on the query field. The initializer loads up some data for an Indie band from New Zealand that I like to listen to.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Schema
  attr_reader :query
  attr_reader :mutation
  attr_reader :subscription

  def initialize()
    # Set up some data for testing.
    artist = Artist.new('Fazerdaze', ['Morningside', 'Auckland', 'New Zealand'])
    Song.new('Jennifer', artist, 240, Time.utc(2017, 5, 5))
    Song.new('Lucky Girl', artist, 170, Time.utc(2017, 5, 5))
    Song.new('Friends', artist, 194, Time.utc(2017, 5, 5))
    Song.new('Reel', artist, 193, Time.utc(2015, 11, 2))
    @artists = {artist.name => artist}

    @query = Query.new(@artists)
  end
end

The final set-up is implementation specific. Here, the server is initialized to include a handler for the /graphql HTTP request path and then it is started.

1
2
Agoo::Server.init(6464, 'root', thread_count: 1, graphql: '/graphql')
Agoo::Server.start()

The GraphQL implementation is then configured with the SDL defined earlier ($songs_sdl) and then the application sleeps while the server processes requests.

1
2
3
4
Agoo::GraphQL.schema(Schema.new) {
  Agoo::GraphQL.load($songs_sdl)
}
sleep

The code for this example can be found on GitHub.

Using the API

To test the API, you can use a web browser, Postman or curl.

The GraphQL query to try that looks like this:

1
2
3
4
5
6
7
8
9
{
  artist(name:"Fazerdaze") {
    name
    songs{
      name
      duration
    }
  }
}

The query asks for the Artist named Fazerdaze and returns the name and songs in a JSON document. For each Song the name and duration of the Song is returned in a JSON object. The output should look like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
  "data":{
    "artist":{
      "name":"Fazerdaze",
      "songs":[
        {
          "name":"Jennifer",
          "duration":240
        },
        {
          "name":"Lucky Girl",
          "duration":170
        },
        {
          "name":"Friends",
          "duration":194
        },
        {
          "name":"Reel",
          "duration":193
        }
      ]
    }
  }
}

After getting rid of the optional whitespace in the query, an HTTP GET made with curl should return that content.

1
curl -w "\n" 'localhost:6464/graphql?query=\{artist(name:"Fazerdaze")\{name,songs\{name,duration\}\}\}&indent=2'

Try changing the query and replace duration with release and note the conversion of the Ruby Time to a JSON string.

Outro to the song

We enjoyed playing around with GraphQL and we hope you tagged along and learned something on the way. Thank you, you’ve been a great audience. If you want to talk more Ruby, we’ll be at the bar selling merch.

This post is written by guest author Peter Ohler. Peter creates quite bit of a high performance code, and writes about it too, every now and then. He made the Agoo gem, which is a pretty cool high performance HTTP server.

10 latest articles

Go back

Subscribe to

Ruby Magic

Magicians never share their secrets. But we do. Sign up for our Ruby Magic email series and receive deep insights about garbage collection, memory allocation, concurrency and much more.

We'd like to set cookies, read why.