GraphQL has its own mental model: a schema defines the contract, resolvers handle the data, clients ask for exactly what they need. AI tools that treat GraphQL like REST produce schemas that fight the framework at every level.
The most common failure modes: REST-style mutations that return nothing useful, overfetching queries that ignore fields, schemas that duplicate database column names instead of expressing domain concepts, and missing type safety at the resolver layer.
A CLAUDE.md file that declares the GraphQL model prevents these patterns from appearing. Here are 13 rules.
Rule 1: Schema is the source of truth — not the database
The GraphQL schema defines the API contract independently
of the database schema. Field names should reflect domain
concepts, not database column names.
Do NOT mirror the database tables 1:1 in the GraphQL schema.
Types should represent business entities, not storage shapes.
Rule 2: Mutations return meaningful types
Every mutation must return a meaningful result type, not Boolean
or a simple success flag:
type CreateUserPayload { user: User! errors: [UserError!]! }
Do NOT return Boolean or null from mutations.
Return union types for error handling at the type level.
Rule 3: Queries are fields, not endpoints
Think in fields, not endpoints.
Bad: getUserById(id: ID!): User — REST thinking
Good: user(id: ID!): User on the Query type
Each query field resolves to a type; the type resolves its fields.
Do NOT name query fields like REST endpoints.
Rule 4: Use fragments for reusable field sets
Repeated field selections should be extracted into fragments.
fragment UserBasic on User { id name email }
Use fragments on the client; do NOT inline the same fields
on every query that needs the same data shape.
Rule 5: Connections for paginated lists
For paginated collections, use the Relay connection pattern:
type UserConnection { edges: [UserEdge!]! pageInfo: PageInfo! }
type UserEdge { node: User! cursor: String! }
Do NOT return [User!]! directly for lists that will be paginated.
Include totalCount in connections when the UI needs it.
Rule 6: DataLoader for N+1 prevention
All resolver functions that load related entities MUST use
DataLoader for batching. Do NOT call the database directly
inside a resolver that may run per-item in a list.
Every field that resolves a relationship needs a DataLoader.
Rule 7: Input types for mutations
All mutation arguments use input types:
input CreateUserInput { name: String! email: String! }
createUser(input: CreateUserInput!): CreateUserPayload!
Do NOT use individual scalar arguments for mutations with
more than 2 fields. Input types are required for nullable
and optional fields on mutations.
Rule 8: Enums for fixed value sets
Status fields, category fields, and any finite value set
use GraphQL enums:
enum UserStatus { ACTIVE SUSPENDED DELETED }
Do NOT use String for fields that have a fixed set of values.
Enum values use SCREAMING_SNAKE_CASE.
Rule 9: Non-null discipline
Apply non-null (!) based on business rules, not optimism.
- IDs are always non-null
- Required fields that can never be absent: non-null
- Optional fields that may not be set: nullable
Do NOT make everything non-null to silence TypeScript warnings.
Nullable means "this field may not exist in valid business states."
Rule 10: Error handling at the type level
Use union types for operations that can fail:
union CreateUserResult = User | ValidationError | AuthError
Or use the payload pattern with an errors array (see Rule 2).
Do NOT use exceptions as the primary error communication path.
The schema should express failure modes as valid types.
Rule 11: Subscriptions for real-time data
Real-time data uses GraphQL subscriptions, not polling.
subscription OnUserUpdated($id: ID!) { userUpdated(id: $id) { ...UserBasic } }
Do NOT implement polling queries when subscriptions can serve
the use case with a subscription-capable transport.
Rule 12: Resolver structure follows the schema
Resolvers are organized to mirror the schema structure:
Query resolvers → query field resolvers
Mutation resolvers → mutation field resolvers
Type resolvers → field-level resolvers for each type
Do NOT put all business logic in Query resolvers.
Each type should have its own resolver map.
Rule 13: Type generation is mandatory
All resolver types, query variables, and response shapes
are generated from the schema (via GraphQL Code Generator
or equivalent). Do NOT write resolver type signatures
by hand — generate them from the schema.
Types: generated; Business logic: handwritten.
The full CLAUDE.md block
# GraphQL Project Rules
## Schema Design
- Schema expresses domain concepts, not database columns
- Mutations return meaningful payload types (never Boolean)
- Query fields named as domain concepts, not REST endpoints
- Enums for all fixed value sets (SCREAMING_SNAKE_CASE)
- Non-null (!) only where business rules require it
## Pagination
- Relay connection pattern for paginated lists
- [Type!]! only for guaranteed-complete non-paginated lists
- Include totalCount in connections when needed
## Resolvers
- DataLoader required for all relationship resolvers (N+1)
- Resolver map mirrors schema structure (Query/Mutation/Types)
- Business logic in services, not in resolvers
## Mutations
- Input types required for mutations with >2 fields
- Return union types or payload with errors array
- Error handling at the type level, not via exceptions
## Types
- Generated from schema (GraphQL Code Generator or equivalent)
- Never write resolver type signatures by hand
## Real-time
- Use subscriptions, not polling queries
Why GraphQL AI drift is predictable
REST is the default mental model for API design. Every tutorial, every framework, every Stack Overflow answer about "how do I build an API" reaches for REST. GraphQL is a fundamentally different model — declare what you want, not what endpoint to call.
Without explicit instruction, AI produces REST-shaped GraphQL: mutations that return Boolean, queries named like endpoints, no DataLoader, manual type signatures. The schema becomes a thin wrapper over REST instead of a typed graph.
The CLAUDE.md file declares the GraphQL model before any schema is generated.
These 13 rules are part of a framework-specific CLAUDE.md series. The Cursor Rules Pack equivalent (50 production-tested rules across all stacks) is available at oliviacraftlat.gumroad.com/l/wyaeil — search "Olivia Craft Cursor Rules" on Gumroad.
Top comments (0)