Continuing from the last installment, we're going to introduce the other type of request in GraphQL: Mutations.
Mutations allow us to create, update, or destroy data in our database. Paired with the Query's ability to read data, we now have a full set of CRUD actions!
In this tutorial, we will focus on building a simple mutation to create a new Order
record in our database.
Once again, this tutorial is largely adapted from this AMAZING tutorial by Matt Boldt. Thanks again, Matt!!
Overview
In this second article, we'll go through the steps to:
- create the needed Mutation files
- create our first mutation (add a new
Order
to the database) - add routing to
/app/graphql/types/mutation_type.rb
- write and execute our first GraphQL Mutation in Insomnia
Let's dive in!
What is a GraphQL Mutation?
From the GraphQL docs:
Most discussions of GraphQL focus on data fetching, but any complete data platform needs a way to modify server-side data as well.
(Emphasis mine)
Mutations are exactly that--a request that modifies (creates, updates, destroys) data in the database.
GraphQL mutation format
Similar to Queries, this is what our first GraphQL mutation will look like:
mutation {
createOrder(input: {
description: "Octo Octa - Resonant Body (vinyl)",
total: 21.82
}) {
order {
id
description
total
payments {
id
amount
}
paymentsCount
}
errors
}
}
Several things to notice:
- At the very top, we define our request as a
mutation
instead ofquery
--this is necessary for routing the request correctly, which we'll cover below -
createOrder
is the name we will give to our mutation, and we call it like a function with the arguments inside parentheses:createOrder( input: {} )
- the mutation accepts
input
, structured as a hash, where we pass a string fordescription
and a float foramount
(we can set these inputs to optional, or make them required) - following the closing parentheses from
createOrder()
, we add another hash to outline the data we want to return from the mutation- notice that it looks essentially the same as our first query, except that we also return
errors
along with the neworder
we created - this return is optional, but it is a good idea--you can both mutate the database AND query the data in one atomic operation, which is very efficient!
- notice that it looks essentially the same as our first query, except that we also return
Okay, now that we know what our mutation request is going to look like, let's actually build out the code!
Adding mutations to our /app/
Navigate to the /app/graphql/mutations
directory, and you'll notice it's empty (except for that empty .keep
file). Right now, our app does not have any mutations. To add them in, we'll need to do a few things:
- Create a
BaseMutation
class for our mutations to inherit from - Create a specific mutation (
createOrder
) which inherits fromBaseMutation
- Add routing to
/app/graphql/types/mutation_type.rb
(much like we did with/app/graphql/types/query_type.rb
in the previous article)
After that, we can repeat steps 2 and 3 for each new mutation we want to add!
Creating a mutation
Create new file /app/graphql/mutations/base_mutation.rb
Let's create a base type for our other mutations to inherit from. Create a new base_mutation.rb
file in the (mostly) empty /app/graphql/mutations
directory, and add this code:
# /app/graphql/mutations/base_mutation
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
end
That's it!
Note that the module we're inheriting from is RelayClassicMutation
--the GraphQL Ruby gem actually has another GraphQL::Schema::Mutation
module, but RelayClassicMutation
gives us some nice functionality, especially around simplifying input:
to accept a single hash.
Create new file for our specific mutation
In the same /app/graphql/mutations
directory, add another file. This will be the same name as the mutation, but in snake-case: for the createOrder
mutation, name the file /app/graphql/mutations/create_order.rb
.
Here's what we'll add to the file to create the mutation:
# /app/graphql/mutations/create_order.rb
class Mutations::CreateOrder < Mutations::BaseMutation
argument :description, String, required: true
argument :total, Float, required: true
field :order, Types::OrderType, null: false
field :errors, [String], null: false
def resolve(description:, total:)
order = Order.new(description: description, total: total, reference_key: SecureRandom.uuid)
if order.save
{
order: order,
errors: []
}
else
{
order: nil,
errors: order.errors.full_messages
}
end
end
end
Let's step through what we're seeing after we declare the class:
arguments
# /app/graphql/mutations/create_order.rb
argument :description, String, required: true
argument :total, Float, required: true
Here, we specify the arguments that our mutation request can take. After argument
, we supply three things:
- name of the argument (
:description
)--we will use this as the key in our key-value pairs for inputs - data type (
String
)--the mutation request will throw errors if the correct data type is not provided - required, as a boolean (
required: true
)--this allows us to specify if an argument must be present to succeed-
required: true
will throw an error if not present -
required: false
will make the argument optional
-
fields
# /app/graphql/mutations/create_order.rb
field :order, Types::OrderType, null: false
field :errors, [String], null: false
Here, we specify the fields that our mutation request returns. Our two fields, :order
and :errors
, will handle two cases:
- if the mutation is successful, an
Order
object will be returned (just like a Query) - if the mutation fails, we will get back a set of errors
After field
, we supply three things:
- name of the field (
:order
,:errors
)--we will use this as the key in our key-value pairs for our return data - data type (
Types::OrderType
,[String]
)--these tell us what type of data to expect in the return- as with Queries, we can specify any data type, including our custom types created in the
/app/graphql/types/
directory - we can also return an array of another type, such as returning an array of errors as strings via
[String]
- as with Queries, we can specify any data type, including our custom types created in the
- null, as a boolean (
required: true
)--this allows us to specify if we are allowed to receivenil
fields in our return data-
null: false
will throw an error a field isnil
-
null: true
will allownil
data to be returned without errors
-
resolve()
# /app/graphql/mutations/create_order.rb
def resolve(description:, total:)
order = Order.new(description: description, total: total)
if order.save
{
order: order,
errors: []
}
else
{
order: nil,
errors: order.errors.full_messages
}
end
end
This is where the magic happens! All mutations have a resolve()
function, and that function returns (as a hash) what the mutation itself will return. Let's step through the code:
-
resolve()
takes thearguments
we specified above - we immediately use ActiveRecord to create a new
Order
, using the data from thearguments
- in a familiar Rails pattern, we test if the
Order
can be saved in the database- if successful, we return a hash with the
order
key containing our newly-savedOrder
instance, and an empty array of errors - if failed, we return a
nil
Order
, and an array of errors
- if successful, we return a hash with the
This is a very straightforward resolve()
method, but it can easily be added to! You can make and call helper functions within the CreateOrder
class, so long as the resolve() function ultimately returns a hash for the mutation's response.
We're almost read to try out our first mutation! But first, we have to make sure our app knows how to route the mutation request properly.
Add routing to /app/graphql/types/mutation_type.rb
By default, our /app/graphql/types/mutation_type.rb
will be created with only a test-field that returns "Hello World", which we are directed to remove. Go ahead and delete that now, so that our class looks nice and empty:
# /app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
end
end
Perfect, a blank slate!
Inside here, we now simply specify a field
that looks for our mutation's name, and routes to the correct mutation class:
# /app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
field :create_order, mutation: Mutations::CreateOrder
end
end
Now, when we send a GraphQL mutation called createOrder
, the arguments we provide will be sent to the Mutations::CreateOrder
class we just created in /app/graphql/mutations/create_order.rb
!
We're finally ready to test!!
Executing our first Mutation
Go ahead and run rails s
in your terminal to start our Rails server on http://localhost:3000/graphql/
.
Next, open up Insomnia and create a new POST request, and set it to a GraphQL structured query.
Then, add in our mutation code from above:
mutation {
createOrder(input: {
description: "Octo Octa - Resonant Body (vinyl)",
total: 21.82
}) {
order {
id
description
total
payments {
id
amount
}
paymentsCount
}
errors
}
}
Note that there are a couple differences from the Query we created last time:
- the top line says
mutation {}
instead ofquery {}
--this is necessary for correct routing to/app/graphql/types/mutation_type.rb
- inside, we call
createOrder()
as a function, and supplyinput:
as a hash containing our fields as keys - immediately following the
createOrder()
call, we also provide a structure for the returned data, including anorder
anderrors
Here's what we've got in Insomnia right now:
Go ahead and send the request, and you'll see it comes back successful. We can access our newly created (and saved!) Order
via the order
key. We can also see that our array of errors is empty:
Now, let's test a failed mutation. Remove the description:
field of the input entirely:
mutation {
createOrder(input: {
total: 21.82
}) {
order {
id
description
total
payments {
id
amount
}
paymentsCount
}
errors
}
}
And resend the request, and you'll see it comes back with an error describing a missing required input field named "description":
Great! The errors provided by the GraphQL Ruby gem are very explicit, so make sure to read them closely when debugging.
Conclusion
We've now implemented a GraphQL API with the ability to read Orders
from our database, and now create new Orders
too! From here, we can build new mutations to handle updating and deleting data as well.
Here's the repo for the code in this article, too. Feel free to tinker around with it!
And once again -- thank you to Matt Boldt and his AWESOME Rails GraphQL tutorial for helping me get this far! <3
Any tips or advice for using GraphQL in Rails, or GraphQL in general? Feel free to contribute below!
Top comments (2)
Great introduction, I would have liked to see some examples of how you would update orders and delete them too though, since I am personally having issues with getting comfortable with these, deletions in particular.
Thanks for these articles! I'm feeling I definitely want to be using GraphQL in my next project, and I'm comfortable with Rails so this seems like a good fit, great timing and information!
Question for you: I keep spinning my wheels on what to use for authentication that will play nice with GraphQL and Rails... I've used Devise for standard Rails projects, but this will be my first time handling authentication for a React front-end tied to a Rails install... I'd love to hear any information you could recommend!
Thanks again for the articles!