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.
1type Artist {
2 name: String!
3 songs: [Song]
4 origin: [String]
5}
6
7type Song {
8 name: String!
9 artist: Artist
10 duration: Int
11 release: String
12}
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
.
1type Query {
2 artist(name: String!): Artist
3}
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.
1class Artist
2 attr_reader :name
3 attr_reader :songs
4 attr_reader :origin
5
6 def initialize(name, origin)
7 @name = name
8 @songs = []
9 @origin = origin
10 end
11
12 # Only used by the Song to add itself to the artist.
13 def add_song(song)
14 @songs << song
15 end
16end
17
18class Song
19 attr_reader :name # string
20 attr_reader :artist # reference
21 attr_reader :duration # integer
22 attr_reader :release # time
23
24 def initialize(name, artist, duration, release)
25 @name = name
26 @artist = artist
27 @duration = duration
28 @release = release
29 artist.add_song(self)
30 end
31end
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.
1class Query
2 attr_reader :artists
3
4 def initialize(artists)
5 @artists = artists
6 end
7
8 def artist(args={})
9 @artists[args['name']]
10 end
11end
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.
1class Schema
2 attr_reader :query
3 attr_reader :mutation
4 attr_reader :subscription
5
6 def initialize()
7 # Set up some data for testing.
8 artist = Artist.new('Fazerdaze', ['Morningside', 'Auckland', 'New Zealand'])
9 Song.new('Jennifer', artist, 240, Time.utc(2017, 5, 5))
10 Song.new('Lucky Girl', artist, 170, Time.utc(2017, 5, 5))
11 Song.new('Friends', artist, 194, Time.utc(2017, 5, 5))
12 Song.new('Reel', artist, 193, Time.utc(2015, 11, 2))
13 @artists = {artist.name => artist}
14
15 @query = Query.new(@artists)
16 end
17end
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.
1Agoo::Server.init(6464, 'root', thread_count: 1, graphql: '/graphql')
2Agoo::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.
1Agoo::GraphQL.schema(Schema.new) {
2 Agoo::GraphQL.load($songs_sdl)
3}
4sleep
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 artist(name:"Fazerdaze") {
3 name
4 songs{
5 name
6 duration
7 }
8 }
9}
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 "data": {
3 "artist": {
4 "name": "Fazerdaze",
5 "songs": [
6 {
7 "name": "Jennifer",
8 "duration": 240
9 },
10 {
11 "name": "Lucky Girl",
12 "duration": 170
13 },
14 {
15 "name": "Friends",
16 "duration": 194
17 },
18 {
19 "name": "Reel",
20 "duration": 193
21 }
22 ]
23 }
24 }
25}
After getting rid of the optional whitespace in the query, an HTTP GET made with curl should return that content.
1curl -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.