Curated library of AI agent skills for Ruby on Rails development. Covers code review, architecture, security, testing (RSpec), engines, service objects, DDD patterns, and workflow automation.
98
99%
Does it follow best practices?
Impact
98%
1.38xAverage score across 26 eval scenarios
Passed
No known issues
A complete, copy-ready reference covering every required pattern. All examples use an Orders domain.
Every class, field, and argument must have a description. Paginated lists use .connection_type — never a plain array.
# app/graphql/types/order_type.rb
# frozen_string_literal: true
module Types
class OrderType < Types::BaseObject
description "A customer order containing one or more line items."
field :id, ID, null: false, description: "Unique identifier."
field :status, String, null: false, description: "Current status: pending, confirmed, shipped, delivered."
field :total_cents, Integer, null: false, description: "Order total in cents."
field :buyer, Types::UserType, null: true, description: "The user who placed the order."
end
end# app/graphql/types/query_type.rb
# frozen_string_literal: true
module Types
class QueryType < Types::BaseObject
description "Root query type."
# connection_type — REQUIRED for paginated lists. Never use [Types::OrderType].
field :orders, Types::OrderType.connection_type, null: false,
description: "Paginated list of orders for the current user.",
resolver: Resolvers::Orders::ListResolver
end
endNever load an association directly on object. Use dataloader.with(Sources::RecordById, Model).load(foreign_key).
# app/graphql/resolvers/orders/list_resolver.rb
# frozen_string_literal: true
module Resolvers
module Orders
class ListResolver < Resolvers::BaseResolver
description "Returns paginated orders for the authenticated user."
type Types::OrderType.connection_type, null: false
def resolve
context[:current_user].orders.order(created_at: :desc)
end
end
end
end# app/graphql/types/order_type.rb — buyer field uses dataloader
field :buyer, Types::UserType, null: true, description: "The user who placed the order."
def buyer
# CORRECT: batch-loads users — no N+1
dataloader.with(Sources::RecordById, User).load(object.user_id)
end# app/graphql/sources/record_by_id.rb
# frozen_string_literal: true
class Sources::RecordById < GraphQL::Dataloader::Source
def initialize(model_class)
@model_class = model_class
end
def fetch(ids)
records = @model_class.where(id: ids).index_by(&:id)
ids.map { |id| records[id] }
end
endType-level authorization is insufficient — sensitive fields need their own guard:
# app/graphql/types/order_type.rb
field :internal_notes, String, null: true,
description: "Internal fulfillment notes — visible to admins only." do
# field-level guard — runs even if type-level auth passes
guard -> (_obj, _args, ctx) { ctx[:current_user]&.admin? }
end
field :payment_reference, String, null: true,
description: "Payment provider reference ID — restricted to finance team." do
guard -> (_obj, _args, ctx) { ctx[:current_user]&.finance? }
endMutations always return { result_field, errors: [String] }. Never let an exception propagate unhandled to the client.
# app/graphql/mutations/create_order.rb
# frozen_string_literal: true
module Mutations
class CreateOrder < Mutations::BaseMutation
description "Creates a new order for the authenticated user."
argument :product_id, ID, required: true, description: "ID of the product to order."
argument :quantity, Integer, required: true, description: "Number of units."
field :order, Types::OrderType, null: true, description: "The created order, or nil on failure."
field :errors, [String], null: false, description: "Validation or processing errors."
def resolve(product_id:, quantity:)
result = Orders::CreateOrder.call(
user: context[:current_user],
product_id: product_id,
quantity: quantity
)
if result[:success]
{ order: result[:response][:order], errors: [] }
else
{ order: nil, errors: Array(result[:response][:errors]) }
end
rescue ActiveRecord::RecordInvalid => e
{ order: nil, errors: e.record.errors.full_messages }
rescue StandardError => e
Rails.logger.error("Mutations::CreateOrder failed: #{e.class}: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
{ order: nil, errors: ["An unexpected error occurred"] }
end
end
end# app/graphql/app_schema.rb
# frozen_string_literal: true
class AppSchema < GraphQL::Schema
mutation(Types::MutationType)
query(Types::QueryType)
use GraphQL::Dataloader
# Disable introspection in production — prevents schema enumeration
disable_introspection_entry_points if Rails.env.production?
# Protect against deeply nested / expensive queries
max_depth 10
max_complexity 300
end# spec/graphql/mutations/create_order_spec.rb
# frozen_string_literal: true
RSpec.describe "Mutations::CreateOrder" do
let(:user) { create(:user) }
let(:product) { create(:product, stock: 5) }
let(:query) do
<<~GQL
mutation CreateOrder($productId: ID!, $quantity: Int!) {
createOrder(input: { productId: $productId, quantity: $quantity }) {
order { id status }
errors
}
}
GQL
end
subject(:result) do
AppSchema.execute(query,
variables: { productId: product.id, quantity: 1 },
context: { current_user: user })
end
it "creates the order" do
expect(result.dig("data", "createOrder", "errors")).to be_empty
expect(result.dig("data", "createOrder", "order", "id")).to be_present
end
context "when unauthenticated" do
subject(:result) do
AppSchema.execute(query, variables: { productId: product.id, quantity: 1 })
end
it "returns an authorization error" do
expect(result["errors"]).not_to be_empty
end
end
context "when product is out of stock" do
before { product.update!(stock: 0) }
it "returns errors and no order" do
expect(result.dig("data", "createOrder", "order")).to be_nil
expect(result.dig("data", "createOrder", "errors")).not_to be_empty
end
end
end| Check | Pattern | Where above |
|---|---|---|
description on every type and field | description "..." on class + each field | Sections 1, 3, 4 |
Paginated list uses connection_type | Types::OrderType.connection_type | Section 1 |
| Association loads use dataloader | dataloader.with(Sources::RecordById, Model).load(fk) | Section 2 |
Sources::RecordById defined | class Sources::RecordById < GraphQL::Dataloader::Source | Section 2 |
| Sensitive fields have field-level guard | guard -> (_obj, _args, ctx) { ctx[:current_user]&.role? } | Section 3 |
Mutation returns errors array | field :errors, [String], null: false | Section 4 |
| Mutation rescues StandardError | rescue StandardError => e with logger | Section 4 |
| Introspection disabled in production | disable_introspection_entry_points if Rails.env.production? | Section 5 |
max_depth and max_complexity set | max_depth 10 / max_complexity 300 | Section 5 |
Specs use AppSchema.execute | Not controller/request dispatch | Section 6 |
api-rest-collection
create-prd
ddd-boundaries-review
ddd-rails-modeling
ddd-ubiquitous-language
docs
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
generate-tasks
mcp_server
rails-architecture-review
rails-background-jobs
rails-bug-triage
rails-code-conventions
rails-code-review
rails-engine-compatibility
rails-engine-docs
rails-engine-extraction
rails-engine-installers
rails-engine-release
rails-engine-reviewer
rails-engine-testing
rails-graphql-best-practices
rails-migration-safety
rails-review-response
rails-security-review
rails-skills-orchestrator
rails-stack-conventions
rails-tdd-slices
refactor-safely
rspec-best-practices
rspec-service-testing
ruby-service-objects
strategy-factory-null-calculator
ticket-planning
yard-documentation