Skip to Content
Course content

187: Building a GraphQL API with graphql-ruby

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first pull in the graphql-ruby gem, the API feels incredibly intuitive. You define a schema, you set up your types, and you write a resolve method to get your data. It’s a natural progression. But there is a trap here that I've seen almost every developer fall into: treating the resolve method as the place where your business logic lives. It feels efficient in the first hour, but it becomes a maintenance nightmare by the end of the first month.

The temptation to put everything in the resolve method

Let's imagine we're building a GraphQL API for a digital bookstore. We want a query that lets users fetch a list of books, perhaps filtered by a specific genre. In a naive implementation, you'd probably write something like this inside your QueryType:

class Types::QueryType < Types::BaseObject
  field :books, [Types::BookType], null: false do
    argument :genre, String, required: false
  end

  def books(genre: nil)
    if genre
      # The logic is living right here in the GraphQL layer
      Book.where(genre: genre).where(active: true).order(published_at: :desc)
    else
      Book.where(active: true).order(published_at: :desc)
    end
  end
end

At first glance, this is fine. It's readable and it works. You're just calling an ActiveRecord query. But notice what's happening: your GraphQL layer now "knows" that only active books should be shown and that they should be ordered by publication date. You've leaked your business rules into the transport layer.

Why fat resolvers will haunt your pull requests

The problem starts when the requirements get real. Suddenly, your boss wants the "active" status to depend on the user's subscription level. Then, they want a complex caching strategy for the genre filter. Now, your books method isn't five lines; it's fifty. You're mixing authorization, database optimization, and business logic all inside a class that is supposed to be defining the shape of your API.

If you ever decide to add a REST endpoint for a mobile app or a background job that needs the same "active books" logic, you can't reuse this code because it's trapped inside Types::QueryType. You'll end up duplicating the logic, and the moment you change the definition of an "active book" in one place, the other will break. I've spent way too many weekends hunting down bugs caused by this exact kind of duplication.

Moving logic into dedicated resolver objects

The better way—the way that will keep your codebase sane—is to treat the GraphQL resolve method as a thin dispatcher. Its only job should be to take the arguments, hand them to a specialized object, and return the result. I usually lean toward creating a dedicated Resolvers namespace for this.

# app/graphql/resolvers/books_resolver.rb
module Resolvers
  class BooksResolver
    def self.call(genre: nil)
      scope = Book.where(active: true)
      scope = scope.where(genre: genre) if genre
      scope.order(published_at: :desc)
    end
  end
end

# app/graphql/types/query_type.rb
class Types::QueryType < Types::BaseObject
  field :books, [Types::BookType], null: false do
    argument :genre, String, required: false
  end

  def books(genre: nil)
    Resolvers::BooksResolver.call(genre: genre)
  end
end

By extracting this logic, we've achieved a few critical things. First, your QueryType stays clean; it's now a map of your API rather than a dumping ground for logic. Second, the BooksResolver is a plain old Ruby object (PORO). You can test it in isolation with RSpec or Minitest without having to boot up the entire GraphQL execution engine, which is significantly faster.

Yes, you're creating more files. Yes, you're adding a layer of indirection. But in a professional Ruby environment, that trade-off is almost always worth it. You're trading a tiny bit of initial speed for a system that doesn't collapse under its own weight as soon as you add your tenth query.




📋 Practical Task

Refactoring the Author Portfolio Query

You have been handed a legacy GraphQL API for a bookstore. Currently, the logic for fetching an author's portfolio—which requires filtering out private drafts and sorting by popularity—is hardcoded directly inside the Types::QueryType. This is making the file unmanageable.

Your Task:

  • Create a new resolver class Resolvers::AuthorPortfolioResolver that encapsulates the logic of fetching books for a specific author.
  • The resolver should accept an author_id and return books that are published: true, ordered by downloads DESC.
  • Refactor the Types::QueryType to remove the logic from the author_portfolio method and instead delegate the call to your new resolver.

Ensure that the QueryType method remains as a "thin" wrapper, simply passing the author_id argument through to the resolver.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.