graphql-operations
Guide for writing GraphQL operations (queries, mutations, fragments) following best practices. Use this skill when: (1) writing GraphQL queries or mutations, (2) organizing operations with fragments, (3) optimizing data fetching patterns, (4) setting up type generation or linting, (5) reviewing operations for efficiency.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install apollographql-skills-graphql-operations
Repository
Skill path: skills/graphql-operations
Guide for writing GraphQL operations (queries, mutations, fragments) following best practices. Use this skill when: (1) writing GraphQL queries or mutations, (2) organizing operations with fragments, (3) optimizing data fetching patterns, (4) setting up type generation or linting, (5) reviewing operations for efficiency.
Open repositoryBest for
Primary workflow: Write Technical Docs.
Technical facets: Full Stack, Data / AI, Tech Writer.
Target audience: everyone.
License: MIT.
Original source
Catalog source: SkillHub Club.
Repository owner: apollographql.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install graphql-operations into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/apollographql/skills before adding graphql-operations to shared team environments
- Use graphql-operations for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: graphql-operations
description: >
Guide for writing GraphQL operations (queries, mutations, fragments) following best practices. Use this skill when:
(1) writing GraphQL queries or mutations,
(2) organizing operations with fragments,
(3) optimizing data fetching patterns,
(4) setting up type generation or linting,
(5) reviewing operations for efficiency.
license: MIT
compatibility: Any GraphQL client (Apollo Client, urql, Relay, etc.)
metadata:
author: apollographql
version: "1.0.0"
allowed-tools: Bash(npm:*) Bash(npx:*) Read Write Edit Glob Grep
---
# GraphQL Operations Guide
This guide covers best practices for writing GraphQL operations (queries, mutations, subscriptions) as a client developer. Well-written operations are efficient, type-safe, and maintainable.
## Operation Basics
### Query Structure
```graphql
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
```
### Mutation Structure
```graphql
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
createdAt
}
}
```
### Subscription Structure
```graphql
subscription OnMessageReceived($channelId: ID!) {
messageReceived(channelId: $channelId) {
id
content
sender {
id
name
}
}
}
```
## Quick Reference
### Operation Naming
| Pattern | Example |
| ------------ | ------------------------------------------- |
| Query | `GetUser`, `ListPosts`, `SearchProducts` |
| Mutation | `CreateUser`, `UpdatePost`, `DeleteComment` |
| Subscription | `OnMessageReceived`, `OnUserStatusChanged` |
### Variable Syntax
```graphql
# Required variable
query GetUser($id: ID!) { ... }
# Optional variable with default
query ListPosts($first: Int = 20) { ... }
# Multiple variables
query SearchPosts($query: String!, $status: PostStatus, $first: Int = 10) { ... }
```
### Fragment Syntax
```graphql
# Define fragment
fragment UserBasicInfo on User {
id
name
avatarUrl
}
# Use fragment
query GetUser($id: ID!) {
user(id: $id) {
...UserBasicInfo
email
}
}
```
### Directives
```graphql
query GetUser($id: ID!, $includeEmail: Boolean!) {
user(id: $id) {
id
name
email @include(if: $includeEmail)
}
}
query GetPosts($skipDrafts: Boolean!) {
posts {
id
title
draft @skip(if: $skipDrafts)
}
}
```
## Key Principles
### 1. Request Only What You Need
```graphql
# Good: Specific fields
query GetUserName($id: ID!) {
user(id: $id) {
id
name
}
}
# Avoid: Over-fetching
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
bio
posts {
id
title
content
comments {
id
}
}
followers {
id
name
}
# ... many unused fields
}
}
```
### 2. Name All Operations
```graphql
# Good: Named operation
query GetUserPosts($userId: ID!) {
user(id: $userId) {
posts {
id
title
}
}
}
# Avoid: Anonymous operation
query {
user(id: "123") {
posts {
id
title
}
}
}
```
### 3. Use Variables, Not Inline Values
```graphql
# Good: Variables
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
# Avoid: Hardcoded values
query {
user(id: "123") {
id
name
}
}
```
### 4. Colocate Fragments with Components
```tsx
// UserAvatar.tsx
export const USER_AVATAR_FRAGMENT = gql`
fragment UserAvatar on User {
id
name
avatarUrl
}
`;
function UserAvatar({ user }) {
return <img src={user.avatarUrl} alt={user.name} />;
}
```
## Reference Files
Detailed documentation for specific topics:
- [Queries](references/queries.md) - Query patterns and optimization
- [Mutations](references/mutations.md) - Mutation patterns and error handling
- [Fragments](references/fragments.md) - Fragment organization and reuse
- [Variables](references/variables.md) - Variable usage and types
- [Tooling](references/tooling.md) - Code generation and linting
## Ground Rules
- ALWAYS name your operations (no anonymous queries/mutations)
- ALWAYS use variables for dynamic values
- ALWAYS request only the fields you need
- ALWAYS include `id` field for cacheable types
- NEVER hardcode values in operations
- NEVER duplicate field selections across files
- PREFER fragments for reusable field selections
- PREFER colocating fragments with components
- USE descriptive operation names that reflect purpose
- USE `@include`/`@skip` for conditional fields
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/queries.md
```markdown
# Query Patterns
This reference covers patterns for writing effective GraphQL queries.
## Table of Contents
- [Query Structure](#query-structure)
- [Field Selection](#field-selection)
- [Aliases](#aliases)
- [Directives](#directives)
- [Query Naming](#query-naming)
- [Query Organization](#query-organization)
- [Performance Optimization](#performance-optimization)
## Query Structure
### Basic Query
```graphql
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
```
Components:
- `query` - Operation type
- `GetUser` - Operation name
- `($id: ID!)` - Variable definitions
- `user(id: $id)` - Field with argument
- `{ id name email }` - Selection set
### Query with Multiple Root Fields
```graphql
query GetDashboardData($userId: ID!) {
user(id: $userId) {
id
name
}
notifications(first: 5) {
id
message
}
stats {
totalPosts
totalComments
}
}
```
### Nested Queries
```graphql
query GetUserWithPosts($userId: ID!) {
user(id: $userId) {
id
name
posts(first: 10) {
edges {
node {
id
title
comments(first: 3) {
edges {
node {
id
body
}
}
}
}
}
}
}
}
```
## Field Selection
### Request Only Needed Fields
```graphql
# For a user card component
query GetUserCard($id: ID!) {
user(id: $id) {
id
name
avatarUrl
# Don't request email, bio, etc. if not displayed
}
}
```
### Always Include ID Fields
Include `id` for any type you might cache or refetch:
```graphql
query GetPost($id: ID!) {
post(id: $id) {
id # Always include for caching
title
author {
id # Include for author cache entry
name
}
}
}
```
### Selecting Connections
For paginated data, request what you need:
```graphql
query GetUserPosts($userId: ID!, $first: Int!, $after: String) {
user(id: $userId) {
id
posts(first: $first, after: $after) {
edges {
node {
id
title
excerpt
}
cursor # Only if implementing infinite scroll
}
pageInfo {
hasNextPage
endCursor
}
totalCount # Only if displaying total
}
}
}
```
## Aliases
### Basic Alias
Rename fields in the response:
```graphql
query GetUserNames($id: ID!) {
user(id: $id) {
userId: id
displayName: name
}
}
# Response: { user: { userId: "123", displayName: "John" } }
```
### Query Same Field Multiple Times
```graphql
query GetMultipleUsers {
admin: user(id: "1") {
id
name
}
moderator: user(id: "2") {
id
name
}
currentUser: user(id: "3") {
id
name
}
}
```
### Alias with Different Arguments
```graphql
query GetPostsByStatus($userId: ID!) {
user(id: $userId) {
id
publishedPosts: posts(status: PUBLISHED, first: 5) {
edges {
node {
id
title
}
}
}
draftPosts: posts(status: DRAFT, first: 5) {
edges {
node {
id
title
}
}
}
}
}
```
## Directives
### @include Directive
Include field only if condition is true:
```graphql
query GetUser($id: ID!, $includeEmail: Boolean!) {
user(id: $id) {
id
name
email @include(if: $includeEmail)
}
}
# Variables: { id: "123", includeEmail: true }
# Returns email field
# Variables: { id: "123", includeEmail: false }
# Does not return email field
```
### @skip Directive
Skip field if condition is true:
```graphql
query GetPost($id: ID!, $isPreview: Boolean!) {
post(id: $id) {
id
title
content @skip(if: $isPreview)
excerpt
}
}
```
### Directives on Fragments
```graphql
query GetUser($id: ID!, $expanded: Boolean!) {
user(id: $id) {
id
name
...UserDetails @include(if: $expanded)
}
}
fragment UserDetails on User {
bio
website
socialLinks {
platform
url
}
}
```
### Combining Directives
```graphql
query GetPost($id: ID!, $showComments: Boolean!, $hideAuthor: Boolean!) {
post(id: $id) {
id
title
author @skip(if: $hideAuthor) {
id
name
}
comments(first: 10) @include(if: $showComments) {
edges {
node {
id
body
}
}
}
}
}
```
## Query Naming
### Naming Conventions
| Purpose | Pattern | Examples |
| --------------------- | ------------------ | ------------------------------------ |
| Fetch single item | `Get{Type}` | `GetUser`, `GetPost` |
| Fetch list | `List{Types}` | `ListUsers`, `ListPosts` |
| Search | `Search{Types}` | `SearchUsers`, `SearchProducts` |
| Fetch for specific UI | `Get{Feature}Data` | `GetDashboardData`, `GetProfilePage` |
### Good Names
```graphql
query GetUserProfile($id: ID!) { ... }
query ListRecentPosts($first: Int!) { ... }
query SearchProducts($query: String!) { ... }
query GetOrderDetails($orderId: ID!) { ... }
query GetHomeFeed($userId: ID!) { ... }
```
### Avoid Generic Names
```graphql
# Avoid
query Data { ... }
query Query1 { ... }
query FetchStuff { ... }
# Prefer
query GetCurrentUser { ... }
query ListActiveProjects { ... }
query SearchCustomers($query: String!) { ... }
```
## Query Organization
### One Query Per File
```
src/
graphql/
queries/
GetUser.graphql
ListPosts.graphql
SearchProducts.graphql
```
```graphql
# GetUser.graphql
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
```
### Colocate with Components
```
src/
components/
UserProfile/
UserProfile.tsx
UserProfile.graphql
UserProfile.test.tsx
```
### Import and Use
```typescript
// With graphql-tag
import { gql } from "@apollo/client";
export const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
`;
// With .graphql files (requires loader)
import { GetUserDocument } from "./UserProfile.generated";
```
## Performance Optimization
### Avoid Over-fetching
Only request fields used by your component:
```graphql
# For a list view - minimal fields
query ListPostsForIndex {
posts(first: 20) {
edges {
node {
id
title
excerpt
author { name }
}
}
}
}
# For detail view - more fields
query GetPostDetail($id: ID!) {
post(id: $id) {
id
title
content
publishedAt
author {
id
name
bio
avatarUrl
}
comments(first: 10) { ... }
}
}
```
### Use Pagination
Never fetch unbounded lists:
```graphql
# Avoid
query GetAllPosts {
posts {
# Could return thousands
id
title
}
}
# Prefer
query GetPosts($first: Int = 20, $after: String) {
posts(first: $first, after: $after) {
edges {
node {
id
title
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
```
### Batch Related Queries
Fetch related data in one request:
```graphql
# Instead of multiple queries
query GetDashboard($userId: ID!) {
user(id: $userId) {
id
name
}
recentPosts: posts(first: 5, orderBy: { field: CREATED_AT, direction: DESC }) {
edges {
node {
id
title
}
}
}
notifications(first: 10, unreadOnly: true) {
edges {
node {
id
message
}
}
}
}
```
### Use Fragments for Repeated Selections
```graphql
query GetPostsWithAuthors {
posts(first: 10) {
edges {
node {
id
title
author {
...AuthorInfo
}
}
}
}
featuredPost {
id
title
author {
...AuthorInfo
}
}
}
fragment AuthorInfo on User {
id
name
avatarUrl
}
```
```
### references/mutations.md
```markdown
# Mutation Patterns
This reference covers patterns for writing effective GraphQL mutations.
## Table of Contents
- [Mutation Structure](#mutation-structure)
- [Input Patterns](#input-patterns)
- [Response Selection](#response-selection)
- [Error Handling](#error-handling)
- [Optimistic Updates](#optimistic-updates)
- [Mutation Naming](#mutation-naming)
## Mutation Structure
### Basic Mutation
```graphql
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
createdAt
}
}
```
Variables:
```json
{
"input": {
"title": "My Post",
"content": "Post content..."
}
}
```
### Mutation with Multiple Arguments
```graphql
mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
updatePost(id: $id, input: $input) {
id
title
updatedAt
}
}
```
### Multiple Mutations
Execute multiple mutations in one request (sequential execution):
```graphql
mutation SetupUserProfile($userId: ID!, $profileInput: ProfileInput!, $settingsInput: SettingsInput!) {
updateProfile(userId: $userId, input: $profileInput) {
id
bio
}
updateSettings(userId: $userId, input: $settingsInput) {
id
theme
notifications
}
}
```
## Input Patterns
### Single Input Object
Recommended pattern - single input argument:
```graphql
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
email
}
}
```
```json
{
"input": {
"email": "[email protected]",
"name": "John Doe",
"password": "secret123"
}
}
```
### Nested Input Objects
```graphql
mutation CreateOrder($input: CreateOrderInput!) {
createOrder(input: $input) {
id
total
}
}
```
```json
{
"input": {
"items": [
{ "productId": "prod_1", "quantity": 2 },
{ "productId": "prod_2", "quantity": 1 }
],
"shippingAddress": {
"street": "123 Main St",
"city": "New York",
"country": "US"
}
}
}
```
### Optional Fields
```graphql
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
bio
}
}
```
```json
{
"id": "user_123",
"input": {
"name": "New Name"
// bio not included - won't be changed
}
}
```
## Response Selection
### Return the Modified Object
Always return the mutated object with updated fields:
```graphql
mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
updatePost(id: $id, input: $input) {
id
title
content
updatedAt # Server-set field
}
}
```
### Return Related Objects
If mutation affects related data, include it:
```graphql
mutation AddComment($input: AddCommentInput!) {
addComment(input: $input) {
id
body
post {
id
commentCount # Updated count
}
author {
id
name
}
}
}
```
### Return for Cache Updates
Select fields needed to update your cache:
```graphql
mutation DeletePost($id: ID!) {
deletePost(id: $id) {
id # Needed to remove from cache
author {
id
postCount # May need to decrement
}
}
}
```
### Return Connections for List Updates
```graphql
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
createdAt
author {
id
posts(first: 1) {
edges {
node {
id
}
}
totalCount
}
}
}
}
```
## Error Handling
### Query Result Unions
When schema uses union types for errors:
```graphql
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
... on CreateUserSuccess {
user {
id
email
}
}
... on ValidationError {
message
field
}
... on EmailAlreadyExists {
message
existingUserId
}
}
}
```
### Handle All Cases
```typescript
const result = await client.mutate({
mutation: CREATE_USER,
variables: { input },
});
const { createUser } = result.data;
switch (createUser.__typename) {
case "CreateUserSuccess":
// Handle success
return createUser.user;
case "ValidationError":
// Handle validation error
throw new ValidationError(createUser.field, createUser.message);
case "EmailAlreadyExists":
// Handle specific business error
throw new EmailExistsError(createUser.existingUserId);
}
```
### GraphQL Errors
Handle network and GraphQL errors:
```typescript
try {
const result = await client.mutate({
mutation: CREATE_POST,
variables: { input },
});
return result.data.createPost;
} catch (error) {
if (error.graphQLErrors?.length) {
// Handle GraphQL errors
const gqlError = error.graphQLErrors[0];
if (gqlError.extensions?.code === "UNAUTHENTICATED") {
// Redirect to login
}
}
if (error.networkError) {
// Handle network error
}
throw error;
}
```
## Optimistic Updates
### Select Fields for Optimistic Response
Include all fields that will display immediately:
```graphql
mutation LikePost($postId: ID!) {
likePost(postId: $postId) {
id
likeCount
isLikedByViewer
}
}
```
```typescript
client.mutate({
mutation: LIKE_POST,
variables: { postId: "post_123" },
optimisticResponse: {
likePost: {
__typename: "Post",
id: "post_123",
likeCount: currentCount + 1,
isLikedByViewer: true,
},
},
});
```
### Include Created IDs
For create mutations, use temporary IDs:
```graphql
mutation AddComment($input: AddCommentInput!) {
addComment(input: $input) {
id
body
createdAt
author {
id
name
avatarUrl
}
}
}
```
```typescript
client.mutate({
mutation: ADD_COMMENT,
variables: { input: { postId, body } },
optimisticResponse: {
addComment: {
__typename: "Comment",
id: `temp-${Date.now()}`, // Temporary ID
body,
createdAt: new Date().toISOString(),
author: {
__typename: "User",
id: currentUser.id,
name: currentUser.name,
avatarUrl: currentUser.avatarUrl,
},
},
},
});
```
## Mutation Naming
### Naming Conventions
| Operation | Pattern | Examples |
| ------------ | -------------------- | ------------------------------- |
| Create | `Create{Type}` | `CreateUser`, `CreatePost` |
| Update | `Update{Type}` | `UpdateUser`, `UpdatePost` |
| Delete | `Delete{Type}` | `DeleteUser`, `DeletePost` |
| Action | `{Verb}{Type}` | `PublishPost`, `ArchiveProject` |
| Relationship | `{Add/Remove}{Type}` | `AddTeamMember`, `RemoveTag` |
### Good Names
```graphql
mutation CreateUser($input: CreateUserInput!) { ... }
mutation UpdateUserProfile($userId: ID!, $input: ProfileInput!) { ... }
mutation DeletePost($id: ID!) { ... }
mutation PublishArticle($id: ID!) { ... }
mutation ArchiveProject($id: ID!) { ... }
mutation AddItemToCart($input: AddItemInput!) { ... }
mutation RemoveTeamMember($teamId: ID!, $userId: ID!) { ... }
mutation FollowUser($userId: ID!) { ... }
mutation MarkNotificationAsRead($id: ID!) { ... }
```
### Operation Name Matches Server
Match client operation name to server mutation:
```graphql
# Server schema
type Mutation {
createPost(input: CreatePostInput!): Post!
}
# Client operation - name reflects the action
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
}
}
```
### Context-Specific Names
Add context when same mutation is used differently:
```graphql
# For creating a draft
mutation CreateDraftPost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
status
}
}
# For creating and publishing immediately
mutation CreateAndPublishPost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
status
publishedAt
}
}
```
```
### references/fragments.md
```markdown
# Fragment Patterns
This reference covers patterns for organizing and using GraphQL fragments effectively.
## Table of Contents
- [Fragment Basics](#fragment-basics)
- [Fragment Colocation](#fragment-colocation)
- [Fragment Reuse](#fragment-reuse)
- [Inline Fragments](#inline-fragments)
- [Type Conditions](#type-conditions)
- [Fragment Composition](#fragment-composition)
- [Anti-Patterns](#anti-patterns)
## Fragment Basics
### Defining Fragments
```graphql
fragment UserBasicInfo on User {
id
name
avatarUrl
}
```
### Using Fragments
```graphql
query GetUser($id: ID!) {
user(id: $id) {
...UserBasicInfo
email
}
}
fragment UserBasicInfo on User {
id
name
avatarUrl
}
```
### Fragment Spread
The `...` operator spreads fragment fields:
```graphql
query GetPost($id: ID!) {
post(id: $id) {
id
title
author {
...UserBasicInfo # Spreads id, name, avatarUrl
}
}
}
```
## Fragment Colocation
### Colocate with Components
Keep fragments next to the components that use them:
```
src/
components/
UserAvatar/
UserAvatar.tsx
UserAvatar.fragment.graphql
UserCard/
UserCard.tsx
UserCard.fragment.graphql
PostList/
PostList.tsx
PostList.query.graphql
```
### Component Owns Its Data
```tsx
// UserAvatar.tsx
import { gql } from "@apollo/client";
export const USER_AVATAR_FRAGMENT = gql`
fragment UserAvatar on User {
id
name
avatarUrl
}
`;
interface UserAvatarProps {
user: UserAvatarFragment;
}
export function UserAvatar({ user }: UserAvatarProps) {
return <img src={user.avatarUrl} alt={user.name} className="avatar" />;
}
```
### Parent Composes Fragments
```tsx
// UserCard.tsx
import { gql } from "@apollo/client";
import { USER_AVATAR_FRAGMENT, UserAvatar } from "./UserAvatar";
export const USER_CARD_FRAGMENT = gql`
fragment UserCard on User {
id
name
bio
...UserAvatar
}
${USER_AVATAR_FRAGMENT}
`;
export function UserCard({ user }: { user: UserCardFragment }) {
return (
<div className="user-card">
<UserAvatar user={user} />
<h3>{user.name}</h3>
<p>{user.bio}</p>
</div>
);
}
```
### Query Uses Component Fragments
```tsx
// UserProfile.tsx
import { gql, useQuery } from "@apollo/client";
import { USER_CARD_FRAGMENT, UserCard } from "./UserCard";
const GET_USER = gql`
query GetUserProfile($id: ID!) {
user(id: $id) {
...UserCard
email
createdAt
}
}
${USER_CARD_FRAGMENT}
`;
export function UserProfile({ userId }: { userId: string }) {
const { data } = useQuery(GET_USER, { variables: { id: userId } });
if (!data) return null;
return (
<div>
<UserCard user={data.user} />
<p>Email: {data.user.email}</p>
</div>
);
}
```
## Fragment Reuse
### Shared Fragments
For common patterns used across many components:
```graphql
# fragments/common.graphql
fragment Timestamps on Node {
createdAt
updatedAt
}
fragment PageInfoFields on PageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
```
### Domain-Specific Fragments
```graphql
# fragments/user.graphql
fragment UserSummary on User {
id
name
avatarUrl
}
fragment UserProfile on User {
...UserSummary
bio
location
website
socialLinks {
platform
url
}
}
fragment UserWithStats on User {
...UserSummary
followerCount
followingCount
postCount
}
```
### Using Shared Fragments
```graphql
query GetPost($id: ID!) {
post(id: $id) {
id
title
...Timestamps
author {
...UserSummary
}
}
}
```
## Inline Fragments
### Anonymous Inline Fragments
For grouping fields with directives:
```graphql
query GetUser($id: ID!, $includeDetails: Boolean!) {
user(id: $id) {
id
name
... @include(if: $includeDetails) {
email
bio
website
}
}
}
```
### Inline Fragments on Interfaces
```graphql
query GetNodes($ids: [ID!]!) {
nodes(ids: $ids) {
id
... on User {
name
email
}
... on Post {
title
content
}
}
}
```
## Type Conditions
### Fragments on Union Types
```graphql
query Search($query: String!) {
search(query: $query) {
... on User {
id
name
avatarUrl
}
... on Post {
id
title
excerpt
}
... on Comment {
id
body
post {
id
title
}
}
}
}
```
### Named Fragments for Unions
```graphql
query Search($query: String!) {
search(query: $query) {
...SearchResultUser
...SearchResultPost
...SearchResultComment
}
}
fragment SearchResultUser on User {
id
name
avatarUrl
}
fragment SearchResultPost on Post {
id
title
excerpt
author {
name
}
}
fragment SearchResultComment on Comment {
id
body
post {
id
title
}
}
```
### Handling \_\_typename
```typescript
function SearchResult({ result }) {
switch (result.__typename) {
case 'User':
return <UserResult user={result} />;
case 'Post':
return <PostResult post={result} />;
case 'Comment':
return <CommentResult comment={result} />;
}
}
```
## Fragment Composition
### Building Up Fragments
```graphql
# Base fragment
fragment PostCore on Post {
id
title
slug
}
# Extended fragment
fragment PostPreview on Post {
...PostCore
excerpt
featuredImage {
url
}
}
# Full fragment
fragment PostFull on Post {
...PostPreview
content
publishedAt
author {
...UserSummary
}
tags {
id
name
}
}
```
### Fragments in Fragments
```graphql
fragment CommentWithAuthor on Comment {
id
body
createdAt
author {
...UserSummary
}
}
fragment PostWithComments on Post {
id
title
comments(first: 10) {
edges {
node {
...CommentWithAuthor
}
}
}
}
```
### Fragment Spread Order
Order doesn't matter - fields are merged:
```graphql
query GetUser($id: ID!) {
user(id: $id) {
...UserProfile
...UserStats
# Both fragments' fields are included
}
}
```
## Anti-Patterns
### Avoid Giant Fragments
```graphql
# Bad: Too many fields, not all needed everywhere
fragment UserEverything on User {
id
name
email
bio
avatarUrl
coverImage
website
location
socialLinks { ... }
posts { ... }
followers { ... }
following { ... }
# ... 50 more fields
}
# Good: Focused fragments for specific uses
fragment UserAvatar on User {
id
name
avatarUrl
}
fragment UserProfile on User {
id
name
bio
avatarUrl
website
location
}
```
### Avoid Unused Fragment Fields
```graphql
# Bad: Component only uses name and avatarUrl
fragment UserInfo on User {
id
name
email # unused
avatarUrl
bio # unused
website # unused
}
# Good: Only request what's needed
fragment UserInfo on User {
id
name
avatarUrl
}
```
### Avoid Deeply Nested Fragments
```graphql
# Bad: Hard to understand what's being fetched
fragment Level1 on User {
...Level2
}
fragment Level2 on User {
...Level3
}
fragment Level3 on User {
...Level4
}
# ... continues
# Good: Keep nesting shallow
fragment UserWithPosts on User {
id
name
posts {
...PostPreview
}
}
```
### Avoid Circular Fragment Dependencies
```graphql
# Bad: Circular reference (won't work)
fragment UserWithPosts on User {
posts {
...PostWithAuthor
}
}
fragment PostWithAuthor on Post {
author {
...UserWithPosts # Circular!
}
}
# Good: Break the cycle
fragment UserWithPosts on User {
posts {
...PostPreview
}
}
fragment PostWithAuthor on Post {
author {
...UserSummary # Different fragment, no cycle
}
}
```
```
### references/variables.md
```markdown
# Variable Patterns
This reference covers patterns for using variables in GraphQL operations.
## Table of Contents
- [Variable Basics](#variable-basics)
- [Variable Types](#variable-types)
- [Default Values](#default-values)
- [Complex Inputs](#complex-inputs)
- [Best Practices](#best-practices)
## Variable Basics
### Declaring Variables
Variables are declared in the operation definition:
```graphql
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
```
### Using Variables
Variables are referenced with `$` prefix:
```graphql
query GetUser($id: ID!) {
user(id: $id) {
# $id used here
id
name
}
}
```
### Passing Variables
Variables are passed as a separate JSON object:
```typescript
const { data } = await client.query({
query: GET_USER,
variables: {
id: "user_123",
},
});
```
### Multiple Variables
```graphql
query SearchPosts($query: String!, $status: PostStatus, $first: Int!, $after: String) {
searchPosts(query: $query, status: $status, first: $first, after: $after) {
edges {
node {
id
title
}
}
}
}
```
```json
{
"query": "graphql",
"status": "PUBLISHED",
"first": 10,
"after": "cursor_abc"
}
```
## Variable Types
### Scalar Types
```graphql
query Example(
$id: ID!
$name: String!
$count: Int!
$price: Float!
$active: Boolean!
) {
# ...
}
```
### Custom Scalar Types
```graphql
query Example(
$date: DateTime!
$email: Email!
$url: URL!
) {
# ...
}
```
### Enum Types
```graphql
query GetPosts($status: PostStatus!) {
posts(status: $status) {
id
title
}
}
```
```json
{
"status": "PUBLISHED"
}
```
### List Types
```graphql
query GetUsers($ids: [ID!]!) {
users(ids: $ids) {
id
name
}
}
```
```json
{
"ids": ["user_1", "user_2", "user_3"]
}
```
### Input Object Types
```graphql
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
}
}
```
```json
{
"input": {
"title": "My Post",
"content": "Post content...",
"tags": ["graphql", "api"]
}
}
```
### Required vs Optional
```graphql
query Example(
$required: String! # Must be provided, cannot be null
$optional: String # Can be omitted or null
$requiredList: [String!]! # List required, items required
$optionalList: [String] # List optional, items optional
) {
# ...
}
```
## Default Values
### Simple Defaults
```graphql
query GetPosts($first: Int = 10, $status: PostStatus = PUBLISHED) {
posts(first: $first, status: $status) {
id
title
}
}
```
If not provided, uses defaults:
```json
{}
// Equivalent to: { "first": 10, "status": "PUBLISHED" }
```
Override defaults:
```json
{
"first": 20
}
// Uses first: 20, status: PUBLISHED (default)
```
### Defaults with Optional Variables
```graphql
# Variable is optional (no !) but has default
query GetPosts($first: Int = 10) {
posts(first: $first) {
id
}
}
```
### Defaults for Complex Types
```graphql
query GetPosts($orderBy: PostOrderInput = { field: CREATED_AT, direction: DESC }) {
posts(orderBy: $orderBy) {
id
title
}
}
```
### When to Use Defaults
Use defaults for:
- Pagination limits (`first: Int = 20`)
- Sort order (`direction: SortDirection = DESC`)
- Common filter values (`status: Status = ACTIVE`)
- Feature flags (`includeArchived: Boolean = false`)
## Complex Inputs
### Nested Input Objects
```graphql
mutation CreateOrder($input: CreateOrderInput!) {
createOrder(input: $input) {
id
total
}
}
```
```json
{
"input": {
"customer": {
"email": "[email protected]",
"name": "John Doe"
},
"items": [
{ "productId": "prod_1", "quantity": 2 },
{ "productId": "prod_2", "quantity": 1 }
],
"shippingAddress": {
"street": "123 Main St",
"city": "New York",
"state": "NY",
"zipCode": "10001",
"country": "US"
}
}
}
```
### Lists of Input Objects
```graphql
mutation BulkCreateUsers($inputs: [CreateUserInput!]!) {
bulkCreateUsers(inputs: $inputs) {
id
email
}
}
```
```json
{
"inputs": [
{ "email": "[email protected]", "name": "User 1" },
{ "email": "[email protected]", "name": "User 2" },
{ "email": "[email protected]", "name": "User 3" }
]
}
```
### Filter Inputs
```graphql
query SearchProducts($filter: ProductFilter!) {
products(filter: $filter) {
id
name
price
}
}
```
```json
{
"filter": {
"category": "ELECTRONICS",
"priceRange": {
"min": 100,
"max": 500
},
"inStock": true,
"tags": ["featured", "sale"]
}
}
```
## Best Practices
### Always Use Variables for Dynamic Values
```graphql
# Good: Uses variable
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
# Bad: Hardcoded value
query GetUser {
user(id: "123") {
id
name
}
}
```
### Match Variable Names to Arguments
```graphql
# Good: Clear relationship
query GetUser($userId: ID!) {
user(id: $userId) {
id
}
}
# Also good: Same name
query GetUser($id: ID!) {
user(id: $id) {
id
}
}
# Bad: Confusing names
query GetUser($x: ID!) {
user(id: $x) {
id
}
}
```
### Use Descriptive Variable Names
```graphql
# Good
query SearchPosts(
$searchQuery: String!
$authorId: ID
$publishedAfter: DateTime
$maxResults: Int = 20
) {
searchPosts(
query: $searchQuery
author: $authorId
after: $publishedAfter
first: $maxResults
) {
# ...
}
}
# Bad
query SearchPosts($q: String!, $a: ID, $d: DateTime, $n: Int) {
# ...
}
```
### Group Related Variables
```typescript
// Good: Variables object mirrors input structure
const variables = {
input: {
title: formData.title,
content: formData.content,
tags: formData.tags,
},
};
// Less clear: Flat variables
const variables = {
title: formData.title,
content: formData.content,
tags: formData.tags,
};
```
### Validate Variables Client-Side
```typescript
function createPost(input: CreatePostInput) {
// Validate before sending
if (!input.title?.trim()) {
throw new Error("Title is required");
}
if (input.title.length > 200) {
throw new Error("Title too long");
}
return client.mutate({
mutation: CREATE_POST,
variables: { input },
});
}
```
### Type Variables with TypeScript
```typescript
// Generated types from schema
interface GetUserQueryVariables {
id: string;
}
// Use with Apollo Client
const { data } = useQuery<GetUserQuery, GetUserQueryVariables>(GET_USER, {
variables: { id: userId }, // Type-checked
});
```
```
### references/tooling.md
```markdown
# Tooling
This reference covers tools for working with GraphQL operations, including code generation and linting.
## Table of Contents
- [GraphQL Code Generator](#graphql-code-generator)
- [ESLint GraphQL](#eslint-graphql)
- [IDE Extensions](#ide-extensions)
- [Operation Validation](#operation-validation)
## GraphQL Code Generator
### Overview
GraphQL Code Generator generates TypeScript types from your schema and operations, ensuring type safety throughout your application.
### Installation
```bash
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typed-document-node
```
### Basic Configuration
Create `codegen.ts`:
```typescript
// codegen.ts
import { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
overwrite: true,
schema: "<URL_OF_YOUR_GRAPHQL_API>",
// This assumes that all your source files are in a top-level `src/` directory - you might need to adjust this to your file structure
documents: ["src/**/*.{ts,tsx}"],
// Don't exit with non-zero status when there are no documents
ignoreNoDocuments: true,
generates: {
// Use a path that works the best for the structure of your application
"./src/types/__generated__/graphql.ts": {
plugins: ["typescript", "typescript-operations", "typed-document-node"],
config: {
avoidOptionals: {
// Use `null` for nullable fields instead of optionals
field: true,
// Allow nullable input fields to remain unspecified
inputValue: false,
},
// Use `unknown` instead of `any` for unconfigured scalars
defaultScalarType: "unknown",
// Apollo Client always includes `__typename` fields
nonOptionalTypename: true,
// Apollo Client doesn't add the `__typename` field to root types so
// don't generate a type for the `__typename` for root operation types.
skipTypeNameForRoot: true,
},
},
},
};
export default config;
```
### Run Generation
```bash
# One-time generation
npx graphql-codegen
# Watch mode for development
npx graphql-codegen --watch
```
### Package Scripts
```json
{
"scripts": {
"codegen": "graphql-codegen",
"codegen:watch": "graphql-codegen --watch"
}
}
```
### Generated Types Usage
```tsx
// Before: Manual typing
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
`;
// Manually typed
interface GetUserData {
user: {
id: string;
name: string;
} | null;
}
const { data } = useQuery<GetUserData>(GET_USER, { variables: { id } });
// After: Generated types
import { useGetUserQuery } from "./generated/graphql";
const { data } = useGetUserQuery({ variables: { id } });
// data.user is fully typed!
```
### Near-Operation File Generation
Generate types next to operations:
```typescript
const config: CodegenConfig = {
schema: "http://localhost:4000/graphql",
documents: ["src/**/*.graphql"],
generates: {
"src/": {
preset: "near-operation-file",
presetConfig: {
extension: ".generated.ts",
baseTypesPath: "generated/graphql.ts",
},
plugins: ["typescript-operations", "typescript-react-apollo"],
},
"src/generated/graphql.ts": {
plugins: ["typescript"],
},
},
};
```
Results in:
```
src/
components/
UserCard/
UserCard.graphql
UserCard.generated.ts # Generated types for this file
```
### Fragment Types
```tsx
// UserAvatar.graphql
// fragment UserAvatar on User {
// id
// name
// avatarUrl
// }
import { UserAvatarFragment } from "./UserAvatar.generated";
interface UserAvatarProps {
user: UserAvatarFragment;
}
export function UserAvatar({ user }: UserAvatarProps) {
return <img src={user.avatarUrl} alt={user.name} />;
}
```
## ESLint GraphQL
### Installation
```bash
npm install -D @graphql-eslint/eslint-plugin
```
### Configuration
```javascript
// eslint.config.js (flat config)
import graphqlPlugin from "@graphql-eslint/eslint-plugin";
export default [
{
files: ["**/*.graphql"],
languageOptions: {
parser: graphqlPlugin.parser,
},
plugins: {
"@graphql-eslint": graphqlPlugin,
},
rules: {
"@graphql-eslint/known-type-names": "error",
"@graphql-eslint/no-anonymous-operations": "error",
"@graphql-eslint/no-duplicate-fields": "error",
"@graphql-eslint/naming-convention": [
"error",
{
OperationDefinition: {
style: "PascalCase",
forbiddenPrefixes: ["Query", "Mutation", "Subscription"],
},
FragmentDefinition: {
style: "PascalCase",
},
},
],
},
},
];
```
### Recommended Rules
```javascript
rules: {
// Syntax and validity
'@graphql-eslint/known-type-names': 'error',
'@graphql-eslint/known-fragment-names': 'error',
'@graphql-eslint/no-undefined-variables': 'error',
'@graphql-eslint/no-unused-variables': 'error',
'@graphql-eslint/no-unused-fragments': 'error',
'@graphql-eslint/unique-operation-name': 'error',
'@graphql-eslint/unique-fragment-name': 'error',
// Best practices
'@graphql-eslint/no-anonymous-operations': 'error',
'@graphql-eslint/no-duplicate-fields': 'error',
'@graphql-eslint/require-id-when-available': 'warn',
// Naming
'@graphql-eslint/naming-convention': ['error', { ... }],
}
```
### Schema-Aware Rules
Provide schema for advanced validation:
```javascript
{
files: ['**/*.graphql'],
languageOptions: {
parser: graphqlPlugin.parser,
parserOptions: {
schema: './schema.graphql',
// or
schema: 'http://localhost:4000/graphql',
},
},
}
```
## IDE Extensions
### VS Code
**GraphQL: Language Feature Support** (GraphQL Foundation)
- Syntax highlighting
- Autocomplete for schema types
- Go to definition
- Hover documentation
- Validation against schema
Configuration (`.graphqlrc.yml`):
```yaml
schema: "http://localhost:4000/graphql"
documents: "src/**/*.{graphql,ts,tsx}"
```
**Apollo GraphQL** (Apollo)
- Apollo-specific features
- Schema registry integration
- Performance insights
### JetBrains IDEs
**GraphQL** plugin:
- Syntax highlighting
- Schema-aware completion
- Validation
- Navigate to definition
Configuration (`.graphqlconfig`):
```json
{
"schemaPath": "./schema.graphql",
"includes": ["src/**/*.graphql"]
}
```
### Configuration Files
Common configuration file names:
- `.graphqlrc` (JSON)
- `.graphqlrc.yml` (YAML)
- `.graphqlrc.json` (JSON)
- `graphql.config.js` (JavaScript)
```yaml
# .graphqlrc.yml
schema: "http://localhost:4000/graphql"
documents: "src/**/*.graphql"
extensions:
codegen:
generates:
./src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
```
## Operation Validation
### Validate Against Schema
```bash
# Using graphql-inspector
npx graphql-inspector validate ./src/**/*.graphql ./schema.graphql
```
### CI Integration
```yaml
# .github/workflows/graphql.yml
name: GraphQL Validation
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Download schema
run: npx graphql-inspector introspect http://localhost:4000/graphql --write schema.graphql
- name: Validate operations
run: npx graphql-inspector validate './src/**/*.graphql' schema.graphql
- name: Check for breaking changes
run: npx graphql-inspector diff schema.graphql http://localhost:4000/graphql
```
### Pre-commit Hook
```json
// package.json
{
"lint-staged": {
"*.graphql": ["eslint --fix", "graphql-inspector validate ./schema.graphql"]
}
}
```
### Operation Complexity Check
```bash
# Check query complexity
npx graphql-query-complexity-checker \
--schema ./schema.graphql \
--query ./src/queries/GetUser.graphql \
--max-complexity 100
```
### Persisted Queries Extraction
Generate persisted queries for production:
```typescript
// codegen.ts
const config: CodegenConfig = {
generates: {
"./persisted-queries.json": {
plugins: ["graphql-codegen-persisted-query-ids"],
config: {
output: "client",
algorithm: "sha256",
},
},
},
};
```
Output:
```json
{
"abc123...": "query GetUser($id: ID!) { user(id: $id) { id name } }",
"def456...": "mutation CreatePost($input: CreatePostInput!) { ... }"
}
```
```