DEV Community

Isa Levine
Isa Levine

Posted on

Ruby on Rails GraphQL API Tutorial: Creating Data with Mutations

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 of query--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 for description and a float for amount (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 new order 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!

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:

  1. Create a BaseMutation class for our mutations to inherit from
  2. Create a specific mutation (createOrder) which inherits from BaseMutation
  3. 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:

  1. name of the argument (:description)--we will use this as the key in our key-value pairs for inputs
  2. data type (String)--the mutation request will throw errors if the correct data type is not provided
  3. 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:

  1. name of the field (:order, :errors)--we will use this as the key in our key-value pairs for our return data
  2. 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]
  3. null, as a boolean (required: true)--this allows us to specify if we are allowed to receive nil fields in our return data
    • null: false will throw an error a field is nil
    • null: true will allow nil 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 the arguments we specified above
  • we immediately use ActiveRecord to create a new Order, using the data from the arguments
  • 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-saved Order instance, and an empty array of errors
    • if failed, we return a nil Order, and an array of errors

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 of query {}--this is necessary for correct routing to /app/graphql/types/mutation_type.rb
  • inside, we call createOrder() as a function, and supply input: as a hash containing our fields as keys
  • immediately following the createOrder() call, we also provide a structure for the returned data, including an order and errors

Here's what we've got in Insomnia right now:

GraphQL mutation in Insomnia

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:

GraphQL mutation in Insomnia, also showing request's return data

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":

GraphQL mutation in Insomnia, showing errors from missing input field

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)

Collapse
 
nozzyez profile image
Mark Sahlgreen

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.

Collapse
 
rpeacan profile image
Ryan Peacan

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!