CrackedRuby logo

CrackedRuby

GraphQL

Overview

GraphQL in Ruby provides a type-safe query language implementation for building APIs that allow clients to request exactly the data they need. The graphql gem serves as the primary Ruby implementation, offering a complete GraphQL specification-compliant framework for creating schemas, defining types, and executing queries.

The core GraphQL Ruby architecture centers around several key classes: GraphQL::Schema defines the API structure, GraphQL::ObjectType represents data models, GraphQL::Field defines available operations, and resolver classes handle business logic. The gem supports the full GraphQL specification including queries, mutations, subscriptions, and introspection.

Ruby GraphQL schemas use a class-based approach where types inherit from base classes like GraphQL::Schema::Object and GraphQL::Schema::Resolver. Fields are defined using the field method with type information, arguments, and resolver logic. The schema compilation process validates type definitions and builds an executable schema that can process GraphQL queries.

class Types::UserType < GraphQL::Schema::Object
  description "A user in the system"
  
  field :id, ID, null: false
  field :email, String, null: false
  field :name, String, null: true
  field :posts, [Types::PostType], null: true
end

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

The execution engine parses incoming GraphQL query strings, validates them against the schema, and executes resolver functions to fetch data. Ruby's GraphQL implementation handles complex scenarios like field aliases, fragments, variables, and nested selections while maintaining type safety throughout the execution pipeline.

Basic Usage

Creating a GraphQL API in Ruby begins with defining a schema that declares available types and operations. The schema acts as a contract between the client and server, specifying what data can be queried and how it can be modified.

Type definitions use Ruby classes that inherit from GraphQL::Schema::Object. Each field declaration includes the field name, return type, nullability, and optional description. Ruby GraphQL supports scalar types (String, Int, Float, Boolean, ID), object types, list types, and custom scalar types.

# Define object types
class Types::PostType < GraphQL::Schema::Object
  field :id, ID, null: false
  field :title, String, null: false
  field :content, String, null: true
  field :author, Types::UserType, null: false
  field :comments, [Types::CommentType], null: true
  field :created_at, GraphQL::Types::ISO8601DateTime, null: false
end

# Define query root type
class Types::QueryType < GraphQL::Schema::Object
  field :user, Types::UserType, null: true do
    argument :id, ID, required: true
  end
  
  field :posts, [Types::PostType], null: true do
    argument :limit, Int, required: false, default_value: 10
  end

  def user(id:)
    User.find(id)
  end
  
  def posts(limit:)
    Post.limit(limit)
  end
end

Resolvers handle the business logic for fetching data. Simple field resolvers can be defined as methods on the type class, while complex logic often uses dedicated resolver classes. The resolver context provides access to the current user, database connections, and other application state.

class Resolvers::PostResolver < GraphQL::Schema::Resolver
  type [Types::PostType], null: true
  
  argument :author_id, ID, required: false
  argument :published, Boolean, required: false, default_value: true
  
  def resolve(author_id: nil, published: true)
    posts = Post.all
    posts = posts.where(author_id: author_id) if author_id
    posts = posts.where(published: published)
    posts.order(created_at: :desc)
  end
end

# Use resolver in query type
class Types::QueryType < GraphQL::Schema::Object
  field :posts, resolver: Resolvers::PostResolver
end

Mutations modify server state and follow similar patterns to queries but with additional input validation and error handling. Input types define the shape of mutation arguments, while payload types structure the mutation response.

class Types::CreatePostInput < GraphQL::Schema::InputObject
  argument :title, String, required: true
  argument :content, String, required: false
  argument :author_id, ID, required: true
end

class Mutations::CreatePost < GraphQL::Schema::Mutation
  argument :input, Types::CreatePostInput, required: true
  
  field :post, Types::PostType, null: true
  field :errors, [String], null: true
  
  def resolve(input:)
    post = Post.new(input.to_h)
    
    if post.save
      { post: post, errors: [] }
    else
      { post: nil, errors: post.errors.full_messages }
    end
  end
end

Advanced Usage

