Implement GraphQL
Use this skill when designing, implementing, or reviewing GraphQL APIs in a Rails application with the graphql-ruby gem.
Quick Reference
| Concern |
Required choice |
| Specs |
Use AppSchema.execute in spec/graphql/; do not dispatch HTTP controller specs |
| Resolver shape |
Dedicated resolver/mutation classes, not inline complex field blocks |
| Associations |
Use GraphQL dataloader sources, never direct object.association loads |
| Collection resolvers |
Return a scoped relation and prime dataloader records that exposed fields will load |
| Collections |
Use Types::*Type.connection_type for paginated collections |
| Security |
Field-level authorization plus depth/complexity limits |
HARD-GATE
Tests gate implementation — write specs before resolver code (see write-tests).
Before shipping a resolver/mutation slice, ALL of the following must be true:
- 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`. 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.
- Dataloader Priming: collection resolvers prime records for association fields that will call dataloader.
Core Process
- SPEC: Write failing spec (happy path + auth + validation error case) — see TESTING.md.
- TYPE: Define arguments and return types. Use
connection_type for pagination shapes. Do not leak internal model names.
- IMPLEMENT: Create resolver/mutation class delegating to a service object. Use dedicated classes instead of inline field blocks.
- N+1 CHECK: Ensure dataloader is used on every association load. For list resolvers, prime the dataloader with the records returned by the relation before fields resolve associated objects. Use
bullet and db-query-matchers in specs.# ✅ batches loads across all records
def buyer
dataloader.with(Sources::RecordById, Buyer).load(object.buyer_id)
end
- AUTH CHECK: Apply field-level guards where data is sensitive using Pundit or custom context guards.
field :internal_notes, String, null: true do
guard -> (_obj, _args, ctx) { ctx[:current_user]&.admin? }
end
- FINAL CHECK: Verify every HARD-GATE item is met. Ensure your mutations return
{ result, errors } shapes on failure.rescue ActiveRecord::RecordInvalid => e
{ order: nil, errors: e.record.errors.full_messages }
- RUN: Ensure the full test suite is green before PR.
DO NOT proceed to step 3 before step 1 is written and failing.
Extended Resources (Progressive Disclosure)
Load these files only when their specific content is needed:
- TESTING.md — For the spec template, paths, and checklist (happy path, unauthenticated, unauthorized, validation errors, N+1 counts, limits).
- EXAMPLES.md — For detailed code examples of dataloaders, mutations, and types.
Output Style
When implementing GraphQL, your output MUST include:
- Schema contract — Types, fields, arguments, nullability, descriptions, and connection shape.
- Resolver/mutation structure — Dedicated class names and service-object delegation points.
- N+1 prevention — Dataloader source and every association load it protects.
- Authorization and limits — Field-level guards, Pundit checks, introspection/depth/complexity decisions.
- Error shape — Mutation
{ result, errors } or equivalent structured failure behavior.
- Verification —
spec/graphql/ commands covering happy path, auth, validation errors, N+1 counts, and schema limits.
- Hard-gate checklist — Explicitly verify all hard-gate items, including resolver structure, type conventions, and dataloader priming.
- Language — Must be in English unless explicitly requested otherwise.
Integration
| Skill |
When to chain |
| define-domain-language |
Type and field naming must match business language |
| plan-tests |
Choose first failing spec (mutation vs query vs resolver unit) |
| write-tests |
Full TDD cycle for resolvers and mutations |
| security-check |
Auth, introspection disable, query depth/complexity limits |
1---2name: implement-graphql3description: 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. Trigger words: graphql, graphql-ruby, resolver, mutation, dataloader, schema.4license: MIT5---6# Implement GraphQL
7
8Use this skill when **designing, implementing, or reviewing GraphQL APIs** in a Rails application with the `graphql-ruby` gem.
9
10## Quick Reference
11
12| Concern | Required choice |
13|---------|-----------------|
14| Specs | Use `AppSchema.execute` in `spec/graphql/`; do not dispatch HTTP controller specs |
15| Resolver shape | Dedicated resolver/mutation classes, not inline complex field blocks |
16| Associations | Use GraphQL dataloader sources, never direct `object.association` loads |
17| Collection resolvers | Return a scoped relation and prime dataloader records that exposed fields will load |
18| Collections | Use `Types::*Type.connection_type` for paginated collections |
19| Security | Field-level authorization plus depth/complexity limits |
20
21## HARD-GATE
22
23```text
24Tests gate implementation — write specs before resolver code (see write-tests).
25Before shipping a resolver/mutation slice, ALL of the following must be true:
26- N+1 Prevention: use `dataloader.with(Source, Model).load(id)` — NEVER `object.association`
27- Authorization: sensitive fields have field-level guards (not type-level alone).
28- Type Conventions: paginated collections use Types::*Type.connection_type, not plain arrays.
29- Schema safeguards: AppSchema disables introspection in production and sets max_depth / max_complexity.
30- TESTING.md: specs in `spec/graphql/` use `AppSchema.execute`. Never use HTTP controller dispatch for GraphQL specs.
31- Error Handling: mutations return `{ result, errors }` with rescue blocks — no unhandled exceptions.
32- Documentation: `description:` on every field in every type.
33- Resolver Structure: dedicated resolver classes, not inline field blocks.
34- Dataloader Priming: collection resolvers prime records for association fields that will call dataloader.
35```
36
37## Core Process
38
391. **SPEC:** Write failing spec (happy path + auth + validation error case) — see [TESTING.md](./TESTING.md).
402. **TYPE:** Define arguments and return types. Use `connection_type` for pagination shapes. Do not leak internal model names.
413. **IMPLEMENT:** Create resolver/mutation class delegating to a service object. Use dedicated classes instead of inline field blocks.
424. **N+1 CHECK:** Ensure dataloader is used on every association load. For list resolvers, prime the dataloader with the records returned by the relation before fields resolve associated objects. Use `bullet` and `db-query-matchers` in specs.
43 ```ruby
44 # ✅ batches loads across all records
45 def buyer
46 dataloader.with(Sources::RecordById, Buyer).load(object.buyer_id)
47 end
48 ```
495. **AUTH CHECK:** Apply field-level guards where data is sensitive using Pundit or custom context guards.
50 ```ruby
51 field :internal_notes, String, null: true do
52 guard -> (_obj, _args, ctx) { ctx[:current_user]&.admin? }
53 end
54 ```
556. **FINAL CHECK:** Verify every HARD-GATE item is met. Ensure your mutations return `{ result, errors }` shapes on failure.
56 ```ruby
57 rescue ActiveRecord::RecordInvalid => e
58 { order: nil, errors: e.record.errors.full_messages }
59 ```
607. **RUN:** Ensure the full test suite is green before PR.
61
62**DO NOT proceed to step 3 before step 1 is written and failing.**
63
64## Extended Resources (Progressive Disclosure)
65
66Load these files only when their specific content is needed:
67
68- **[TESTING.md](./TESTING.md)** — For the spec template, paths, and checklist (happy path, unauthenticated, unauthorized, validation errors, N+1 counts, limits).
69- **[EXAMPLES.md](EXAMPLES.md)** — For detailed code examples of dataloaders, mutations, and types.
70
71## Output Style
72
73When implementing GraphQL, your output MUST include:
74
751. **Schema contract** — Types, fields, arguments, nullability, descriptions, and connection shape.
762. **Resolver/mutation structure** — Dedicated class names and service-object delegation points.
773. **N+1 prevention** — Dataloader source and every association load it protects.
784. **Authorization and limits** — Field-level guards, Pundit checks, introspection/depth/complexity decisions.
795. **Error shape** — Mutation `{ result, errors }` or equivalent structured failure behavior.
806. **Verification** — `spec/graphql/` commands covering happy path, auth, validation errors, N+1 counts, and schema limits.
817. **Hard-gate checklist** — Explicitly verify all hard-gate items, including resolver structure, type conventions, and dataloader priming.
828. **Language** — Must be in English unless explicitly requested otherwise.
83
84## Integration
85
86| Skill | When to chain |
87|-------|---------------|
88| **define-domain-language** | Type and field naming must match business language |
89| **plan-tests** | Choose first failing spec (mutation vs query vs resolver unit) |
90| **write-tests** | Full TDD cycle for resolvers and mutations |
91| **security-check** | Auth, introspection disable, query depth/complexity limits |