Rails GraphQL API in 2026: graphql-ruby, Subscriptions and Interview Questions

Build a production-ready GraphQL API with Rails 8 and graphql-ruby. Covers schema design, queries, mutations, subscriptions with ActionCable, and common interview questions.

Rails GraphQL API architecture diagram showing schema, resolvers and subscriptions

GraphQL has become the default choice for APIs that need flexible data fetching, and graphql-ruby brings this capability to Rails with tight ActiveRecord integration. Rails 8, released in late 2024, pairs well with graphql-ruby 2.4, which introduced improved subscription handling and better lazy execution for N+1 prevention.

What graphql-ruby brings to Rails

The graphql-ruby gem provides a complete GraphQL implementation: schema definition with a Ruby DSL, automatic type inference from ActiveRecord models, built-in DataLoader for batching, and ActionCable integration for real-time subscriptions.

Setting Up graphql-ruby in a Rails 8 Application

The gem generator creates the initial schema structure and mounts the GraphQL endpoint. Start with the gem and run the install generator to scaffold the base files.

ruby
# Gemfile
gem 'graphql', '~> 2.4'
bash
# Terminal
bundle install
rails generate graphql:install

The generator creates app/graphql/ with the schema file, base types, and a mutations directory. It also adds a route at /graphql and optionally mounts GraphiQL for development.

ruby
# app/graphql/sharpskill_schema.rb
class SharpskillSchema < GraphQL::Schema
  mutation(Types::MutationType)
  query(Types::QueryType)
  subscription(Types::SubscriptionType)

  # Use the built-in DataLoader for N+1 prevention
  use GraphQL::Dataloader

  # Required for subscriptions
  use GraphQL::Subscriptions::ActionCableSubscriptions
end

The schema file acts as the entry point. It declares which types handle queries, mutations, and subscriptions, and registers any middleware like DataLoader.

Defining Types and Resolvers for Rails Models

Each ActiveRecord model that appears in the API needs a corresponding GraphQL type. The type defines which fields are exposed and how they resolve.

ruby
# app/graphql/types/user_type.rb
module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :email, String, null: false
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false

    # Association with automatic batching via DataLoader
    field :posts, [Types::PostType], null: false

    def posts
      dataloader.with(Sources::ActiveRecordCollection, Post, :user_id).load(object.id)
    end
  end
end

The dataloader.with call batches multiple posts fetches into a single SQL query. Without DataLoader, a query requesting 50 users with their posts would execute 51 queries. With DataLoader, it runs 2.

ruby
# app/graphql/sources/active_record_collection.rb
class Sources::ActiveRecordCollection < GraphQL::Dataloader::Source
  def initialize(model, foreign_key)
    @model = model
    @foreign_key = foreign_key
  end

  def fetch(ids)
    records = @model.where(@foreign_key => ids).group_by(&@foreign_key)
    ids.map { |id| records[id] || [] }
  end
end

This pattern appears in every serious graphql-ruby project. The official DataLoader documentation covers additional use cases like caching and nested sources.

Building Queries with Arguments and Filtering

The QueryType defines the root fields available to clients. Arguments allow filtering and pagination.

ruby
# app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false do
      argument :email_contains, String, required: false
      argument :limit, Integer, required: false, default_value: 20
    end

    def users(email_contains: nil, limit:)
      scope = User.all
      scope = scope.where('email ILIKE ?', "%#{email_contains}%") if email_contains
      scope.limit(limit)
    end

    field :user, Types::UserType, null: true do
      argument :id, ID, required: true
    end

    def user(id:)
      User.find_by(id: id)
    end
  end
end

Resolver methods receive keyword arguments matching the field definition. Returning nil from a nullable field is valid; raising an error from a non-null field triggers a GraphQL error response.

Ready to ace your Ruby on Rails interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Mutations: Creating and Updating Records

Mutations follow a stricter convention than queries. Each mutation lives in its own class and returns a payload type that includes both the result and any errors.

ruby
# app/graphql/mutations/create_post.rb
module Mutations
  class CreatePost < Mutations::BaseMutation
    argument :title, String, required: true
    argument :body, String, required: true

    field :post, Types::PostType, null: true
    field :errors, [String], null: false

    def resolve(title:, body:)
      post = context[:current_user].posts.build(title: title, body: body)

      if post.save
        { post: post, errors: [] }
      else
        { post: nil, errors: post.errors.full_messages }
      end
    end
  end
end

The context[:current_user] comes from the controller. Authentication happens before the GraphQL layer, typically using Devise or a JWT library.

ruby
# app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
  def execute
    context = {
      current_user: current_user,
      request: request
    }
    result = SharpskillSchema.execute(
      params[:query],
      variables: params[:variables],
      context: context,
      operation_name: params[:operationName]
    )
    render json: result
  end
end

Access control inside resolvers checks context[:current_user] and raises GraphQL::ExecutionError when unauthorized. The graphql-ruby authorization guide details field-level and type-level authorization patterns.

Real-Time Updates with GraphQL Subscriptions and ActionCable

Subscriptions let clients receive updates when server-side events occur. Rails ActionCable handles the WebSocket connection, and graphql-ruby integrates via GraphQL::Subscriptions::ActionCableSubscriptions.

ruby
# app/graphql/types/subscription_type.rb
module Types
  class SubscriptionType < Types::BaseObject
    field :post_created, Types::PostType, null: false do
      argument :user_id, ID, required: false
    end

    def post_created(user_id: nil)
      object # The object passed from trigger
    end
  end