Advanced GraphQL Ruby patterns involve custom scalar types, interfaces, unions, and sophisticated resolver architectures. Custom scalars handle domain-specific data types like email addresses, URLs, or encrypted values with validation and serialization logic.

class Types::EmailType < GraphQL::Schema::Scalar
  description "A valid email address"
  
  def self.coerce_input(input_value, context)
    return unless input_value.is_a?(String)
    return unless input_value.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
    input_value.downcase
  end
  
  def self.coerce_result(ruby_value, context)
    ruby_value.to_s
  end
end

Interfaces define common fields shared across multiple types, enabling polymorphic queries. Types implement interfaces by including the interface module and defining required fields. Union types group different object types under a single field, requiring type resolution logic.

module Types::ContentInterface
  include GraphQL::Schema::Interface
  
  field :id, ID, null: false
  field :title, String, null: false
  field :created_at, GraphQL::Types::ISO8601DateTime, null: false
  
  definition_methods do
    def resolve_type(object, context)
      case object
      when Post then Types::PostType
      when Video then Types::VideoType
      else raise "Unexpected object: #{object}"
      end
    end
  end
end

class Types::PostType < GraphQL::Schema::Object
  implements Types::ContentInterface
  
  field :content, String, null: true
  field :author, Types::UserType, null: false
end

class Types::VideoType < GraphQL::Schema::Object
  implements Types::ContentInterface
  
  field :duration, Int, null: false
  field :url, String, null: false
end

class Types::ContentUnion < GraphQL::Schema::Union
  possible_types Types::PostType, Types::VideoType
  
  def self.resolve_type(object, context)
    case object
    when Post then Types::PostType
    when Video then Types::VideoType
    end
  end
end

Sophisticated resolver patterns include field-level authorization, argument validation, and lazy loading. The GraphQL::Schema::Resolver base class provides hooks for these cross-cutting concerns through method overrides and modules.

class Resolvers::BaseResolver < GraphQL::Schema::Resolver
  private
  
  def authorized?(action: :read)
    return false unless context[:current_user]
    
    case action
    when :read then context[:current_user].can_read?(object)
    when :write then context[:current_user].can_write?(object)
    else false
    end
  end
  
  def validate_arguments(**args)
    args.each do |key, value|
      next unless value.is_a?(String)
      next if value.length <= 1000
      
      raise GraphQL::ExecutionError, "#{key} exceeds maximum length"
    end
  end
end

class Resolvers::UserPostsResolver < Resolvers::BaseResolver
  type [Types::PostType], null: true
  
  argument :status, Types::PostStatusEnum, required: false
  argument :limit, Int, required: false, default_value: 20
  
  def resolve(**args)
    return nil unless authorized?
    validate_arguments(**args)
    
    posts = object.posts
    posts = posts.where(status: args[:status]) if args[:status]
    posts.limit(args[:limit]).order(created_at: :desc)
  end
end

Schema-level customizations include custom directives, middleware, and query analysis. Directives modify field behavior based on arguments, while middleware provides request-level processing like authentication, logging, and caching.

class Schema < GraphQL::Schema
  query Types::QueryType
  mutation Types::MutationType
  
  use GraphQL::Execution::Interpreter
  use GraphQL::Analysis::AST
  
  # Custom middleware for authentication
  use GraphQL::Schema::Middleware::Chain.new([
    GraphQL::Schema::Middleware.new(
      resolve: ->(type, field, obj, args, ctx, next_middleware) {
        if field.metadata[:auth_required] && !ctx[:current_user]
          raise GraphQL::ExecutionError, "Authentication required"
        end
        next_middleware.call
      }
    )
  ])
  
  # Query complexity analysis
  max_complexity 1000
  max_depth 10
  
  def self.resolve_type(type, obj, ctx)
    raise GraphQL::RequiredImplementationMissingError
  end
end

Error Handling & Debugging

GraphQL Ruby provides multiple error handling mechanisms for different failure scenarios. Execution errors surface to clients as part of the GraphQL response, while system errors typically terminate query processing entirely.

