GraphQL Mutations in Ruby

Introduction

Welcome to the first lesson of our "GraphQL Mutations and Advanced GraphQL Server" course, part of the "Comprehensive Intro to GraphQL in Ruby" series. In this lesson, you'll learn how to add mutations, which will allow you to modify data on the server.

Revisiting GraphQL Server Basics

We'll start with a quick review of key components without introducing mutations.

  1. Set Up Project.

    First, create a new Ruby project and install the necessary gems. You'll need graphql, sinatra, and securerandom (which is part of Ruby's standard library).

    # Gemfile
    source 'https://rubygems.org'
    
    gem 'graphql'
    gem 'sinatra'
    gem 'json'
  2. Import Required Libraries.

    Import the necessary modules for setting up the GraphQL server.

    require 'sinatra'
    require 'graphql'
    require 'json'
    require 'securerandom'
  3. Define GraphQL Types.

    Define the GraphQL Book type using graphql-ruby's class-based approach.

    class BookType < GraphQL::Schema::Object
      field :id, ID, null: false
      field :title, String, null: false
      field :author, String, null: false
    end
  4. Sample Data.

    Provide some sample book data to be served by our query.

    BOOKS = [
      { id: '1', title: 'The Hobbit', author: 'J.R.R. Tolkien' },
      { id: '2', title: 'Harry Potter', author: 'J.K. Rowling' }
    ]
  5. Define Query Type.

    Create a query type that defines how to fetch book data.

    class QueryType < GraphQL::Schema::Object
      field :books, [BookType], null: false
      field :book, BookType, null: true do
        argument :id, ID, required: true
      end
    
      def books
        BOOKS
      end
    
      def book(id:)
        BOOKS.find { |book| book[:id] == id }
      end
    end
  6. Define Schema.

    Create the GraphQL schema that ties everything together.

    class AppSchema < GraphQL::Schema
      query QueryType
    end
  7. Initialize and Start Server.

    Create a Sinatra server with a GraphQL endpoint.

    post '/graphql' do
      request_payload = JSON.parse(request.body.read)
      query = request_payload['query']
      variables = request_payload['variables'] || {}
      
      result = AppSchema.execute(query, variables: variables)
      content_type :json
      result.to_json
    end
    
    set :port, 4000

Introduction to Mutations

