# GRAPHQL

> GraphQL rules for modern frontend - Apollo Client, schema SDL, queries, mutations, subscriptions, fragments, normalized cache, field policies, optimistic updates, error handling, codegen, local state, testing, SSR

- Skill: `14bryanespinoza/graphql` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 14bryanespinoza/graphql`
- Raw SKILL.md: https://api.skillmd.com/api/skills/14bryanespinoza/graphql/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: 14BryanEspinoza (https://skillmd.com/u/14bryanespinoza)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/14bryanespinoza/graphql

---


# GraphQL — Rules and Conventions

---

## 1. Philosophy

1. **Schema-first** — SDL is the contract. Generate types from schema, not vice versa.
2. **Colocate queries** — Keep GraphQL documents next to components that use them.
3. **Fragments for reuse** — Define fragments on types, spread in queries.
4. **Normalized cache** — Apollo InMemoryCache with type policies = single source of truth.
5. **Type-safe end-to-end** — Codegen from schema → TypeScript types → typed hooks.

---

## 2. Minimum Versions

| Technology    | Minimum Version |
| ------------- | --------------- |
| Apollo Client | 3.10+           |
| Apollo Server | 4.10+ (if used) |
| GraphQL       | 16.8+           |
| TypeScript    | 5.4+            |
| Node.js       | 22+             |
| React         | 18.3+           |

---

## 3. Schema Fundamentals (SDL)

### Core Types

```graphql
type User {
  id: ID!
  email: String!
  name: String
  avatar: String
  posts: [Post!]!
  createdAt: DateTime!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  publishedAt: DateTime
}

type Query {
  user(id: ID!): User
  posts(filter: PostFilter, pagination: Pagination): PostConnection!
  me: User
}

type Mutation {
  createPost(input: CreatePostInput!): Post!
  updatePost(id: ID!, input: UpdatePostInput!): Post!
  deletePost(id: ID!): Boolean!
}

input PostFilter {
  authorId: ID
  search: String
  published: Boolean
}

input Pagination {
  first: Int
  after: String
}

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
}

type PostEdge {
  node: Post!
  cursor: String!
}

type PageInfo {
  hasNextPage: Boolean!
  endCursor: String
}
```

### Rules

- **ID is `ID!`** — not `String`
- **Non-null by default** — use `!` for required fields
- **Connections for lists** — Relay-style pagination
- **Scalars** — `DateTime` as ISO string, `JSON` for arbitrary data
- **Enums for fixed values** — `enum Status { DRAFT PUBLISHED ARCHIVED }`

---

## 4. Apollo Client Setup

### Client Configuration

```ts
// lib/apollo/client.ts
import { ApolloClient, InMemoryCache, HttpLink, from } from "@apollo/client";
import { onError } from "@apollo/client/link/error";
import { registerApolloClient } from "@apollo/experimental-nextjs-app-support/rsc";

const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors)
    graphQLErrors.forEach(({ message, locations, path }) =>
      console.error(`[GraphQL error]: ${message}`),
    );
  if (networkError) console.error(`[Network error]: ${networkError}`);
});

const httpLink = new HttpLink({
  uri: process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT || "/api/graphql",
  credentials: "include",
});

export const { getClient } = registerApolloClient(
  () =>
    new ApolloClient({
      link: from([errorLink, httpLink]),
      cache: new InMemoryCache({
        typePolicies: typePolicies, // see §8
      }),
      defaultOptions: {
        watchQuery: { fetchPolicy: "cache-and-network" },
        query: { fetchPolicy: "cache-first" },
      },
    }),
);
```

### Provider (Client Components)

```tsx
// providers/ApolloProvider.tsx
"use client";

import { ApolloProvider } from "@apollo/client";
import { getClient } from "@/lib/apollo/client";