Resolver methods raise GraphQL::ExecutionError to indicate field-level failures that should appear in the response's errors array. These errors can include path information, error codes, and custom metadata for client consumption.

class Resolvers::PostResolver < GraphQL::Schema::Resolver
  def resolve(id:)
    post = Post.find_by(id: id)
    
    unless post
      raise GraphQL::ExecutionError.new(
        "Post not found",
        extensions: { code: "POST_NOT_FOUND", id: id }
      )
    end
    
    unless can_read?(post)
      raise GraphQL::ExecutionError.new(
        "Access denied",
        extensions: { code: "ACCESS_DENIED" }
      )
    end
    
    post
  rescue ActiveRecord::ConnectionTimeoutError => e
    raise GraphQL::ExecutionError.new(
      "Database unavailable",
      extensions: { code: "DATABASE_ERROR", retry_after: 30 }
    )
  end
end

Validation errors occur during query parsing and schema validation phases. The GraphQL Ruby engine automatically handles syntax errors, type mismatches, and schema violations. Custom validation rules can be added to enforce business-specific constraints.

class CustomValidationRule
  include GraphQL::StaticValidation::Message
  
  def validate(context)
    super
    
    context.visitor[GraphQL::Language::Nodes::Field] << ->(node, parent) {
      if node.name == "expensiveField" && context.query.depth > 5
        context.errors << create_error(
          "expensiveField cannot be nested deeper than 5 levels",
          nodes: [node]
        )
      end
    }
  end
end

class Schema < GraphQL::Schema
  validate_with CustomValidationRule
end

Debugging GraphQL queries requires understanding the execution flow and introspection capabilities. The GraphQL Ruby gem provides detailed tracing information through instrumentation hooks and query analyzers.

# Enable detailed tracing
class Schema < GraphQL::Schema
  trace_with GraphQL::Tracing::ScoutTracing if Rails.env.production?
  trace_with GraphQL::Tracing::DataDogTracing if Rails.env.production?
  
  # Custom tracing for development
  trace_with GraphQL::Tracing::CallLegacyTracers
  
  tracer GraphQL::Tracing::CallLegacyTracers.new(
    trace: ->(key, data) {
      if Rails.env.development?
        puts "GraphQL trace: #{key}, duration: #{data[:duration]}ms"
        puts "  Query: #{data[:query]}" if data[:query]
        puts "  Variables: #{data[:variables]}" if data[:variables]
      end
    }
  )
end

Error recovery patterns handle partial failures gracefully while maintaining query execution for successful fields. Union types and nullable fields provide structure for representing both success and error states in mutations.

class Types::PostResult < GraphQL::Schema::Union
  possible_types Types::PostType, Types::PostError
  
  def self.resolve_type(object, context)
    case object
    when Post then Types::PostType
    when PostError then Types::PostError
    end
  end
end

class Types::PostError < GraphQL::Schema::Object
  field :message, String, null: false
  field :code, String, null: false
  field :path, [String], null: true
end

class Mutations::CreatePost < GraphQL::Schema::Mutation
  field :result, Types::PostResult, null: false
  
  def resolve(**args)
    post = Post.new(args)
    
    if post.save
      post
    else
      PostError.new(
        message: post.errors.full_messages.join(", "),
        code: "VALIDATION_ERROR",
        path: ["createPost"]
      )
    end
  end
end

Production Patterns

Production GraphQL Ruby deployments require careful consideration of performance, security, and monitoring. Schema organization typically separates types, resolvers, and mutations into dedicated directories with clear naming conventions and module structures.

# app/graphql/types/base_object.rb
class Types::BaseObject < GraphQL::Schema::Object
  edge_type_class(Types::BaseEdge)
  connection_type_class(Types::BaseConnection)
  
  field_class Types::BaseField
end

# app/graphql/types/base_field.rb
class Types::BaseField < GraphQL::Schema::Field
  argument_class Types::BaseArgument
  
  def initialize(*args, auth_required: false, **kwargs, &block)
    @auth_required = auth_required
    super(*args, **kwargs, &block)
  end
  
  def authorized?(obj, args, ctx)
    return super unless @auth_required
    return false unless ctx[:current_user]
    super
  end
end

Authentication and authorization integrate with existing application security systems through context objects and field-level permissions. The context hash carries user information, permissions, and request metadata throughout query execution.

# In your controller
class GraphqlController < ApplicationController
  def execute
    context = {
      current_user: current_user,
      current_ability: current_ability,
      request: request,
      session: session
    }
    
    result = Schema.execute(
      params[:query],
      variables: params[:variables],
      context: context,
      operation_name: params[:operationName]
    )
    
    render json: result
  end
  
  private
  
  def current_ability
    @current_ability ||= Ability.new(current_user)
  end
end

# Authorization in resolvers
class Resolvers::BaseResolver < GraphQL::Schema::Resolver
  def authorized_user?(user = nil)
    user ||= context[:current_user]
    return false unless user
    context[:current_ability].can?(:read, object)
  end
  
  def require_authentication!
    return if context[:current_user]
    raise GraphQL::ExecutionError, "Authentication required"
  end
end

Caching strategies for GraphQL require field-level granularity since clients request arbitrary data combinations. Rails fragment caching integrates with GraphQL through custom field extensions and resolver decorators.

class Types::BaseField < GraphQL::Schema::Field
  def initialize(*args, cache: false, cache_key: nil, **kwargs, &block)
    @cache_enabled = cache
    @cache_key_proc = cache_key
    super(*args, **kwargs, &block)
  end
  
  def resolve_field(obj, args, ctx)
    return super unless @cache_enabled
    
    cache_key = build_cache_key(obj, args, ctx)
    
    Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
      super
    end
  end
  
  private
  
  def build_cache_key(obj, args, ctx)
    if @cache_key_proc
      @cache_key_proc.call(obj, args, ctx)
    else
      "graphql:#{obj.class.name}:#{obj.id}:#{name}:#{args.to_h.hash}"
    end
  end
end

# Usage in type definitions
class Types::UserType < Types::BaseObject
  field :profile, Types::ProfileType, 
        null: true,
        cache: true,
        cache_key: ->(user, args, ctx) { "user_profile:#{user.id}:#{user.updated_at.to_i}" }
end

Monitoring and observability require custom instrumentation for GraphQL-specific metrics like query complexity, execution time per field, and error rates. Integration with APM tools provides production visibility into GraphQL performance characteristics.

class GraphqlInstrumentation
  def initialize
    @query_counter = Prometheus::Client::Counter.new(
      :graphql_queries_total,
      docstring: 'Total GraphQL queries executed',
      labels: [:operation_name, :operation_type]
    )
    
    @query_duration = Prometheus::Client::Histogram.new(
      :graphql_query_duration_seconds,
      docstring: 'GraphQL query execution duration',
      labels: [:operation_name, :operation_type]
    )
  end
  
  def trace(key, data)
    case key
    when 'execute_query'
      operation = data[:query].operation_name || 'anonymous'
      operation_type = data[:query].operation_type || 'query'
      
      @query_counter.increment(
        labels: { operation_name: operation, operation_type: operation_type }
      )
      
      start_time = Time.current
      result = yield
      duration = Time.current - start_time
      
      @query_duration.observe(
        duration,
        labels: { operation_name: operation, operation_type: operation_type }
      )
      
      result
    else
      yield
    end
  end
end

class Schema < GraphQL::Schema
  trace_with GraphqlInstrumentation
end

Performance & Memory

GraphQL Ruby performance optimization focuses on resolving the N+1 query problem, managing query complexity, and efficient data loading patterns. The graphql-batch gem provides batched loading capabilities that consolidate database queries across resolver executions.

Database query optimization requires careful attention to association loading and query batching. The GraphQL Ruby execution model can trigger numerous database queries if resolvers naively access associated records without preloading.

# Install graphql-batch gem and configure
class Schema < GraphQL::Schema
  use GraphQL::Batch
end

