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.
Write every operation against the project's schema, not against the examples in this guide.
- Find the schema before you write anything. Check the
schema entry in codegen.ts, graphql.config.*, or apollo.config.*, then look for *.graphql SDL files.
- Use only the fields, arguments, and enum values that the schema defines. Field names in this guide, such as
updatedAt or orderBy: { field: CREATED_AT, direction: DESC }, are illustrations.
- Save the operation as a document next to the client's existing operations. Leave the server schema unchanged unless the user asks for a schema change.
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
createdAt
}
}
subscription OnMessageReceived($channelId: ID!) {
messageReceived(channelId: $channelId) {
id
content
sender {
id
name
}
}
}
| Pattern | Example |
|---|
Query | GetUser, ListPosts, SearchProducts
|
Mutation | CreateUser, UpdatePost, DeleteComment
|
Subscription | OnMessageReceived, OnUserStatusChanged
|
# 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) { ... }
# Define fragment
fragment UserBasicInfo on User {
id
name
avatarUrl
}
# Use fragment
query GetUser($id: ID!) {
user(id: $id) {
...UserBasicInfo
email
}
}
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)
}
}
# 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
}
}
# Good: Named operation
query GetUserPosts($userId: ID!) {
user(id: $userId) {
posts {
id
title
}
}
}
# Avoid: Anonymous operation
query {
user(id: "123") {
posts {
id
title
}
}
}
# Good: Variables
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
# Avoid: Hardcoded values
query {
user(id: "123") {
id
name
}
}
// 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} />;
}
Detailed documentation for specific topics:
- Queries - Query patterns and optimization
- Mutations - Mutation patterns and error handling
- Fragments - Fragment organization and reuse
- Variables - Variable usage and types
- Tooling - Code generation and linting
- ALWAYS read the project's schema first, and use only the fields, arguments, and enum values it defines
- 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
- PREFER fragments for reusable field selections
- PREFER colocating fragments with components
- USE descriptive operation names that reflect purpose
- USE
@include/@skip for conditional fields