Rule Writing

CLAUDE.md for GraphQL and Apollo

AI generates REST-shaped GraphQL and skips Apollo's cache. Rules for schema design, resolvers, code generation, Apollo Client cache, and type-safe operations.

8 min read·November 20, 2025

Apollo's cache makes GraphQL faster than REST — AI ignores it and refetches everything

Graph schema, codegen types, cache policies, fragments, and DataLoader resolvers

Why GraphQL + Apollo Needs Schema and Cache Rules

GraphQL with Apollo Client is the most powerful data fetching layer in frontend development — but AI generates REST patterns wrapped in GraphQL syntax. Instead of designing a graph that clients traverse, AI creates one fat query per page (like a REST endpoint). Instead of using Apollo's normalized cache, AI refetches on every navigation. Instead of using GraphQL Code Generator for types, AI writes manual TypeScript interfaces that drift from the schema.

The result is GraphQL that's slower, harder to maintain, and less type-safe than the REST API it replaced — with the added complexity of a schema, resolvers, and a client-side cache that nobody uses correctly.

These rules cover both the server (schema design, resolvers) and the client (Apollo Client, caching, codegen). They target Apollo Server 4+ and Apollo Client 3+.

Rule 1: Schema as a Graph, Not REST Endpoints

The rule: 'Design the schema as a traversable graph — not as REST endpoints mapped to GraphQL types. Types are domain entities connected by relationships. User has posts, Post has author and comments, Comment has author. Clients traverse the graph to get exactly the data they need. Never create one query per page — create reusable types connected by relationships.'

For schema design: 'Use the Relay connection pattern for paginated lists: type UserConnection { edges: [UserEdge!]!; pageInfo: PageInfo! }. Use interfaces for shared fields: interface Node { id: ID! }. Use unions for polymorphic returns: union SearchResult = User | Post | Comment. Use input types for all mutation arguments.'

AI generates: query GetDashboard { dashboardData { users { ... } recentOrders { ... } stats { ... } } } — one blob per page. The graph approach: query { viewer { name orders(first: 5) { edges { node { total items { name } } } } } } — the client picks exactly what it needs from connected types.

  • Types are domain entities with relationships — not page-shaped blobs
  • Relay connection pattern for all paginated lists — edges/nodes/pageInfo
  • Interfaces for shared fields — unions for polymorphic returns
  • Input types for all mutation arguments — never bare scalars
  • Clients traverse the graph — they don't fetch page-shaped endpoints

Rule 2: GraphQL Code Generator for All Types

The rule: 'Use @graphql-codegen/cli to generate TypeScript types from the schema and operations. Never write GraphQL types manually — they drift from the schema. Configure codegen.yml: generates typed hooks (useGetUserQuery), typed documents (GetUserDocument), and typed result types. Run codegen after every schema change: npx graphql-codegen --watch in development.'

For typed operations: 'Write operations in .graphql files: query GetUser($id: ID!) { user(id: $id) { id name email posts { title } } }. Codegen generates: a typed hook (useGetUserQuery({ variables: { id } })), the result type (GetUserQuery), and the variables type (GetUserQueryVariables). The client is fully typed end-to-end.'

AI generates manual TypeScript interfaces for GraphQL responses — they're wrong the moment the schema changes. Codegen eliminates this: schema changes automatically update every type, every hook, and every variable definition. If the schema removes a field, codegen breaks the build — catching the error before runtime.

💡 Codegen Breaks the Build

When the schema removes a field, codegen removes it from the TypeScript type. Every query using that field fails at compile time — not at runtime in production. Manual interfaces drift silently; codegen catches immediately.

Rule 3: Apollo Client Cache Policies

The rule: 'Configure Apollo Client's normalized cache — it's the primary performance feature of Apollo Client. Every entity with an id field is cached by type and ID: cache: new InMemoryCache({ typePolicies: { User: { keyFields: ["id"] } } }). After a mutation, update the cache instead of refetching: update: (cache, { data }) => { cache.modify({ ... }) }. Use cache-first fetch policy for data that doesn't change often, network-only for data that must be fresh.'