# Create batch loaders for efficient data access
class UserLoader < GraphQL::Batch::Loader
  def initialize(scope: User.all)
    @scope = scope
  end
  
  def perform(user_ids)
    users = @scope.where(id: user_ids).index_by(&:id)
    user_ids.each { |id| fulfill(id, users[id]) }
  end
end

class PostsByUserLoader < GraphQL::Batch::Loader
  def perform(user_ids)
    posts_by_user_id = Post.where(user_id: user_ids)
                          .includes(:comments, :tags)
                          .group_by(&:user_id)
    
    user_ids.each do |user_id|
      fulfill(user_id, posts_by_user_id[user_id] || [])
    end
  end
end

# Use loaders in resolvers
class Types::UserType < Types::BaseObject
  field :posts, [Types::PostType], null: true
  
  def posts
    PostsByUserLoader.load(object.id)
  end
end

class Types::PostType < Types::BaseObject
  field :author, Types::UserType, null: false
  
  def author
    UserLoader.load(object.user_id)
  end
end

Query complexity analysis prevents resource-intensive queries from overwhelming the server. The GraphQL Ruby gem includes configurable complexity analysis that assigns costs to fields and limits total query complexity.

class Schema < GraphQL::Schema
  # Set maximum query complexity
  max_complexity 1000
  max_depth 15
  
  # Custom complexity calculation
  def self.type_error(err, query_ctx)
    case err
    when GraphQL::ComplexityError
      Rails.logger.warn "Complex query blocked: #{err.message}"
      GraphQL::ExecutionError.new(
        "Query too complex",
        extensions: { code: "QUERY_TOO_COMPLEX", max_complexity: max_complexity }
      )
    else
      super
    end
  end
end

# Assign custom complexity to expensive fields
class Types::UserType < Types::BaseObject
  field :posts, [Types::PostType], null: true, complexity: 5
  field :followers, [Types::UserType], null: true, complexity: 10
  field :activity_feed, [Types::ActivityType], null: true do
    complexity ->(ctx, args, child_complexity) {
      # Dynamic complexity based on arguments
      limit = args[:limit] || 20
      limit * child_complexity * 2
    }
  end
end

Memory usage optimization involves careful object allocation and garbage collection considerations. Large result sets and deep nesting can consume significant memory during GraphQL query execution.

# Streaming large datasets with cursor-based pagination
class Types::BaseConnection < GraphQL::Types::Relay::BaseConnection
  # Override to implement efficient counting
  def count
    if object.respond_to?(:count_estimate)
      object.count_estimate
    else
      object.count
    end
  end
end

# Lazy loading for expensive computations
class Types::UserType < Types::BaseObject
  field :expensive_calculation, String, null: true
  
  def expensive_calculation
    @expensive_calculation ||= LazyLoader.new do
      # Expensive operation here
      perform_complex_calculation(object)
    end
  end
end

class LazyLoader
  def initialize(&block)
    @block = block
    @loaded = false
  end
  
  def load
    return @result if @loaded
    @result = @block.call
    @loaded = true
    @result
  end
  
  def to_s
    load.to_s
  end
end

Profiling GraphQL queries requires specialized tools that understand the execution model. Custom instrumentation provides detailed timing information for individual field resolutions and database queries.

class DetailedTracing
  def initialize
    @field_timings = {}
    @db_queries = []
  end
  
  def trace(key, data)
    case key
    when 'execute_field'
      field_name = "#{data[:owner]}.#{data[:field].name}"
      start_time = Time.current
      
      result = yield
      
      execution_time = Time.current - start_time
      @field_timings[field_name] = execution_time
      
      Rails.logger.debug "Field #{field_name}: #{execution_time.round(3)}s"
      result
    when 'execute_query'
      ActiveSupport::Notifications.subscribe 'sql.active_record' do |*args|
        event = ActiveSupport::Notifications::Event.new(*args)
        @db_queries << {
          sql: event.payload[:sql],
          duration: event.duration,
          name: event.payload[:name]
        }
      end
      
      result = yield
      
      Rails.logger.info "Query executed in #{@field_timings.values.sum.round(3)}s"
      Rails.logger.info "#{@db_queries.length} database queries"
      
      result
    else
      yield
    end
  end
