GraphQL with Sinatra Web FrameworkWe explore how to create a GraphQL server using the Sinatra framework in Ruby.

header-nodes-graph
iMango Admin

GraphQL has redefined the way we deal with APIs and endpoints. With the client being able to specify the fields it needs, development in the client could in one way be independent of the server changes, if a schema is predefined.

GraphQL + Sinatra

We’ll see how to create a GraphQL server using the Sinatra framework in Ruby. In this example we will be creating the schema for a conference app, where you can add speakers and list them.

STEP 1: Create a Sinatra application

I wish there was a template to create a sinatra application using a cli. But there isn’t a lot of boilerplate files to create, so lets add it one-by-one.

We’ll be using puma as the server. Create an app.rb file which defines a basic sinatra app with a / route. Also a Gemfile is added and bundle install is run.

require ‘sinatra’
class ConferenceApp < Sinatra::Base
get ‘/’ do
‘It Works!’
end
end
view raw app.rb hosted with ❤ by GitHub

Next we need a rackup file config.ru so that puma will pickup the app as a rack application.

require ‘./app’
run ConferenceApp
view raw config.ru hosted with ❤ by GitHub

Running the puma server will serve your application at http://localhost:9292, and you should see the message ‘It Works!’.

bundle exec puma

Yay! The app is up.

STEP 2: Add JSON responses

For the server to respond to JSON, we add the sinatra-contrib gem, which adds a JSON helper. Change the app.rb file to respond to json.

#file app.rb
require ‘sinatra/json’
#…
get ‘/hello.json’ do
message = { success: true, message: ‘hello’}
json message
end
#…
view raw app_2.rb hosted with ❤ by GitHub

Now our app contains just these files:

conference_app
  |
  ├── Gemfile
  ├── Gemfile.lock
  ├── app.rb
  └── config.ru

STEP 3: Add database connections and models with ActiveRecord

For talking to the database, we’ll use activerecord gem.

Add database configuration to connect to sqlite3

Also add a configuration file database.yml with the connection details and the sqlite3 gem for connecting to the sqlite database. app.rb needs changes to update this configuration.

#changes in app.rb
# …
require ‘sinatra/activerecord’
# …
class ConferenceApp < Sinatra::Base
set :database_file, ‘config/database.yml’
# …
# …
end
view raw app_3.rb hosted with ❤ by GitHub

Add Rakefile

Add the rake gem along with the Rakefile. This gives handy rake tasks for creating the table (migrations) and managing them.

# Rakefile
require ‘./app’
require ‘sinatra/activerecord/rake’
view raw Rakefile hosted with ❤ by GitHub

 

bundle exec rake -T will display the added rake tasks.

Create the sqlite database, by running bundle exec rake db:create.

Add a migration and model for Speaker object

Create a migration with the following rake command:

bundle exec rake db:create_migration NAME=create_speakers

Change the created migration file in db/migrate folder, to add the required database fields.

class CreateSpeakers < ActiveRecord::Migration[5.1]
def change
create_table :speakers do |t|
t.string :name, null: false
t.string :twitter_handle
t.text :bio
t.string :talk_title
end
end

Run migrations with the rake task bundle exec rake db:migrate

Create a model file for Speaker, to access this table.

# file models/speaker.rb
class Speaker < ActiveRecord::Base
validates :name, presence: true
end
view raw speaker.rb hosted with ❤ by GitHub

I’ve added a basic validation for the model. Read more on activerecord basics in the official basics introduction.

Add the pry gem for debugging and execute the following two statements in the pry console, for adding rows to the speakers table.

Seed Speakers

require './app'

Speaker.create(name: 'John', twitter_handle: 'johnruby',
  bio: 'This is John\'s bio', talk_title: 'How to bootstrap a sinatra application')

Speaker.create(name: 'Jacob', twitter_handle: 'jacob-ruby',
  bio: 'This is Jacob\'s bio', talk_title: 'Introduction to graphql')

Add a /speakers endpoint

Create a new endpoint to show the list of speakers, as JSON.

# Changes to file app.rb
# …
require ‘sinatra/activerecord’
require_relative ‘models/speaker’
class ConferenceApp < Sinatra::Base
# …
get ‘/speakers’ do
@speakers = Speaker.all
json @speakers
end
end
view raw app_4.rb hosted with ❤ by GitHub