For fetch policies: 'cache-first (default): read from cache if available, fetch only on miss. network-only: always fetch, update cache. cache-and-network: read from cache immediately, then fetch and update. no-cache: fetch and don't cache. Use cache-first for: user profiles, categories, static content. Use network-only for: dashboards, real-time data, search results.'

AI uses no-cache or refetchQueries on everything — defeating the cache entirely. Apollo's normalized cache is what makes GraphQL faster than REST on subsequent navigations: entities fetched on one page are instantly available on another. Ignoring the cache makes Apollo a slow fetch() wrapper.

⚠️ No-Cache = Slow Apollo

Using no-cache or refetchQueries everywhere defeats Apollo's primary feature. The normalized cache means entities fetched on page A are instantly available on page B. Ignoring the cache makes Apollo a slow fetch() wrapper.

Rule 4: Fragments for Component Data Requirements

The rule: 'Use fragments to define each component's data requirements: fragment UserCard on User { id name avatar email }. Components export their fragment, parent queries include it: query GetUsers { users { ...UserCard } }. This is colocated data fetching — the component declares what it needs, the query composes fragments. Never duplicate field selections across queries — use fragments.'

For fragment composition: 'UserCard needs name and avatar. UserProfile needs name, avatar, email, bio, createdAt. UserProfile's fragment extends UserCard's: fragment UserProfile on User { ...UserCard email bio createdAt }. When UserCard adds a field, every query that includes it automatically gets the new field.'

AI duplicates field selections in every query: query A selects { id name email }, query B selects { id name email avatar }. One renamed field requires updating every query. Fragments centralize the field list — one change propagates everywhere.

  • Fragment per component: fragment UserCard on User { id name avatar }
  • Queries compose fragments: query { users { ...UserCard } }
  • Components export fragments — parent queries include them
  • Fragment extension: UserProfile extends UserCard with additional fields
  • Never duplicate field selections — fragments are the single source of truth
ℹ️ Fragments = Colocated Data

Each component exports a fragment declaring its data needs. Parent queries compose fragments. One field change in a fragment propagates to every query that includes it. No more hunting through 20 queries to add a field.

Rule 5: Resolver Patterns

The rule: 'Use DataLoader for all nested field resolvers: const userLoader = new DataLoader(async (ids) => { const users = await db.users.findMany({ where: { id: { in: ids } } }); return ids.map(id => users.find(u => u.id === id)); }). Create one loader per request (in context). Use resolver chains for computed fields — the parent resolver provides the entity, child resolvers compute or load additional data.'

For context: 'Create context per request with: authenticated user, DataLoaders, database connection. const context = async ({ req }) => ({ user: await getUser(req), loaders: { user: new DataLoader(...), post: new DataLoader(...) }, db }). Never create DataLoaders outside the request — they batch within a single request, not across requests.'

For error handling: 'Use GraphQL errors with extensions: throw new GraphQLError("Not found", { extensions: { code: "NOT_FOUND" } }). Never throw generic Error — GraphQL errors include the path (which field failed) and extensions (error code for client handling). Use formatError on the server to strip internal details from production error responses.'

Complete GraphQL + Apollo Rules Template

Consolidated rules for GraphQL with Apollo Server and Apollo Client.

  • Schema as a graph: connected types, Relay connections, interfaces, unions — not REST blobs
  • GraphQL Code Generator: typed hooks, typed documents — never manual TypeScript interfaces
  • Apollo cache: normalized InMemoryCache, typePolicies, cache-first default — never no-cache for all
  • Fragments per component: colocated data requirements — never duplicate field selections
  • DataLoader for all nested resolvers — one loader per request in context
  • GraphQLError with extensions codes — formatError for production — never generic Error
  • Fetch policies per use case: cache-first for static, network-only for dynamic
  • Apollo DevTools for debugging — schema-first development — codegen in CI