end

Triggering a subscription happens from anywhere in the application, typically in a model callback or service object.

ruby
# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user

  after_create_commit :notify_subscribers

  private

  def notify_subscribers
    SharpskillSchema.subscriptions.trigger(
      :post_created,
      { user_id: user_id },
      self
    )
  end
end

The ActionCable channel that handles GraphQL subscriptions ships with the gem.

ruby
# app/channels/graphql_channel.rb
class GraphqlChannel < ApplicationCable::Channel
  def subscribed
    @subscription_ids = []
  end

  def execute(data)
    result = SharpskillSchema.execute(
      data['query'],
      variables: data['variables'],
      context: { current_user: current_user, channel: self },
      operation_name: data['operationName']
    )

    payload = { result: result.to_h, more: result.subscription? }

    @subscription_ids << result.context[:subscription_id] if result.subscription?

    transmit(payload)
  end

  def unsubscribed
    @subscription_ids.each do |sid|
      SharpskillSchema.subscriptions.delete_subscription(sid)
    end
  end
end

On the client, libraries like Apollo Client or urql connect to the ActionCable WebSocket and manage subscription state. For interview prep on ActionCable mechanics, see the ActionCable & WebSockets module.

Testing GraphQL APIs with RSpec

Testing a GraphQL API requires executing queries against the schema and asserting on the response structure and data.

ruby
# spec/graphql/queries/users_spec.rb
RSpec.describe 'Users query' do
  let!(:user) { create(:user, email: 'test@example.com') }

  let(:query) do
    <<~GRAPHQL
      query {
        users(emailContains: "test") {
          id
          email
        }
      }
    GRAPHQL
  end

  it 'returns users matching the filter' do
    result = SharpskillSchema.execute(query)
    users = result.dig('data', 'users')

    expect(users.length).to eq(1)
    expect(users.first['email']).to eq('test@example.com')
  end
end

Mutation tests verify both success and validation error paths.

ruby
# spec/graphql/mutations/create_post_spec.rb
RSpec.describe Mutations::CreatePost do
  let(:user) { create(:user) }
  let(:context) { { current_user: user } }

  let(:mutation) do
    <<~GRAPHQL
      mutation($title: String!, $body: String!) {
        createPost(input: { title: $title, body: $body }) {
          post { id title }
          errors
        }
      }
    GRAPHQL
  end

  it 'creates a post when valid' do
    result = SharpskillSchema.execute(
      mutation,
      variables: { title: 'Hello', body: 'World' },
      context: context
    )

    data = result.dig('data', 'createPost')
    expect(data['errors']).to be_empty
    expect(data['post']['title']).to eq('Hello')
  end

  it 'returns errors when invalid' do
    result = SharpskillSchema.execute(
      mutation,
      variables: { title: '', body: 'World' },
      context: context
    )

    data = result.dig('data', 'createPost')
    expect(data['errors']).to include("Title can't be blank")
  end
end

For more on Rails testing patterns, see the Testing with RSpec module.

Common GraphQL Interview Questions for Rails Developers

Interviewers testing GraphQL knowledge in a Rails context often focus on practical implementation concerns rather than abstract type theory.

How do you prevent N+1 queries in graphql-ruby?

Use DataLoader with custom sources. Define a source class that batches database calls, and call dataloader.with(SourceClass, args).load(id) in resolvers. DataLoader collects all IDs requested in a single GraphQL execution and fetches them in one query.

What is the difference between a query and a mutation?

Semantics: queries read data and should have no side effects. Mutations modify data and may have side effects. GraphQL guarantees parallel execution for query fields and sequential execution for mutation fields, so two mutations in the same request execute in order.

How do subscriptions work in Rails?

Subscriptions use ActionCable for WebSocket connections. The client sends a subscription query through the WebSocket. The server stores the subscription and, when trigger is called, pushes the result to all matching clients. The subscription ID allows clients to unsubscribe.

When would you choose GraphQL over REST for a Rails API?

GraphQL reduces over-fetching when clients need different subsets of data, which is common in mobile apps with varying screen sizes. It also eliminates the need for multiple REST endpoints when a single view requires data from several models. REST remains simpler for CRUD-heavy APIs with consistent data shapes and for public APIs where caching is critical.

How do you handle authentication and authorization in graphql-ruby?

Authentication happens in the controller before GraphQL execution, typically via Devise, Warden, or JWT verification. The authenticated user is passed in context. Authorization happens in resolvers or via graphql-ruby's authorized? hook on types and mutations, raising GraphQL::ExecutionError when access is denied.

For more Ruby on Rails interview preparation, see the Rails API Mode module, which covers REST API design patterns that complement GraphQL knowledge.

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Building Production GraphQL APIs with Rails: Key Takeaways

  • Install graphql-ruby with rails generate graphql:install to scaffold the schema, types, and routing
  • Use DataLoader sources for all associations to batch database queries and prevent N+1 performance issues
  • Define mutations in separate classes with explicit error fields in the return type
  • Integrate subscriptions via ActionCable for real-time updates, triggering from model callbacks or service objects
  • Test queries and mutations by executing them against the schema directly in RSpec
  • Authentication belongs in the controller; authorization checks happen inside resolvers using context[:current_user]
  • Choose GraphQL when clients need flexible data fetching; stick with REST for simple CRUD APIs with uniform responses
Daily challenge

Can you spot the bug in Ruby on Rails?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on September 19, 2026

Tags

#rails
#graphql
#api
#graphql-ruby
#subscriptions

Share

Related articles