Setting up GraphQL with Ruby on Rails
Restful APIs are pretty useful, but there is often the problem of over-fetching. The only way to get the name of a person is to fetch a person and get all of their attributes, including needless information like height, gender, race, etc. This is where graphQL comes in! In graphQL you have the ability to fetch specific attributes only, so say goodbye to fetching too much data! Today I will go over the basics of how to create a graphQL backend.
Start by creating a file.
rails new sample-graphqlcd sample-graphqlrails db:createrails s
Add gem “graphql” to the gem file.
bundle installrails generate graphql:installbundle install
Create some sample data. For example using Yu-Gi-OH: duelist, decks, and cards. The relationship
Generate a model for players.
rails generate model duelist name rank:integer
Generate a model for cards.
rails generate model card name attack:integer defense:integer duelist_id:integer
Generate a model for decks.
rails generate model deck name duelist_id:integer
For decks and cards however, the relationship is a little different. Intuitively you might say a deck has many cards, which would be accurate since a deck generally contains 40 cards. However, in all Yu-Gi-Oh games you can make multiple decks, and you can re-use cards that were in a previous deck. (Imagine needing 6 copies of an ultra-rare card if you wanted to include 3 copies of it in two different decks?) So a card can also belong to many decks. Decks have many cards and card have many decks, so you need a join table for the two.
rails generate model deckcard deck_id:integer card_id:integer
Now run:
rails db:migrate
Now we need to add the relationships to the models.
For duelists.
class Duelist < ApplicationRecord
has_many :cards
has_many :decks
end
For cards.
class Card < ApplicationRecord
belongs_to :duelist
has_many :deckcards
has_many :decks, through: :deckcards
end
For decks.
class Deck < ApplicationRecord
belongs_to :duelist
has_many :deckcards
has_many :cards, through: :decks
end
For deckcards.
class Deckcard < ApplicationRecord
belongs_to :card
belongs_to :deck
end
Now let’s create some seed data in db > seed.db.
duelist1 = Duelist.create(name: "Yugi", rank: 1)
duelist2 = Duelist.create(name: "Kaiba", rank: 2)
duelist3 = Duelist.create(name: "Joey", rank: 3)card1 = Card.create(name: "Dark Magician", attack: 2500, defense: 2100, duelist_id: duelist1.id)
card2 = Card.create(name: "Blue-Eyes White Dragon", attack: 3000, defense: 2500, duelist_id: duelist2.id)
card3 = Card.create(name: "Red-Eyes Black Dragon", attack: 2400, defense: 2000, duelist_id: duelist3.id)deck1 = Deck.create(name: "Yugi Deck", duelist_id: duelist1.id)
deck2 = Deck.create(name: "Kaiba Deck", duelist_id: duelist2.id)
deck3 = Deck.create(name: "Joey Deck", duelist_id: duelist3.id)
deck4 = Deck.create(name: "Joey Deck 2", duelist_id: duelist3.id)Deckcard.create(deck_id: deck1.id, card_id: card1.id)
Deckcard.create(deck_id: deck2.id, card_id: card2.id)
Deckcard.create(deck_id: deck3.id, card_id: card3.id)
Deckcard.create(deck_id: deck4.id, card_id: card3.id)
Now we need to create graphQL object types.
rails generate graphql:object duelist
rails generate graphql:object card
rails generate graphql:object deck
Now we need to fill out the duelist, card, and deck type files. graphQL > types > duelist_type.rb
module Types
class DuelistType < GraphQL::Schema::Object
field :id, ID, null: false
field :name, String, null: false
field :rank, Integer, null: false
field :cards, [CardType], null: true
field :decks, [DeckType], null: true
end
end
By default it assumes a value can be null, so if a field is mandatory write null: false next to it.
Do the same for deck_type.rb:
module Types
class DeckType < GraphQL::Schema::Object
field :id, ID, null: false
field :name, String, null: false
field :duelist, DuelistType, null: false
field :cards, [CardType], null: true
end
end
And the same again for card_type.rb:
module Types
class CardType < GraphQL::Schema::Object
field :id, ID, null: false
field :name, String, null: false
field :attack, Integer, null: true
field :defense, Integer, null: true
field :duelist, DuelistType, null: false
field :decks, [DeckType], null: true
end
end
Now for the query form. This is basically the graphQL version of the controllers when making a restful api.
graphQL > types > query_type.rb
Write a query for getting all duelists:
module Types
class QueryType < Types::BaseObject
description "root query" field :duelists, [DuelistType], null: true do
description "gets all duelists"
end def duelists
Duelist.all
end
end
end
Now go to http://localhost:3000/graphiql
The left side is where you can enter queries to retrieve data. Try entering:
{
duelists {
name
rank
id
cards {
name
attack
defense
}
decks {
name
}
}
}
On the right side you will get back an array of all the duelists, as well as their name, rank, and an array for their cards and decks.
To write a query for a single duelist add the following lines to query_type.rb
field :duelist, DuelistType, null: true do
description "find duelist by id"
argument :id, ID, required: true
enddef duelist(id:)
Duelist.find(id)
end
Notice the change from “field :duelists, [DuelistType], null: true do” to “field :duelist, DuelistType, null: true do”. This is because the query to get all duelists returns an array of them all, while the query to find a single duelist returns only that duelist as an object.
To do a single query go back to http://localhost:3000/graphiql and for the query enter:
{
duelist(id: 1) {
name
rank
id
cards {
name
attack
defense
}
decks {
name
}
}
}
You should get back only a single duelist object with the id of 1, Yugi in this case.
Add the same all search and single search queries for cards and decks. It should end up looking like:
module Types
class QueryType < Types::BaseObject
description "root query" field :duelists, [DuelistType], null: true do
description "get all duelists"
end field :duelist, DuelistType, null: true do
description "find duelist by id"
argument :id, ID, required: true
end field :cards, [CardType], null: true do
description "get all cards"
end field :card, CardType, null: true do
description "find card by id"
argument :id, ID, required: true
end field :decks, [DeckType], null: true do
description "get all decks"
end field :deck, DeckType, null: true do
description "find deck by id"
argument :id, ID, required: true
end def duelists
Duelist.all
end def duelist(id:)
Duelist.find(id)
end def cards
Card.all
end def card(id:)
Card.find(id)
end def decks
Deck.all
end def deck(id:)
Deck.find(id)
end
end
end
You can now run all or single queries on each of them.
We now need to build for mutations, which allows us to post, modify, and delete items from the database. Create a root mutation.
touch app/graphql/mutations/base_mutation.rb
Inside write the following code:
class Mutations::BaseMutation < GraphQL::Schema::Mutationend
Now we need to create a mutation to create, update, and delete duelists, cards, and decks.
rails g graphql:mutation create_duelist.rb
rails g graphql:mutation update_duelist.rb
rails g graphql:mutation create_card.rb
rails g graphql:mutation update_card.rb
rails g graphql:mutation delete_card.rb
rails g graphql:mutation create_deck.rb
rails g graphql:mutation update_deck.rb
rails g graphql:mutation delete_deck.rb
Modify the create, update, and delete files for each one.
For create duelist:
module Mutations
class CreateDuelist < GraphQL::Schema::RelayClassicMutation
field :duelist, Types::DuelistType, null: false argument :name, String, required: true
argument :rank, Integer, required: false def resolve(name:, rank:)
duelist = Duelist.create!(name: name, rank: rank) {
duelist: duelist
}
end end
end
Update duelist:
module Mutations
class UpdateDuelist < GraphQL::Schema::RelayClassicMutation
field :duelist, Types::DuelistType, null: false argument :id, ID, required: true
argument :name, String, required: true
argument :rank, Integer, required: false def resolve(id:, name:, rank:)
duelist = Duelist.find(id)
duelist.update!(name: name, rank: rank) {
duelist: duelist
}
end
end
end
Delete duelist:
module Mutations
class DeleteDuelist < GraphQL::Schema::RelayClassicMutation
field :duelist, Types::DuelistType, null: false argument :id, ID, required: true def resolve(id:)
duelist = Duelist.find(id)
duelist.destroy! {
duelist: duelist
}
end
end
end
Do the equivalent for all three, changing the class names and attributes to fit to the corresponding model.
You can try adding a duelist by running the following query:
mutation {
createDuelist(input: {name: "Marik", rank: 5}){
duelist {
name,
rank
}
}
}
Now if you fetch all duelists again with
{
duelists {
name
rank
cards {
name
attack
defense
}
decks {
name
}
}
}
You will see that our new duelist, Marik has been added to the list!
To update a duelist enter:
mutation {
updateDuelist(input: {id: 5, name: "Zane", rank: 1}){
duelist {
name,
rank
}
}
}
If you get all duelists again you will see that duelist with id number 4, formerly known as Marik, will now have his name changed to “Zane” and his rank changed to 1.
To delete the newest duelest enter:
mutation {
deleteDuelist(input: {id: 4}){
duelist {
name,
rank
}
}
}
Now if you get all duelists again you will see that Zane is gone, and only the original one are left.
Congratulations, your graphQL API is all set up! We have successfully set up a backend api where seed data can be created, objects can be fetched (and specific attributes from them can be fetched too!), and objects can be added, updated, or destroyed! My next article will go into how to connect this to a react app using Apollo!