Nested Resolvers in GraphQL

Introduction

GraphQL enables efficient and flexible data querying, reducing payload size and improving application performance.

In this lesson, we use graphql-ruby and Sinatra to explore nested resolvers and data relationships in GraphQL. By the end, you should be able to create a GraphQL schema with nested types and use nested resolvers to handle complex data relationships.

GraphQL Schemas and Types

A GraphQL schema defines the structure of the API and the types of data it can return.

Here's an example defining two types, Author and Book, which have a nested relationship:

Ruby
class AuthorType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :name, String, null: false
  field :books, [[-> { BookType }]], null: true
end

class BookType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :title, String, null: false
  field :author, -> { AuthorType }, null: true
end

class QueryType < GraphQL::Schema::Object
  field :books, [BookType], null: false
  field :authors, [AuthorType], null: false
end
  • AuthorType has an id, name, and a list of books.
  • BookType has an id, title, and an author.
  • QueryType fetches lists of both books and authors.

Handling Circular Type References

When types reference each other (like Author referencing BookType and Book referencing AuthorType), Ruby needs special handling because classes must be defined before they're referenced.

There are two approaches:

1. Using Lazy Loading with Lambdas (Recommended)

Ruby
class AuthorType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :name, String, null: false
  field :books, [-> { BookType }], null: true  # Lazy-loaded reference
end

class BookType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :title, String, null: false
  field :author, -> { AuthorType }, null: true  # Lazy-loaded reference
end

2. Reordering Definitions

Define types in an order where each type only references already-defined types. However, this doesn't work for bidirectional relationships.

Why This Matters:

Ruby evaluates class bodies immediately during definition. Without lazy loading (-> { }), referencing BookType before it's defined would raise a NameError. The lambda syntax defers type resolution until GraphQL actually needs it, allowing circular references to work correctly.

For simple schemas, you might get away without lambdas if you're careful about definition order, but using lazy loading is a best practice that prevents subtle bugs as your schema grows.

Building and Understanding Resolvers

Resolvers fetch the data for fields defined in the schema. We use nested resolvers for nested data.

Here's an example:

Ruby
class QueryType < GraphQL::Schema::Object
  field :books, [BookType], null: false
  field :authors, [AuthorType], null: false

  def books
    BOOKS
  end

  def authors
    AUTHORS
  end
end

class AuthorType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :name, String, null: false
  field :books, [-> { BookType }], null: true

  def books
    BOOKS.select { |book| book[:author][:id] == object[:id] }
  end
end

class BookType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :title, String, null: false
  field :author, -> { AuthorType }, null: true

  def author
    AUTHORS.find { |author| author[:id] == object[:author][:id] }
  end
end

In the example above:

  • The QueryType resolver returns the full lists of books and authors.
  • The AuthorType resolver fetches books associated with an author.
  • The BookType resolver retrieves the author associated with a book.

The nested resolvers work seamlessly because GraphQL calls the resolvers depth-first. For example, when querying a book's author, it first resolves the book and then the associated author field. Circular type references (like Author → Book → Author) are safe because GraphQL only resolves the fields explicitly requested in the query. Since the query structure itself is finite, resolution naturally terminates. However, in production applications, you should enforce depth and complexity limits to prevent overly deep or expensive queries from consuming excessive server resources.

Example Implementation

Given the previously defined schema and resolvers, let's define our sample data:

Ruby
AUTHORS = [
  { id: '1', name: 'J.R.R. Tolkien' },
  { id: '2', name: 'J.K. Rowling' }
]

BOOKS = [
  { id: '1', title: 'The Hobbit', author: AUTHORS[0] },
  { id: '2', title: 'Harry Potter', author: AUTHORS[1] }
]

And create our GraphQL schema and Sinatra application:

Ruby
require 'sinatra'
require 'graphql'
require 'json'

class AppSchema < GraphQL::Schema
  query(QueryType)
end

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

When you run your server file with ruby server.rb, it should print:

text
== Sinatra (v3.0.0) has taken the stage on 4567 for development with backup from Puma

Executing Queries

Now that your GraphQL server is up and running, let's execute a query to fetch nested data.

Example Query:

Ruby
query = <<~GRAPHQL
  query {
    books {
      title
      author {
        name
      }
    }
    authors {
      name
      books {
        title
      }
    }
  }
GRAPHQL

Fetching Data:

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

query = <<~GRAPHQL
  query {
    books {
      title
      author {
        name
      }
    }
    authors {
      name
      books {
        title
      }
    }
  }
GRAPHQL

url = URI.parse('http://localhost:4000/graphql')
http = Net::HTTP.new(url.host, url.port)

request = Net::HTTP::Post.new(url.path)
request['Content-Type'] = 'application/json'
request.body = JSON.generate({ query: query })

response = http.request(request)
data = JSON.parse(response.body)

puts JSON.pretty_generate(data)

Executing this script fetches the nested data relationships and logs them. You should see output similar to:

JSON
{
  "data": {
    "books": [
      {
        "title": "The Hobbit",
        "author": {
          "name": "J.R.R. Tolkien"
        }
      },
      {
        "title": "Harry Potter",
        "author": {
          "name": "J.K. Rowling"
        }
      }
    ],
    "authors": [
      {
        "name": "J.R.R. Tolkien",
        "books": [
          {
            "title": "The Hobbit"
          }
        ]
      },
      {
        "name": "J.K. Rowling",
        "books": [
          {
            "title": "Harry Potter"
          }
        ]
      }
    ]
  }
}

Lesson Summary

In this lesson, we covered how to define GraphQL schemas with nested types and implement nested resolvers for complex data relationships. We also demonstrated executing queries to fetch nested data.

As you move on to the practice exercises, try creating your own schemas and resolvers. Practice is essential to solidify your understanding and improve your skills. This will set a strong foundation for tackling more advanced topics in GraphQL.

Good luck, and enjoy coding!

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