end

Reference

Core Classes

Class Description Key Methods
GraphQL::Schema Root schema definition execute, query, mutation, subscription
GraphQL::Schema::Object Base class for object types field, implements
GraphQL::Schema::Resolver Base resolver class resolve, authorized?, ready?
GraphQL::Schema::InputObject Input type definition argument
GraphQL::Schema::Enum Enumeration type value
GraphQL::Schema::Scalar Custom scalar type coerce_input, coerce_result
GraphQL::Schema::Interface Interface definition field, resolve_type
GraphQL::Schema::Union Union type possible_types, resolve_type

Field Definition Options

Option Type Description Example
null Boolean Field nullability null: false
description String Field documentation description: "User email"
deprecation_reason String Deprecation message deprecation_reason: "Use newField"
complexity Integer/Proc Query complexity cost complexity: 5
authorize Symbol/Proc Authorization check authorize: :admin
resolver Class Resolver class resolver: UserResolver
resolver_method Symbol Method name resolver_method: :find_user
hash_key Symbol Object key accessor hash_key: :user_name

Argument Definition Options

Option Type Description Example
required Boolean Argument requirement required: true
default_value Any Default when not provided default_value: 10
as Symbol Parameter name mapping as: :user_id
prepare Proc Argument preprocessing prepare: ->(val, ctx) { val.strip }
validate Hash Validation rules validate: { length: { minimum: 5 } }

Built-in Scalar Types

Type Ruby Class Description
String String UTF-8 character sequences
Int Integer 32-bit signed integers
Float Float IEEE 754 double precision
Boolean Boolean true/false values
ID String/Integer Unique identifiers
GraphQL::Types::ISO8601DateTime Time ISO 8601 datetime strings
GraphQL::Types::JSON Hash/Array JSON objects
GraphQL::Types::BigInt Integer Arbitrary precision integers

Execution Context Methods

Method Returns Description
context[:current_user] Object Currently authenticated user
context[:request] ActionDispatch::Request HTTP request object
context.query GraphQL::Query Current query object
context.schema GraphQL::Schema Schema being executed
context.field GraphQL::Schema::Field Current field definition
context.path Array Field execution path
context.ast_node GraphQL::Language::Nodes::Field Query AST node

Schema Configuration Options

Option Type Description Default
max_complexity Integer Maximum query complexity nil (unlimited)
max_depth Integer Maximum nesting depth nil (unlimited)
validate_timeout Numeric Validation timeout seconds nil
default_page_size Integer Connection page size nil
default_max_page_size Integer Maximum page size nil
introspection Boolean Enable introspection true
query_execution_strategy Class Execution strategy GraphQL::Execution::Interpreter

Error Classes

Exception Inherits From Use Case
GraphQL::ExecutionError StandardError Field-level errors
GraphQL::RequiredImplementationMissingError NotImplementedError Missing interface methods
GraphQL::InvalidNullError ExecutionError Non-null field returns null
GraphQL::ComplexityError StandardError Query too complex
GraphQL::AnalysisError StandardError Query analysis failure
GraphQL::CoercionError TypeError Type coercion failure
GraphQL::UnauthorizedError ExecutionError Authorization failure

Query Analysis Classes

Class Purpose Usage
GraphQL::Analysis::QueryComplexity Calculate complexity Schema complexity limits
GraphQL::Analysis::QueryDepth Calculate nesting depth Schema depth limits
GraphQL::Analysis::FieldUsage Track field usage Analytics and deprecation
GraphQL::Analysis::MaxQueryDepth Enforce depth limits Query validation

Instrumentation Hooks

Hook Data Available Use Case
execute_query query, variables, context Request-level metrics
execute_field field, object, arguments Field-level timing
execute_field_lazy field, object, lazy_value Lazy field resolution
authorized object, type, field Authorization logging
authorized_lazy object, type, lazy_auth Lazy authorization
resolve_type object, type, context Type resolution