STEP 4: Add graphql and define a query to list speakers

Now we have a sinatra app that connects to the database and shows a list of speakers as a JSON response. Now let’s add graphql and define a schema for speakers.

Add the graphql gem. https://github.com/rmosolgo/graphql-ruby.

Also the rack-contrib gem needs to be added so that the sinatra app can accept raw JSON payloads.

Add type, query and schema for graphql

Now we need to add a type for Speaker, also a query and a schema for GraphQL.

require ‘graphql’
require_relative ‘base_object’
class Types::Speaker < Types::BaseObject
description ‘Resembles a Speaker Object Type’
field :id, ID, null: false
field :name, String, null: false
field :twitter_handle, String, null: true
field :bio, String, null: true
field :talk_title, String, null: true
end
view raw speaker.rb hosted with ❤ by GitHub

We need to then add a root query.

require ‘graphql’
require_relative ‘types/speaker’
class QueryType < GraphQL::Schema::Object
description “The query root of this schema”
field :speakers, [Types::Speaker], null: false do
description ‘Get all speakers of the system’
end
def speakers
Speaker.all
end
end
view raw query.rb hosted with ❤ by GitHub

Define a schema for GraphQL.

require ‘graphql’
require_relative ‘query’
class ConferenceAppSchema < GraphQL::Schema
query QueryType
end
view raw schema.rb hosted with ❤ by GitHub

The /graphql endpoint

We now need to have a POST endpoint for GraphQL.

GraphQL schema can be executed to give a GraphQL::Query::Result which can then be converted to JSON. app.rb needs change to include this endpoint.

# Changes to file app.rb
# …
require ‘rack/contrib’
class ConferenceApp < Sinatra::Base
# …
use Rack::PostBodyContentTypeParser
# …
# …
post ‘/graphql’ do
result = ConferenceAppSchema.execute(
params[:query],
variables: params[:variables],
context: { current_user: nil },
)
json result
end
# …
end
view raw app_5.rb hosted with ❤ by GitHub

Querying the endpoint

You can use the GraphiQL app or the Postman app to query the endpoint. Make sure that you have puma running and the server is up.

Postman Query Graphql

A JSON response like the below will be obtained.

{
“data”: {
“speakers”: [
{
“name”: John,
“twitterHandle”: johnruby,
“bio”: This is John’s bio
},
{
“name”: Jacob,
“twitterHandle”: jacob-ruby,
“bio”: This is Jacob\\‘s bio
}
]
}
}

You have a GraphQL server up and running on sinatra, and you can query the endpoint to get a list of speakers with the fields defined in the GraphQL query.

Now let us add mutations.

Mutations

A mutation is something that “mutates” or changes the data in the server. In DB terms, if we need to change the data in a table using graphql we need mutations — be it an INSERT, UPDATE or DELETE. Only SELECTs are covered with a Query.

So to add a new speaker to the database we need a mutation.

Graphql Mutations

In the GraphQL language, a mutation is of the form

mutation AddSpeaker($name:String, $talkTitle:String) {
  createSpeaker(name: $name, talkTitle:$talkTitle) {
    success
    errors
  }
}

A set of “query” variables needs to be supplied to the GraphQL endpoint. Say for example,

{
  "name": "John Doe",
  "talkTitle": "Introduction to GraphQL in Ruby"
}

Read more about GraphQL mutations and its syntax in the specifications — https://graphql.org/learn/queries/#mutations.

For our little server to accept mutations, we need to make some changes and add more files for defining mutations. Lets see how, step-by-step.

STEP 5: Adding a Mutation root type

A mutation root MutationType has to be created and it should then be added to our Schema, like the QueryType that was added in the last post.

require ‘graphql’
require_relative ‘mutations/create_speaker’
class MutationType < GraphQL::Schema::Object
description “The mutation root of this schema”
field :createSpeaker, mutation: Mutations::CreateSpeaker
end
view raw mutation.rb hosted with ❤ by GitHub
require ‘graphql’
require_relative ‘query’
require_relative ‘mutation’
class ConferenceAppSchema < GraphQL::Schema
query QueryType
mutation MutationType
end
view raw schema.rb hosted with ❤ by GitHub

