CtrlK
BlogDocsLog inGet started
Tessl Logo

igmarin/rails-agent-skills

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

1.38x
Quality

99%

Does it follow best practices?

Impact

98%

1.38x

Average score across 26 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

SKILL.mdrails-graphql-best-practices/

name:
rails-graphql-best-practices
description:
Use when building or reviewing GraphQL APIs in Rails with the graphql-ruby gem. Covers schema design, N+1 prevention with dataloaders, field-level auth, query limits, error handling, and testing resolvers/mutations with RSpec.

Rails GraphQL Best Practices

Use this skill when designing, implementing, or reviewing GraphQL APIs in a Rails application with the graphql-ruby gem.

HARD-GATE

Tests gate implementation — write specs before resolver code (see rspec-best-practices).
Before shipping a resolver/mutation slice, ALL of the following must be true (details in linked sections; do not duplicate checks in prose here):
- N+1 Prevention: use `dataloader.with(Source, Model).load(id)` — NEVER `object.association`
- Authorization: sensitive fields have field-level guards (not type-level alone).
- Type Conventions: paginated collections use Types::*Type.connection_type, not plain arrays.
- Schema safeguards: AppSchema disables introspection in production and sets max_depth / max_complexity.
- TESTING.md: specs in `spec/graphql/` use `AppSchema.execute` — **ALL spec files** (resolver specs AND mutation specs). Never use HTTP controller dispatch for GraphQL specs.
- Error Handling: mutations return `{ result, errors }` with rescue blocks — no unhandled exceptions.
- Documentation: `description:` on every field in every type.
- Resolver Structure: dedicated resolver classes, not inline field blocks.

Workflow: Adding a New Resolver or Mutation

1. SPEC:       Write failing spec (happy path + auth + validation error case) — see TESTING.md
2. TYPE:       Arguments and return types — Type Conventions for pagination shape
3. IMPLEMENT:  Resolver/mutation class delegating to a service object
4. N+1 CHECK:  N+1 Prevention (dataloader on every association load from GraphQL)
5. AUTH CHECK: Authorization (field-level guards where data is sensitive)
6. FINAL CHECK: Verify every HARD-GATE item above against the code you wrote — all 8 must be true
7. RUN:        Full suite green before PR

DO NOT proceed to step 3 before step 1 is written and failing.

Schema Design

Type Conventions

  • Match type and field names to domain language — do not leak internal model names.
  • Paginated collections: use connection_type, never a plain array of nodes.
field :orders, Types::OrderType.connection_type, null: false, resolver: Resolvers::Orders::ListResolver

Resolver Structure

  • Prefer dedicated resolver classes over inline field blocks for non-trivial logic.
  • Keep QueryType and MutationType as entry points only — delegate: field :summary, resolver: Resolvers::Orders::SummaryResolver.

N+1 Prevention

Detection

  • Enable bullet gem in development — treat GraphQL N+1s as Critical severity.
  • Assert query counts with expect { }.to make_database_queries(count: N) using db-query-matchers.

Resolution

FORBIDDEN: Never call object.buyer, object.user, or any association directly — every association load MUST use the dataloader (graphql-ruby 1.12+):

# ❌ causes N+1 for every record in the list
def buyer; object.buyer; end

# ✅ batches loads across all records
def buyer
  dataloader.with(Sources::RecordById, Buyer).load(object.buyer_id)
end

Source class definition:

# app/graphql/sources/record_by_id.rb
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
end

Authorization

Field-Level Authorization

Type-level auth alone is insufficient — add field-level guards for sensitive fields:

field :internal_notes, String, null: true do
  guard -> (_obj, _args, ctx) { ctx[:current_user]&.admin? }
end

For Pundit: authorize! object, to: :read?, with: OrderPolicy in the resolver's resolve method.

Schema safeguards

Configure production introspection and query limits on AppSchema in one place:

class AppSchema < GraphQL::Schema
  disable_introspection_entry_points if Rails.env.production?

  max_depth 10
  max_complexity 300
end

Adjust depth/complexity to your API; document the chosen limits in the PR or schema comments if non-default.

Error Handling

Mutations must return a structured response — never raise unhandled exceptions to the client:

class Mutations::CreateOrder < Mutations::BaseMutation
  argument :product_id, ID, required: true

  field :order, Types::OrderType, null: true
  field :errors, [String], null: false

  def resolve(product_id:)
    result = Orders::CreateOrder.call(user: context[:current_user], product_id: product_id)
    result.success? ? { order: result.order, errors: [] } : { order: nil, errors: result.errors }
  rescue ActiveRecord::RecordInvalid => e
    { order: nil, errors: e.record.errors.full_messages }
  rescue StandardError => e
    Rails.logger.error("Mutation failed: #{e.class}: #{e.message}")
    { order: nil, errors: ['An unexpected error occurred'] }
  end
end

Testing

See TESTING.md for the spec template, paths, and checklist (happy path, unauthenticated, unauthorized, validation errors, N+1 counts, limits).

Documentation

Write description: inline on every field in every type — no field left undescribed:

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."
end

Integration

SkillWhen to chain
ddd-ubiquitous-languageType and field naming must match business language
rails-tdd-slicesChoose first failing spec (mutation vs query vs resolver unit)
rspec-best-practicesFull TDD cycle for resolvers and mutations
rails-security-reviewAuth, introspection disable, query depth/complexity limits

rails-graphql-best-practices

README.md

tile.json