Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
188: Resolvers and Mutations in graphql-ruby
When people first dive into graphql-ruby, they usually fall into one specific trap: they treat their resolvers and mutations like Rails controllers. They start cramming validation logic, database transactions, and third-party API calls directly into the resolve method. I've seen this in dozens of code reviews. The thought process is usually, "Well, this is where the request hits the server, so this is where the logic goes."
The "Fat Resolver" Fallacy
Let's look at how this usually manifests. Imagine we're building a bookstore API. You might be tempted to write a mutation to update a book's price like this:
class Mutations::UpdateBookPrice < GraphQL::Schema::RelayClassicMutation
argument :id, ID, required: true
argument :price, Float, required: true
def resolve(id:, price:)
book = Book.find(id)
if price < 0
raise GraphQL::ExecutionError, "Price cannot be negative"
end
book.update!(price: price)
# I'm imagining a notification system here too
NotificationService.notify_price_drop(book) if price < book.price_was
book
end
end
At first glance, it works. But you've just tied your business logic to your transport layer. If you ever need to update a price via a background job, a CLI task, or a legacy REST endpoint, you're stuck. You'd either have to duplicate this logic or do something awkward like manually invoking a GraphQL mutation from inside a Ruby worker. It's a maintenance nightmare waiting to happen.
Orchestration, Not Implementation
The correct way to think about resolvers and mutations is as orchestrators. Their only job is to take the input arguments, hand them off to a domain object or a service class, and then return the result in a format the GraphQL schema understands. I like to keep my resolve methods to about five lines of code maximum.
Let's refactor that bookstore example. We'll move the logic into a dedicated service object. Notice how the mutation becomes a thin wrapper:
# app/services/books/update_price_service.rb
module Books
class UpdatePriceService
def self.call(book_id, new_price)
book = Book.find(book_id)
raise ArgumentError, "Price cannot be negative" if new_price < 0
book.transaction do
book.update!(price: new_price)
NotificationService.notify_price_drop(book) if new_price < book.price_was
end
book
end
end
end
# app/graphql/mutations/update_book_price.rb
class Mutations::UpdateBookPrice < GraphQL::Schema::RelayClassicMutation
argument :id, ID, required: true
argument :price, Float, required: true
def resolve(id:, price:)
Books::UpdatePriceService.call(id, price)
rescue ArgumentError => e
GraphQL::ExecutionError.new(e.message)
end
end
Now, the GraphQL layer doesn't care how the price is updated; it only cares that it is updated and that the result is returned. This makes your code vastly easier to test. You can write a fast unit test for UpdatePriceService without loading the entire GraphQL schema.
Handling Complex Resolvers
The same principle applies to read-only resolvers. While graphql-ruby is smart enough to automatically resolve associations (like book.author), you'll eventually hit a wall where you need custom logic—like filtering a list of books based on a user's subscription tier.
Avoid putting that filtering logic in the Field definition. Instead, use a dedicated resolver class. This keeps your type definitions clean and your query logic encapsulated. If your resolver starts getting complex, move the query logic into a scope on the model or a specialized Query object. Remember: if you can't describe what a resolver does without using the word "and" three times, it's doing too much.
📋 Practical Task
Implement a Book Archival System
You need to implement a feature that allows administrators to "archive" a book. Archiving a book isn't just flipping a boolean; it requires updating the archived_at timestamp and logging the action in an AuditLog table.
Your task:
- Create a service class
Books::ArchiveServicethat handles the database logic: settingarchived_atto the current time and creating anAuditLogentry. - Implement a GraphQL mutation
Mutations::ArchiveBookthat accepts abookId. - Ensure the mutation remains a "thin wrapper" by calling the service class.
- Handle the case where a book is not found by returning a
GraphQL::ExecutionError.
There are no comments for now.