export function ApolloProvider({ children }: { children: React.ReactNode }) {
  const client = getClient();
  return <ApolloClient client={client}>{children}</ApolloClient>;
}
```

---

## 5. Queries

### Query Document

```graphql
# graphql/posts.graphql
query GetPosts($filter: PostFilter, $pagination: Pagination) {
  posts(filter: $filter, pagination: $pagination) {
    edges {
      node {
        id
        title
        excerpt
        publishedAt
        author {
          id
          name
          avatar
        }
      }
      cursor
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
```

### Typed Hook (Codegen)

```tsx
// hooks/usePosts.ts
import { useQuery } from "@apollo/client";
import { GetPostsDocument, GetPostsQueryVariables } from "@/graphql/generated";

export function usePosts(variables?: GetPostsQueryVariables) {
  return useQuery<GetPostsQuery, GetPostsQueryVariables>(GetPostsDocument, {
    variables,
    notifyOnNetworkStatusChange: true,
  });
}
```

### Component Usage

```tsx
// components/PostsList.tsx
export function PostsList() {
  const { data, loading, error, fetchMore } = usePosts({
    filter: { published: true },
  });

  if (loading && !data) return <Skeleton count={5} />;
  if (error) return <Alert message="Failed to load posts" />;

  return (
    <div>
      {data?.posts.edges.map(({ node }) => (
        <PostCard key={node.id} post={node} />
      ))}
      {data?.posts.pageInfo.hasNextPage && (
        <button
          onClick={() =>
            fetchMore({
              variables: {
                pagination: { after: data.posts.pageInfo.endCursor },
              },
            })
          }
        >
          Load More
        </button>
      )}
    </div>
  );
}
```

---

## 6. Mutations

### Mutation Document

```graphql
mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
    createdAt
  }
}
```

### Typed Hook

```tsx
// hooks/useCreatePost.ts
import { useMutation } from "@apollo/client";
import {
  CreatePostDocument,
  CreatePostMutationVariables,
} from "@/graphql/generated";

export function useCreatePost() {
  const [createPost, { loading, error }] = useMutation<
    CreatePostMutation,
    CreatePostMutationVariables
  >(CreatePostDocument, {
    update(cache, { data }) {
      // Update cache with new post (see §10)
    },
    onError: (error) => toast.error(error.message),
  });
  return { createPost, loading, error };
}
```

### Component Usage Mutations

```tsx
export function CreatePostForm() {
  const [createPost, { loading }] = useCreatePost();

  return (
    <form
      onSubmit={async (e) => {
        e.preventDefault();
        const formData = new FormData(e.currentTarget);
        await createPost({
          variables: { input: Object.fromEntries(formData) },
        });
        e.currentTarget.reset();
      }}
    >
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit" disabled={loading}>
        {loading ? "Creating..." : "Create Post"}
      </button>
    </form>
  );
}
```

---

## 7. Subscriptions

### Subscription Document

```graphql
subscription OnPostCreated {
  postCreated {
    id
    title
    author {
      id
      name
    }
  }
}
```

### Usage

```tsx
import { useSubscription } from '@apollo/client'
import { OnPostCreatedDocument } from '@/graphql/generated'

export function LivePosts() {
  const { data, loading } = useSubscription(OnPostCreatedDocument)

  // Merge with existing cache data
  useEffect(() => {
    if (data?.postCreated) {
      queryClient.setQueryData(['posts'], (old) => ({
        ...old,
        edges: [{ node: data.postCreated, cursor: '' }, ...old.edges]
      })
    }
  }, [data])

  return null // Or render live indicator
}
```

### Rules Subscriptions

- **WebSocket link required** — `WebSocketLink` in Apollo Client
- **Merge with cache** — manually update cache in subscription handler
- **Auth over WS** — connectionParams for token

---

## 8. Fragments

### Define on Types

```graphql
# graphql/fragments.graphql
fragment PostSummary on Post {
  id
  title
  excerpt
  publishedAt
  author {
    id
    name
    avatar
  }
}

fragment PostDetail on Post {
  ...PostSummary
  content
  tags
  publishedAt
  author {
    id
    name
    email
    avatar
  }
}
```

### Spread in Queries

```graphql
query GetPosts($pagination: Pagination) {
  posts(pagination: $pagination) {
    edges {
      node {
        ...PostSummary
      }
      cursor
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
```

### Typed Fragment Hooks

```tsx
// hooks/usePostFragment.ts
import { PostSummaryFragmentDoc } from "@/graphql/generated";

export function PostCard({ post }: { post: PostSummaryFragment }) {
  return (
    <article>
      <h2>{post.title}</h2>
    </article>
  );
}
```

---

## 9. Caching (InMemoryCache)

### Type Policies

```ts
// lib/apollo/typePolicies.ts
import { TypePolicies } from "@apollo/client";

export const typePolicies: TypePolicies = {
  Query: {
    fields: {
      posts: {
        keyArgs: ["filter"],
        merge(existing, incoming, { args }) {
          if (!args?.pagination?.after) return incoming;
          return {
            ...incoming,
            edges: [...(existing?.edges || []), ...incoming.edges],
          };
        },
      },
      me: {
        read() {
          /* return from auth */
        },
      },
    },
  },
  Post: {
    keyFields: ["id"],
    fields: {
      author: { merge: false }, // Keep reference
    },
  },
  User: {
    keyFields: ["id"],
    fields: {
      posts: { merge: true },
    },
  },
};
```

### Cache Operations

```tsx
// Read
const data = cache.readQuery({ query: GetPostsDocument, variables });

// Write
cache.writeQuery({ query: GetPostsDocument, variables, data });

// Modify (partial update)
cache.modify({
  id: cache.identify({ __typename: "Post", id: "1" }),
  fields: {
    title() {
      return "New Title";
    },
  },
});

// Evict
cache.evict({ id: "Post:1" });
```

---

## 9. Field Policies

### Pagination Merge

```ts
posts: {
  keyArgs: ['filter'],
  merge(existing, incoming, { args }) {
    if (!args?.pagination?.after) return incoming
    return {
      ...incoming,
      edges: [...(existing?.edges || []), ...incoming.edges]
    }
  }
}
```

### Reference Fields

```ts
Post: {
  fields: {
    author: {
      merge: false, // Keep existing reference
      read(existing, { toReference }) { return toReference(existing) }
    }
  }
}
```

### Custom Reads

```ts
me: {
  read(_, { readField }) {
    return readField('id') ? { __typename: 'User', id: readField('id') } : null
  }
}
```

---

## 10. Optimistic Updates

### Mutation with Optimistic Response

```tsx
const [createPost] = useMutation(CREATE_POST_MUTATION, {
  optimisticResponse: (vars) => ({
    createPost: {
      __typename: 'Post',
      id: `temp-${Date.now()}`,
      title: vars.input.title,
      content: vars.input.content,
      publishedAt: new Date().toISOString(),
      author: { __typename: 'User', id: currentUserId, name: 'Me' }
    }
  },
  update(cache, { data }) {
    // Update list cache
    cache.modify({
      fields: {
        posts(existing, { toReference }) {
          const newPostRef = cache.writeFragment({
            data: data.createPost,
            fragment: PostSummaryFragmentDoc
          })
          return [newPostRef, ...(existing || [])]
        }
      }
    })
  }
})
```

### Rules Optimization Update

- **`optimisticResponse`** — shape matches mutation return type
- **`update` function** — manipulate cache directly
- **Rollback on error** — Apollo handles automatically
- **Type-safe** — TypeScript validates optimistic response shape

---

## 11. Error Handling

### Error Link

```ts
// lib/apollo/client.ts
import { onError } from "@apollo/client/link/error";

const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
  if (graphQLErrors) {
    for (const err of graphQLErrors) {
      if (err.extensions?.code === "UNAUTHENTICATED") {
        // Redirect to login
        window.location.href = "/login";
      }
      logError(err.message, {
        path: err.path,
        operation: operation.operationName,
      });
    }
  }
  if (networkError) {
    logError("Network error", { error: networkError });
  }
});
```

### Component Error Boundary

```tsx
import { ApolloError } from "@apollo/client";

function ErrorBoundary({ children }) {
  const [error, setError] = useState<ApolloError | null>(null);

  return (
    <ApolloErrorBoundary onError={setError}>
      {error ? (
        <Alert severity="error" onRetry={() => setError(null)}>
          {error.graphQLErrors?.[0]?.message || error.networkError?.message}
        </Alert>
      ) : (
        children
      )}
    </ApolloErrorBoundary>
  );
}
```

---

## 12. Local State

### Reactive Variables

```ts
// lib/apollo/localState.ts
import { makeVar } from "@apollo/client";

export const cartVar = makeVar<CartItem[]>([]);
export const themeVar = makeVar<"light" | "dark">("light");

// Initialize from localStorage
if (typeof window !== "undefined") {
  cartVar(JSON.parse(localStorage.getItem("cart") || "[]"));
  themeVar(localStorage.getItem("theme") === "dark" ? "dark" : "light");
}

// Persist
cartVar.onNextChange((val) =>
  localStorage.setItem("cart", JSON.stringify(val)),
);
themeVar.onNextChange((val) => localStorage.setItem("theme", val));
```

### Usage Local State

```tsx
import { useReactiveVar } from "@apollo/client";
import { cartVar } from "@/lib/apollo/localState";

export function CartCount() {
  const cart = useReactiveVar(cartVar);
  return <span>{cart.length} items</span>;
}

function AddToCart({ product }) {
  const add = () => cartVar((current) => [...current, { product, qty: 1 }]);
  return <button onClick={add}>Add to Cart</button>;
}
```

---

## 13. Codegen

### Config

```ts
// graphql-codegen.config.ts
import { defineConfig } from "@graphql-codegen/cli";

export default defineConfig({
  schema: "https://api.example.com/graphql",
  documents: ["src/**/*.graphql", "src/**/*.tsx"],
  generates: {
    "src/graphql/generated.ts": {
      plugins: [
        "typescript",
        "typescript-operations",
        "typescript-react-apollo",
        "typed-document-node",
      ],
      config: {
        withHooks: true,
        withHOC: false,
        withComponent: false,
        scalars: { DateTime: "string", JSON: "Record<string, unknown>" },
      },
    },
  },
});
```

### Generated Types

```ts
// src/graphql/generated.ts (auto-generated)
export type GetPostsQuery = { posts: { edges: PostEdge[]; pageInfo: PageInfo } }
export type GetPostsQueryVariables = { filter?: PostFilter; pagination?: Pagination }
export const GetPostsDocument = { kind: 'Document', definitions: [...] }
```

---

## 14. Methodology

Before using ANY GraphQL pattern not documented in this skill:

1. **MCP Context7** (priority): `context7_resolve-library-id` + `context7_query-docs` for Apollo Client, GraphQL Code Generator.
2. **Official docs**: apollographql.com/docs, graphql.org — verify current APIs.
3. **Project config**: `apollo-client.ts`, `codegen.ts`, `tsconfig.json` — verify against actual setup.
4. **HARD RULE**: If not in this skill AND cannot be verified against 2 authoritative sources → DO NOT USE IT. Document as assumption or risk in report to orchestrator.

---

## 15. Prohibitions

- ❌ Do not write queries inline in components — use `.graphql` files + codegen
- ❌ Do not skip fragments — reuse via fragment spreading
- ❌ Do not use `fetchPolicy: 'network-only'` everywhere — default cache-first
- ❌ Do not skip `keyFields` in typePolicies — cache needs identity
- ❌ Do not use `any` in typed hooks — codegen provides strict types
- ❌ Do not mutate cache directly outside `update` functions
- ❌ Do not skip error handling — GraphQL errors are not exceptions
- ❌ Do not use subscriptions without auth on WebSocket connection
- ❌ Do not store sensitive data in reactive variables — localStorage is readable

---

## 16. References

> **Note:** For React patterns (hooks, components), see [React](../reactjs/SKILL.md)
> **Note:** For TypeScript rules, see [TypeScript](../typescript/SKILL.md)
> **Note:** For JavaScript conventions, see [JavaScript](../javascript/SKILL.md)
> **Note:** For Next.js patterns (SSR), see [Next.js](../nextjs/SKILL.md)
> **Note:** For Testing patterns, see [Testing](../testing/SKILL.md)
> **Note:** For Security (rate limiting, auth), see [Security](../security/SKILL.md)
> **Note:** For Performance (caching, batching), see [Performance](../performance/SKILL.md)
> **Note:** For Deployment, see [Deploy](../deploy/SKILL.md)

---

Last updated: 2026-08

