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.
73
91%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Passed
No known issues
Use this skill when designing, implementing, or reviewing GraphQL APIs in a Rails application with the graphql-ruby gem.
Core principle: GraphQL shifts validation and security responsibility to the resolver layer. Every field, type, and mutation needs explicit attention to authorization, N+1 risk, and error shape.
| Topic | Rule |
|---|---|
| Type naming | PascalCase, match domain language from ddd-ubiquitous-language |
| Mutations | Return { result, errors } — never raise from a mutation |
| N+1 | Every association load in a resolver must use a dataloader or batch loader |
| Authorization | Field-level auth required — type-level auth is not sufficient |
| Production | Disable introspection; set max_depth and max_complexity |
| Testing | Use schema.execute in request or integration specs |
| Docs | Write description on every type, field, argument, and mutation |
Tests gate implementation — write specs before resolver code (see rspec-best-practices).
DO NOT add a new resolver or mutation without completing the N+1 analysis step below.
DO NOT rely solely on type-level authorization — see Authorization section.1. SPEC: Write failing spec (happy path + auth cases + validation error case)
2. TYPE: Define argument and return types
3. IMPLEMENT: Write resolver or mutation class — delegate logic to a service object
4. N+1 CHECK: Verify every association load goes through a dataloader source
5. AUTH CHECK: Confirm field-level guards on all sensitive fields
6. RUN: All new specs pass; run full suite before opening PRDO NOT proceed to step 3 before step 1 is written and failing.
OrderType, not ApiOrderType)# BAD — raw array, no cursor-based pagination
field :orders, [Types::OrderType], null: false
# GOOD — connection type enables cursor pagination
field :orders, Types::OrderType.connection_type, null: falseQueryType and MutationType as entry points only — delegate to resolver objects# BAD — business logic inline in the type
field :summary, String, null: false do
def resolve
object.line_items.sum(&:total) # N+1 risk, logic buried in type
end
end
# GOOD — resolver object handles logic and receives preloaded data
field :summary, resolver: Resolvers::Orders::SummaryResolverbullet gem in development — treat GraphQL N+1s as Critical severityexpect { }.to make_database_queries(count: N) using db-query-matchersUse dataloader (built into graphql-ruby 1.12+) or graphql-batch:
# BAD — N+1: one query per order
def resolve
object.user # called for every order in the list
end
# GOOD — batch loads all users in one query
def resolve
dataloader.with(Sources::RecordById, User).load(object.user_id)
endRule: If a resolver calls an ActiveRecord association on object, it must go through a dataloader source.
Type-level authorization is not sufficient. Add field-level checks for sensitive fields:
# Type-level only — insufficient when the type is reused elsewhere
class Types::UserType < Types::BaseObject
guard -> (obj, args, ctx) { ctx[:current_user].admin? }
field :email, String, null: false
field :internal_notes, String, null: true # sensitive — needs its own guard
end
# GOOD — field-level guard on sensitive fields
field :internal_notes, String, null: true do
guard -> (obj, args, ctx) { ctx[:current_user].admin? }
enddef resolve
authorize! object, to: :read?, with: OrderPolicy
# ... resolver logic
endclass AppSchema < GraphQL::Schema
disable_introspection_entry_points if Rails.env.production?
endclass AppSchema < GraphQL::Schema
max_depth 10
max_complexity 300
endDefault to conservative limits and increase only when there is a documented reason.
Mutations must return a structured response — never raise unhandled exceptions:
class Types::Mutations::CreateOrderPayload < Types::BaseObject
field :order, Types::OrderType, null: true
field :errors, [String], null: false
end
class Mutations::CreateOrder < Mutations::BaseMutation
argument :product_id, ID, required: true
argument :quantity, Integer, required: true
type Types::Mutations::CreateOrderPayload
def resolve(product_id:, quantity:)
result = Orders::CreateOrder.call(
user: context[:current_user],
product_id: product_id,
quantity: quantity
)
if result.success?
{ order: result.order, errors: [] }
else
{ order: nil, errors: result.errors }
end
rescue ActiveRecord::RecordInvalid => e
{ order: nil, errors: e.record.errors.full_messages }
end
endShape contract: errors is always present and always an array. System errors are rescued at the schema level, not per-mutation.
GraphQL::Tracing::DataDogTracing; OpenTelemetry: GraphQL::Tracing::OpenTelemetryTracing)# spec/graphql/mutations/create_order_spec.rb
RSpec.describe "Mutations::CreateOrder", type: :request 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 }
errors
}
}
GQL
end
subject(:result) do
AppSchema.execute(query, variables: { productId: product.id, quantity: 1 },
context: { current_user: user })
end
it "creates an order" do
expect(result.dig("data", "createOrder", "errors")).to be_empty
expect(result.dig("data", "createOrder", "order", "id")).to be_present
end
end| Test type | Suggested path |
|---|---|
| Query resolvers | spec/graphql/queries/..._spec.rb |
| Mutations | spec/graphql/mutations/..._spec.rb |
| Types | spec/graphql/types/..._spec.rb (only if type has custom logic) |
| Resolver objects | spec/graphql/resolvers/..._spec.rb |
Write description on every type, field, argument, and mutation — GraphQL schemas are self-documenting:
class Types::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 order status: pending, confirmed, shipped, delivered."
field :total_cents, Integer, null: false, description: "Total order amount in cents."
endPrefer Insomnia or GraphQL Playground over Postman for GraphQL endpoints — see api-rest-collection.
| Mistake | Correct approach |
|---|---|
| Type-level auth is enough | Add field-level guards on sensitive fields — types are reused |
| Raw arrays for list fields | Use connection types for any collection that could paginate |
| Resolvers call associations directly | Every association load needs a dataloader source |
| Mutations raise on validation errors | Return { result, errors } — never raise from user input |
Missing description on fields | Schema is self-documenting — fill every description |
| No authorization tests | Always test unauthenticated and unauthorized cases |
| Skill | When to chain |
|---|---|
| ddd-ubiquitous-language | Type and field naming must match business language |
| rails-tdd-slices | Choose first failing spec (mutation vs query vs resolver unit) |
| rspec-best-practices | Full TDD cycle for resolvers and mutations |
| rails-migration-safety | When GraphQL schema changes require DB migrations |
| rails-security-review | Auth, introspection disable, query depth/complexity limits |
| yard-documentation | Document resolver Ruby classes |
api-rest-collection
create-prd
ddd-boundaries-review
ddd-rails-modeling
ddd-ubiquitous-language
generate-tasks
rails-agent-skills
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-stack-conventions
rails-tdd-slices
refactor-safely
rspec-best-practices
rspec-service-testing
ruby-api-client-integration
ruby-service-objects
strategy-factory-null-calculator
ticket-planning
yard-documentation