Querying with Arguments

Introduction to Querying with Arguments in GraphQL

In our previous lessons, we've discussed setting up a basic GraphQL server using graphql-ruby and Sinatra, and creating basic types and queries. In this lesson, we will focus on querying with arguments in GraphQL, which allows you to retrieve specific data based on parameters you provide.

To understand querying with arguments, consider that you might want to fetch details about a specific book from a large collection. Instead of retrieving all books and filtering on the client side, you can pass an argument to your query to get just the book you need directly from the server.

Defining the GraphQL Schema with Arguments

To query specific data, we need to extend our GraphQL schema to include arguments. Recall from previous lessons that the schema defines the types of data and the shape of our queries.

In graphql-ruby, we define types as Ruby classes that inherit from GraphQL::Schema::Object. Here's how we define our Book type:

Ruby
require 'graphql'

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

In this type definition:

  • The BookType class inherits from GraphQL::Schema::Object.
  • Each field is defined using the field method with its name, type, and nullability.
  • The id field uses the ID type, which is a special GraphQL scalar type for unique identifiers. While technically a string, using ID signals that this field represents a unique identifier rather than regular text data, which helps GraphQL tools provide better validation and optimization.
  • The null: false option signifies that these fields are non-nullable, meaning they must always have a value and cannot be null.

Next, we define our QueryType with arguments:

Ruby
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

In this query type:

  • The books field returns an array of BookType items.
  • The book field takes an id argument of type ID (marked as required: true) and returns a single BookType.
  • Notice that the book field has null: true, which allows it to return null if no book with the given ID is found. This is important because the find method in our resolver can return nil when no matching book exists.
  • The argument method is used within the field definition to specify the argument.

Creating Resolvers for Queries with Arguments

In graphql-ruby, resolvers are methods defined within the type classes rather than separate resolver objects. These methods handle the logic for fetching and returning data.

First, let's define our sample data:

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

The resolver methods are already defined in our QueryType class:

Ruby
def books
  BOOKS
end

def book(id:)
  BOOKS.find { |book| book[:id] == id }
end

Explanation:

  • The books method returns the entire list of books.
  • The book method takes an id argument as a keyword parameter and returns the book that matches the given id.
  • Arguments are accessed directly as method parameters, following Ruby's keyword argument syntax.

Running the Sinatra Server with Updated Schema and Resolvers

To see our updated schema and resolvers in action, we need to define our GraphQL schema class and set up Sinatra to handle GraphQL requests.

First, define the schema class:

Ruby
class MySchema < GraphQL::Schema
  query(QueryType)
end

Next, set up the Sinatra server to handle GraphQL requests:

Ruby
require 'sinatra'
require 'json'

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

Finally, start the Sinatra server. Create a file named main.rb with all the code above and add:

Ruby
set :port, 4000

puts "🚀 Server ready at http://localhost:4000/"

When you run this code with ruby main.rb, you should see the following output:

text
🚀 Server ready at http://localhost:4000/

This indicates that your server is running and ready to handle queries.

Making GraphQL Queries with Arguments

Now, let's make some GraphQL queries that include arguments using Ruby's Net::HTTP library.

Create a new file named run.rb with the following code:

Ruby
require 'net/http'
require 'json'
require 'uri'

QUERY_BOOKS = <<~GRAPHQL
  query {
    books {
      id
      title
      author
    }
  }
GRAPHQL

def query_book_by_id(id)
  <<~GRAPHQL
    query {
      book(id: "#{id}") {
        id
        title
        author
      }
    }
  GRAPHQL
end

def run_queries
  url = URI('http://localhost:4000/graphql')
  
  # Query all books
  request = Net::HTTP::Post.new(url)
  request['Content-Type'] = 'application/json'
  request.body = JSON.generate({ query: QUERY_BOOKS })
  
  # Use Net::HTTP.start with a block to manage the connection
  # This approach automatically opens and closes the HTTP connection,
  # ensuring proper resource cleanup even if an error occurs
  response = Net::HTTP.start(url.hostname, url.port) do |http|
    http.request(request)
  end
  
  books_data = JSON.parse(response.body)
  puts 'Books: ' + JSON.pretty_generate(books_data)
  
  # Query a book by ID
  request = Net::HTTP::Post.new(url)
  request['Content-Type'] = 'application/json'
  request.body = JSON.generate({ query: query_book_by_id('1') })
  
  response = Net::HTTP.start(url.hostname, url.port) do |http|
    http.request(request)
  end
  
  book_by_id_data = JSON.parse(response.body)
  puts 'Book by ID: ' + JSON.pretty_generate(book_by_id_data)
end

begin
  run_queries
rescue StandardError => e
  puts "Error: #{e.message}"
end

Explanation:

  • We define two GraphQL queries: One to fetch all books and another to fetch a specific book by its id.
  • We create the request object using Net::HTTP::Post.new(url), then set the Content-Type header and request body as separate steps. This gives us more control over the request configuration.
  • We use Net::HTTP.start with a block to manage HTTP connections. The start method with a block automatically handles opening and closing the connection, which is more efficient when making multiple requests and ensures proper cleanup even if an error occurs.
  • The JSON.generate method converts Ruby hashes to JSON format.
  • The JSON.parse method parses the JSON response back into Ruby data structures.
  • We use Ruby's begin/rescue block for error handling.
  • The server processes the queries and returns the requested data.

Output:

JSON
Books: {
  "data": {
    "books": [
      {
        "id": "1",
        "title": "The Hobbit",
        "author": "J.R.R. Tolkien"
      },
      {
        "id": "2",
        "title": "Harry Potter",
        "author": "J.K. Rowling"
      }
    ]
  }
}

Book by ID: {
  "data": {
    "book": {
      "id": "1",
      "title": "The Hobbit",
      "author": "J.R.R. Tolkien"
    }
  }
}

Lesson Summary

In this lesson, we expanded our knowledge of GraphQL by learning how to query with arguments. We:

  • Defined a schema that supports arguments in queries using graphql-ruby's class-based type definitions.
  • Created resolvers as methods within type classes to handle these queries.
  • Set up a Sinatra server with the updated schema and resolvers.
  • Made and executed GraphQL queries with arguments using Ruby's Net::HTTP library.

You should now proceed to the practice exercises to solidify your understanding of querying with arguments. In the next lesson, we will tackle more advanced features to further enhance your skills. Keep practicing and refining your knowledge!

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