Mutations in GraphQL work like creating, updating, or deleting data records. Let's define the mutations using graphql-ruby's class-based approach.

  1. AddBook Mutation.

    Define a mutation class to add a new book by specifying a title and author.

    class AddBookMutation < GraphQL::Schema::Mutation
      # Arguments: the INPUT data the client sends when calling this mutation
      argument :title, String, required: true
      argument :author, String, required: true
    
      # Fields: the RETURN type — these define what the mutation sends back
      # to the client after it executes. They are NOT additional inputs.
      field :id, ID, null: false
      field :title, String, null: false
      field :author, String, null: false
    
      def resolve(title:, author:)
        new_book = {
          id: SecureRandom.uuid,
          title: title,
          author: author
        }
        BOOKS << new_book
        new_book
      end
    end

    Notice the distinction between argument and field here. The argument declarations define what the client must provide as input (the book's title and author). The field declarations define what the mutation returns in its response — in this case, the newly created book's id, title, and author. Think of argument as the request and field as the response shape.

  2. DeleteBook Mutation.

    Define a mutation class to delete a book by specifying its id.

    class DeleteBookMutation < GraphQL::Schema::Mutation
      argument :id, ID, required: true
    
      field :id, ID, null: true
      field :title, String, null: true
      field :author, String, null: true
    
      def resolve(id:)
        book_index = BOOKS.find_index { |book| book[:id] == id }
        return nil if book_index.nil?
        
        deleted_book = BOOKS.delete_at(book_index)
        deleted_book
      end
    end
  3. Define Mutation Type.

    Create a mutation type that includes all mutations.

    class MutationType < GraphQL::Schema::Object
      field :add_book, mutation: AddBookMutation
      field :delete_book, mutation: DeleteBookMutation
    end
  4. Update Schema.

    Update the schema to include mutations.

    class AppSchema < GraphQL::Schema
      query QueryType
      mutation MutationType
    end

Writing Resolvers for Mutations

Resolvers execute the behavior for a given type in the schema. In graphql-ruby, mutation resolvers are defined as resolve methods within mutation classes.

  1. Adding a Book.

    The resolver method takes the title and author, creates a new book with a unique id using SecureRandom.uuid, adds it to the list, and returns the new book.

    def resolve(title:, author:)
      new_book = {
        id: SecureRandom.uuid,
        title: title,
        author: author
      }
      BOOKS << new_book
      new_book
    end
  2. Deleting a Book.

    The resolver method takes the book id, finds and removes the book from the list using delete_at, and returns the deleted book. If the book is not found, it returns nil.

    def resolve(id:)
      book_index = BOOKS.find_index { |book| book[:id] == id }
      return nil if book_index.nil?
      
      deleted_book = BOOKS.delete_at(book_index)
      deleted_book
    end

Testing Mutations

To test our mutations, we'll use a Ruby script to make HTTP requests to our GraphQL server.

  1. Import Required Libraries.

    Import the necessary libraries for making HTTP requests and handling JSON.

    require 'net/http'
    require 'json'
    require 'uri'
  2. Define Queries and Mutations.

    Define the queries and mutations we want to perform for testing.

    QUERY_BOOKS = <<~GRAPHQL
      query {
        books {
          id
          title
          author
        }
      }
    GRAPHQL
    
    def add_book_mutation(title, author)
      <<~GRAPHQL
        mutation {
          addBook(title: "#{title}", author: "#{author}") {
            id
            title
            author
          }
        }
      GRAPHQL
    end
    
    def delete_book_mutation(id)
      <<~GRAPHQL
        mutation {
          deleteBook(id: "#{id}") {
            id
            title
            author
          }
        }
      GRAPHQL
    end
  3. Function to Execute Requests.

    Create a function to send HTTP requests to the GraphQL server and print the response.

    URL = URI('http://localhost:4000/graphql')
    
    def make_request(query)
      http = Net::HTTP.new(URL.host, URL.port)
      request = Net::HTTP::Post.new(URL.path, { 'Content-Type' => 'application/json' })
      request.body = { query: query }.to_json
      
      response = http.request(request)
      result = JSON.parse(response.body)
      puts JSON.pretty_generate(result)
    rescue StandardError => e
      puts "Error: #{e.message}"
    end
  4. Execute Sample Requests.

    Run a sequence of requests to query books, add a new book, and delete a book, then observe the changes.

    puts "Query all books:"
    make_request(QUERY_BOOKS)
    
    puts "\nAdd a new book:"
    make_request(add_book_mutation("New Book", "New Author"))
    
    puts "\nQuery all books after addition:"
    make_request(QUERY_BOOKS)
    
    puts "\nDelete a book:"
    make_request(delete_book_mutation("1"))
    
    puts "\nQuery all books after deletion:"
    make_request(QUERY_BOOKS)

Expected Output

When running the script, you should see logged outputs similar to:

{
  "data": {
    "books": [
      { "id": "1", "title": "The Hobbit", "author": "J.R.R. Tolkien" },
      { "id": "2", "title": "Harry Potter", "author": "J.K. Rowling" }
    ]
  }
}
{
  "data": {
    "addBook": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "title": "New Book",
      "author": "New Author"
    }
  }
}

Note: The id value above is an example. SecureRandom.uuid generates a random UUID each time, so your output will show a different value in the standard UUID format (e.g., "f47ac10b-58cc-4372-a567-0e02b2c3d479").

{
  "data": {
    "deleteBook": {
      "id": "1",
      "title": "The Hobbit",
      "author": "J.R.R. Tolkien"
    }
  }
}

Review and Next Steps

In this lesson, you learned how to:

  1. Set up a basic GraphQL server using graphql-ruby and Sinatra.
  2. Define a GraphQL schema with mutations.
  3. Write resolver functions for mutations.
  4. Test your mutations using a Ruby script.

Next, you'll get hands-on practice with these concepts through a series of exercises. In the upcoming lessons, we will delve deeper into advanced features and best practices in GraphQL and graphql-ruby.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal