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.

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.
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.
# Gemfile
gem 'graphql', '~> 2.4'# Terminal
bundle install
rails generate graphql:installThe 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.
# 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
endThe 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.
# 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
endThe 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.
# 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
endThis 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.
# 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
endResolver 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.
# 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
endThe context[:current_user] comes from the controller. Authentication happens before the GraphQL layer, typically using Devise or a JWT library.
# 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
endAccess 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.
# 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
endTriggering a subscription happens from anywhere in the application, typically in a model callback or service object.
# 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
endThe ActionCable channel that handles GraphQL subscriptions ships with the gem.
# 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
endOn 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.
# 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
endMutation tests verify both success and validation error paths.
# 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
endFor 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:installto 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
Can you spot the bug in Ruby on Rails?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on September 19, 2026
Tags
Share
Related articles

Rails Stimulus and Importmaps in 2026: Modern JavaScript Without Build Tools
Learn how to use Stimulus controllers and Importmaps in Rails 8.1 to build interactive applications without webpack, esbuild, or any JavaScript bundler.

Rails API Mode in 2026: RESTful APIs, Serialization and Best Practices
Master Rails API mode with best practices for RESTful design, JSON serialization with Alba and jsonapi-serializer, authentication strategies, and error handling in Rails 8.

Rails Turbo and Hotwire in 2026: Real-Time Applications and Interview Questions
Master Rails Turbo and Hotwire for building real-time web applications. Deep dive into Turbo 8 morphing, Turbo Streams broadcasting, Stimulus integration, and interview preparation.