STEP 6: Define a Mutation for speaker creation

Next, we need to tell GraphQL about the parameters that needs to be accepted for creating a new speaker.

Let’s split this into a separate file, that handles this mutation — create_speaker.rb. Instead of inheriting from GraphQL::Schema::Mutation we create a mutation base class Mutations::BaseMutation. Also group all the mutations in mutations folder.

require ‘graphql’
module Mutations
class BaseMutation < GraphQL::Schema::Mutation
end
end
require ‘graphql’
require_relative ‘base_mutation’
class Mutations::CreateSpeaker < Mutations::BaseMutation
description ‘Creates a speaker’
# input arguments for the mutation
# function to execute on mutation
end

It needs to accept all the fields for a speaker, which we created as strings in the DB. In GraphQL ruby, strings are represented with the String type, as defined in the gem.

require ‘graphql’
require_relative ‘base_mutation’
class Mutations::CreateSpeaker < Mutations::BaseMutation
description ‘Creates a speaker’
argument :name, String, required: false
argument :bio, String, required: false
argument :twitter_handle, String, required: false
argument :talk_title, String, required: false
# …
end

Next we need to take these fields and then call speaker.save with the defined input fields in the resolve function.

require ‘graphql’
require_relative ‘base_mutation’
class Mutations::CreateSpeaker < Mutations::BaseMutation
description ‘Creates a speaker’
# input arguments
def resolve(name:, bio:, twitter_handle:, talk_title:)
speaker = Speaker.new(
name: name,
bio: bio,
twitter_handle: twitter_handle,
talk_title: talk_title
)
if speaker.save
{
success: true,
errors: []
}
else
{
success: false,
errors: speaker.errors.full_messages
}
end
end

 

This returns a hash with success and errors. We need to tell GraphQL about it as well. Note: errors is an array of Strings. We define these as “fields” in the mutation.

require ‘graphql’
require_relative ‘base_mutation’
class Mutations::CreateSpeaker < Mutations::BaseMutation
description ‘Creates a speaker’
# input arguments
field :success, Boolean, null: false
field :errors, [String], null: false
# …
# resolve function
# …
end

Now the CreateSpeaker mutation is complete. It needs to be added to the root mutation — MutationType so that it gets included in the schema.

require ‘graphql’
require_relative ‘mutations/create_speaker’
class MutationType < GraphQL::Schema::Object
description “The mutation root of this schema”
field :createSpeaker, mutation: Mutations::CreateSpeaker
end
view raw mutation2.rb hosted with ❤ by GitHub

Restart the server with bundle exec puma.

If you use a client like GraphiQL, you should be able to see the docs in the right sidebar changes and now has a Mutation.

Add a new speaker, with the mutation and the query variables in the client.

mutation AddSpeaker($name:String, $talkTitle:String, $bio:String, $twitterHandle:String) {
createSpeaker(name: $name, talkTitle: $talkTitle, bio: $bio, twitterHandle: $twitterHandle) {
success
errors
}
{
“name”: Adam Smith,
“talkTitle”: Web Development Fundamentals,
“twitterHandle”: adamsm,
“bio”: Adam is a seasoned web developer, who has worked as a Software Engineer in Google, and now is the force behind the GraphiQL web client
}

Execute the mutation in the GraphiQL client. You should be able to see the response data json, with something like below:

GraphiQL Mutation Execution

The CreateSpeaker mutation has all the fields optional, but the Speaker model validates the presence of the name field. If you try to create a speaker without giving a name field, it will show up in errors return field.

We can avoid this validation at the schema level, by making the required argument mandatory. You just need to change null: false to the respective arguments.

module SpeakerMutations
Create = GraphQL::Relay::Mutation.define do
name ‘CreateSpeaker’
description ‘Creates a speaker’
input_field :name, !types.String # note the !
input_field :bio, types.String
input_field :twitterHandle, types.String
input_field :talkTitle, !types.String
# … rest of the mutation body
end

Now the name and talk_title fields are non-nullable fields, and you’ll have to always give these fields when executing the mutation. Read about the type system in the official documentation.

Looking for a First-Class Business Plan Consultant?