# 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. - Published: 2026-09-19 - Updated: 2026-09-19 - Author: Anthony Fillion-Maillet - Tags: rails, graphql, api, graphql-ruby, subscriptions - Reading time: 12 min --- GraphQL has become the default choice for APIs that need flexible data fetching, and [graphql-ruby](https://graphql-ruby.org/) 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](https://graphql-ruby.org/dataloader/overview.html) 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. ## 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](https://graphql-ruby.org/authorization/overview.html) 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](https://www.apollographql.com/docs/react/) or [urql](https://commerce.nearform.com/open-source/urql/) connect to the ActionCable WebSocket and manage subscription state. For interview prep on ActionCable mechanics, see the [ActionCable & WebSockets module](/technologies/ruby-on-rails/interview-questions/action-cable-websockets). ## 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](/technologies/ruby-on-rails/interview-questions/rails-testing-rspec). ## 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](/technologies/ruby-on-rails), see the [Rails API Mode module](/technologies/ruby-on-rails/interview-questions/rails-api-mode), which covers REST API design patterns that complement GraphQL knowledge. ## 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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/ruby-on-rails/rails-graphql-api-graphql-ruby-subscriptions-interview-questions