Why GraphQL Needs Specific AI Rules
GraphQL gives clients the power to request exactly the data they need — but that power comes with performance and security risks that AI assistants don't manage by default. Without rules, AI generates resolvers that trigger N+1 queries for every nested field, schemas without pagination, mutations without input validation, and no protection against malicious queries that request the entire database.
The core issue: AI assistants treat GraphQL resolvers like REST endpoints — one query per resolver. But GraphQL's nested field resolution means a single query can trigger hundreds of database calls if resolvers aren't properly batched. The AI doesn't think about the execution model unless you tell it to.
These rules apply regardless of your GraphQL implementation — Apollo Server, GraphQL Yoga, Mercurius, Strawberry, or gqlgen. The patterns are universal; the implementation varies.
Rule 1: Schema Design Patterns
The rule: 'Design schemas as a graph, not as REST endpoints mapped to types. Types represent domain entities with relationships. Use connection patterns (edges/nodes) for paginated lists. Use input types for all mutation arguments — never pass scalar arguments directly. Use union types and interfaces for polymorphic returns.'
For naming: 'Use PascalCase for types (User, OrderItem). Use camelCase for fields (firstName, createdAt). Mutations are verbs: createUser, updateOrder, deleteComment. Queries are nouns: user, orders, searchResults. Subscriptions are events: onOrderCreated, onMessageReceived.'
For nullability: 'Fields are nullable by default in GraphQL — use non-null (!) deliberately. ID fields are always non-null. Fields that are always present in the database are non-null. Fields that depend on permissions or optional joins are nullable. Never make every field non-null — it prevents graceful degradation on partial failures.'
- Connection pattern for paginated lists — edges/nodes with pageInfo
- Input types for all mutation arguments — never raw scalars
- PascalCase types, camelCase fields, verb mutations, noun queries
- Non-null (!) only for fields guaranteed to exist — nullable by default
- Union types for polymorphic returns — interfaces for shared fields
Rule 2: DataLoader for N+1 Prevention
The N+1 problem is the #1 performance issue in GraphQL APIs. A query like { users { posts { comments } } } with naive resolvers triggers: 1 query for users, N queries for posts (one per user), and M queries for comments (one per post). For 100 users with 10 posts each, that's 1,101 queries.
The rule: 'Use DataLoader for all nested field resolvers that access a database or external service. Create one DataLoader instance per request (not global). Batch all lookups by parent ID. Every resolver that loads related data must go through a DataLoader — never query the database directly in a field resolver.'
For implementation: 'Create a DataLoader context factory that instantiates fresh loaders per request. Loaders batch and deduplicate within a single GraphQL execution. This turns 1,101 queries into 3 queries (one per level) with IN clauses.'
Without DataLoader, a query for 100 users with 10 posts each triggers 1,101 database queries. With DataLoader, it's 3 queries. This single pattern is the difference between a fast API and an unusable one.
Rule 3: Cursor-Based Pagination
The rule: 'All list fields that can return more than 50 items must be paginated. Use cursor-based pagination (Relay connection spec) — never offset-based pagination. Connection type: { edges: [{ node: T, cursor: String }], pageInfo: { hasNextPage, hasPreviousPage, startCursor, endCursor } }. Accept first/after for forward pagination, last/before for backward pagination.'
For default limits: 'Set a default page size (20-50 items). Set a maximum page size (100 items). If the client requests more than the maximum, silently cap at the maximum — never return unbounded result sets. Include the total count only if it's cheap to compute — expensive counts should be a separate query.'
AI assistants default to returning all items (no pagination) or offset-based pagination (breaks on mutations that add/remove items). Cursor pagination is the correct pattern for GraphQL — this rule prevents both anti-patterns.
Offset pagination breaks when items are added/removed between pages. Cursor pagination is stable, performant, and the Relay spec standard. Always use first/after, last/before.
Rule 4: Query Complexity and Security
GraphQL's flexibility is a security risk: a malicious client can craft a deeply nested query that triggers exponential database load. Without limits, a single query can DDoS your own API.
The rule: 'Implement query depth limiting (max 10 levels). Implement query complexity analysis — assign costs to fields and reject queries exceeding the budget. Implement rate limiting per client/IP. Disable introspection in production — it exposes your entire schema. Never expose internal error messages to clients — log details server-side, return generic error types to clients.'
For authentication: 'Check authorization at the resolver level, not just the query level. A user may have access to query { user(id: 1) } but not query { user(id: 2) }. Use middleware or directives (@auth, @hasRole) for declarative authorization. Never rely on the client to filter unauthorized data — the resolver must enforce access control.'
- Query depth limit: max 10 levels of nesting
- Complexity analysis: assign costs to fields, reject over-budget queries
- Rate limiting per client/IP — prevent query-based DDoS
- Introspection disabled in production — enabled in development only
- Resolver-level authorization — never trust client-side filtering
- Generic error messages to clients — detailed logs server-side
A single deeply nested GraphQL query can DDoS your own API. Depth limits (max 10), complexity budgets, and rate limiting are security essentials — not optimizations.
Rule 5: Mutation Input Validation
The rule: 'Validate all mutation inputs at the resolver level before processing. Use custom scalar types (DateTime, Email, URL) for domain-specific validation. Return structured error types — not generic strings. Use a union return type for mutations: type CreateUserResult = User | ValidationError | NotFoundError. This makes error handling type-safe on the client.'
For error patterns: 'Define a standard error interface: interface Error { message: String!, code: ErrorCode!, path: [String!] }. Return arrays of errors for validation failures. Use error codes (VALIDATION_ERROR, NOT_FOUND, UNAUTHORIZED) instead of HTTP status codes — GraphQL always returns 200 at the transport level.'
AI assistants generate mutations that assume valid input and throw generic errors. This rule enforces type-safe error handling that clients can pattern-match against.
Complete GraphQL Rules Template
Consolidated rules for GraphQL APIs regardless of implementation language.
- Schema as graph: connection pattern, input types for mutations, union return types
- DataLoader for all nested resolvers — batch by parent ID, one instance per request
- Cursor-based pagination on all lists >50 items — first/after, last/before
- Query depth limit (10) + complexity analysis + rate limiting
- Introspection off in production — generic client errors, detailed server logs
- Resolver-level authorization — never trust client-side filtering
- Structured mutation errors: union types with error interfaces and codes
- Custom scalars (DateTime, Email) — validation at the schema level