GraphQL Mutations
Expert guidance for designing effective GraphQL mutations.
Quick Reference
| Pattern |
Use When |
Structure |
| Result payload |
All mutations |
mutationName(input): MutationNamePayload! |
| Field-specific errors |
Validation failures |
errors: [FieldError!]! in payload |
| Input objects |
Complex arguments |
input: MutationNameInput! |
| Noun + Verb naming |
State changes |
createUser, deletePost, closeCard |
| Idempotent mutations |
Safe retries |
Design for repeatable calls |
| Optimistic UI |
Client-side updates |
Return predicted result |
What Do You Need?
- Payload design - Return types, error handling
- Input objects - Structuring mutation arguments
- Error patterns - Field-specific vs top-level errors
- Naming - Mutation naming conventions
- Side effects - Handling async operations
Specify a number or describe your mutation scenario.
Routing
| Response |
Reference to Read |
| 1, "payload", "return", "response" |
payloads.md |
| 2, "input", "argument", "parameter" |
inputs.md |
| 3, "error", "validation", "field error" |
errors.md |
| 4, "naming", "convention" |
naming.md |
| 5, general mutations |
Read relevant references |
Critical Rules
- Always return a payload: Never just a boolean or the object
- Use input objects for complex arguments: Don't use many scalars
- Field-specific errors in response: Let clients handle per-field failures
- Noun + verb naming: createUser, deleteUser, not user
- Mutations are POST-only: Never use GET for mutations
- Design for idempotency: Safe to call multiple times
Mutation Template
# Input object for complex arguments
input CreateUserInput {
name: String!
email: String!
password: String!
}
# Payload with result and errors
type CreateUserPayload {
user: User
errors: [UserError!]!
}
# Field-specific error type
type UserError {
field: [String!]! # Path to field: ["email"] or ["user", "emails", 0]
message: String!
}
# Mutation definition
type Mutation {
"""
Creates a new user account
"""
createUser(input: CreateUserInput!): CreateUserPayload!
}
Mutation Implementation
// Good: Mutation with proper payload and field errors
func (r *mutationResolver) CreateUser(ctx context.Context, input CreateUserInput) (*CreateUserPayload, error) {
// Validate
var errs []UserError
if input.Name == "" {
errs = append(errs, UserError{
Field: []string{"name"},
Message: "Name is required",
})
}
if !isValidEmail(input.Email) {
errs = append(errs, UserError{
Field: []string{"email"},
Message: "Invalid email format",
})
}
if len(errs) > 0 {
return &CreateUserPayload{Errors: errs}, nil
}
// Create
user, err := r.db.CreateUser(input)
if err != nil {
if errors.Is(err, db.ErrDuplicate) {
return &CreateUserPayload{
Errors: []UserError{{
Field: []string{"email"},
Message: "Email already exists",
}},
}, nil
}
return nil, fmt.Errorf("failed to create user")
}
return &CreateUserPayload{User: user, Errors: []UserError{}}, nil
}
Common Mutation Patterns
Create
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
}
type CreateUserPayload {
user: User
errors: [UserError!]!
}
Update
type Mutation {
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
}
type UpdateUserPayload {
user: User
errors: [UserError!]!
}
Delete
type Mutation {
deleteUser(id: ID!): DeleteUserPayload!
}
type DeleteUserPayload {
deletedUserId: ID
errors: [UserError!]!
}
State Change (Noun + Verb)
type Mutation {
"""
Closes a card (marks as closed, not deleted)
"""
closeCard(id: ID!): CloseCardPayload!
}
type CloseCardPayload {
card: Card
errors: [UserError!]!
}
Error Handling Patterns
| Error Type |
Response Pattern |
| Validation errors |
Return in payload errors field |
| Duplicate unique key |
Return in payload errors field |
| Not found |
Return in payload errors field |
| Permission denied |
Return in payload errors field |
| Internal server error |
Return nil, wrap error (don't expose) |
HTTP Semantics
| Concern |
Guidance |
| HTTP method |
Always POST for mutations |
| Caching |
Mutations are never cached |
| Idempotency |
Design mutations to be safely repeatable |
| Side effects |
Document non-obvious side effects |
| Async operations |
Return payload with job ID, query for status |
Common Mutation Mistakes
| Mistake |
Severity |
Fix |
| Returning just boolean |
Medium |
Use payload with result |
| No field-specific errors |
High |
Add errors array to payload |
| Too many scalar arguments |
Medium |
Use input object |
| Verb + noun naming |
Low |
Use noun + verb (createUser) |
| Using GET for mutations |
Critical |
Always use POST |
| No validation errors in payload |
High |
Return validation failures |
Reference Index
| File |
Topics |
| payloads.md |
Result types, error patterns, response structure |
| inputs.md |
Input objects, nested inputs, validation |
| errors.md |
Field errors, error types, client handling |
| naming.md |
Conventions, verb selection, consistency |
Success Criteria
Mutations are well-designed when:
- All mutations return a payload type
- Field-specific errors returned in payload
- Input objects used for complex arguments
- Noun + verb naming (createUser, deletePost)
- POST only (never GET)
- Idempotent where possible
- Validation errors returned, not thrown
- No internal errors exposed to clients
1---2name: graphql-mutations3description: GraphQL mutation design including payload patterns, field-specific errors, input objects, and HTTP semantics. Use when designing or implementing GraphQL mutations.4---5
6# GraphQL Mutations
7
8Expert guidance for designing effective GraphQL mutations.
9
10## Quick Reference
11
12| Pattern | Use When | Structure |
13|---------|----------|-----------|
14| Result payload | All mutations | `mutationName(input): MutationNamePayload!` |
15| Field-specific errors | Validation failures | `errors: [FieldError!]!` in payload |
16| Input objects | Complex arguments | `input: MutationNameInput!` |
17| Noun + Verb naming | State changes | `createUser`, `deletePost`, `closeCard` |
18| Idempotent mutations | Safe retries | Design for repeatable calls |
19| Optimistic UI | Client-side updates | Return predicted result |
20
21## What Do You Need?
22
231. **Payload design** - Return types, error handling
242. **Input objects** - Structuring mutation arguments
253. **Error patterns** - Field-specific vs top-level errors
264. **Naming** - Mutation naming conventions
275. **Side effects** - Handling async operations
28
29Specify a number or describe your mutation scenario.
30
31## Routing
32
33| Response | Reference to Read |
34|----------|-------------------|
35| 1, "payload", "return", "response" | [payloads.md](./references/payloads.md) |
36| 2, "input", "argument", "parameter" | [inputs.md](./references/inputs.md) |
37| 3, "error", "validation", "field error" | [errors.md](./references/errors.md) |
38| 4, "naming", "convention" | [naming.md](./references/naming.md) |
39| 5, general mutations | Read relevant references |
40
41## Critical Rules
42
43- **Always return a payload**: Never just a boolean or the object
44- **Use input objects for complex arguments**: Don't use many scalars
45- **Field-specific errors in response**: Let clients handle per-field failures
46- **Noun + verb naming**: createUser, deleteUser, not user
47- **Mutations are POST-only**: Never use GET for mutations
48- **Design for idempotency**: Safe to call multiple times
49
50## Mutation Template
51
52```graphql
53# Input object for complex arguments
54input CreateUserInput {
55 name: String!
56 email: String!
57 password: String!
58}
59
60# Payload with result and errors
61type CreateUserPayload {
62 user: User
63 errors: [UserError!]!
64}
65
66# Field-specific error type
67type UserError {
68 field: [String!]! # Path to field: ["email"] or ["user", "emails", 0]
69 message: String!
70}
71
72# Mutation definition
73type Mutation {
74 """
75 Creates a new user account
76 """
77 createUser(input: CreateUserInput!): CreateUserPayload!
78}
79```
80
81## Mutation Implementation
82
83```go
84// Good: Mutation with proper payload and field errors
85func (r *mutationResolver) CreateUser(ctx context.Context, input CreateUserInput) (*CreateUserPayload, error) {
86 // Validate
87 var errs []UserError
88 if input.Name == "" {
89 errs = append(errs, UserError{
90 Field: []string{"name"},
91 Message: "Name is required",
92 })
93 }
94 if !isValidEmail(input.Email) {
95 errs = append(errs, UserError{
96 Field: []string{"email"},
97 Message: "Invalid email format",
98 })
99 }
100 if len(errs) > 0 {
101 return &CreateUserPayload{Errors: errs}, nil
102 }
103
104 // Create
105 user, err := r.db.CreateUser(input)
106 if err != nil {
107 if errors.Is(err, db.ErrDuplicate) {
108 return &CreateUserPayload{
109 Errors: []UserError{{
110 Field: []string{"email"},
111 Message: "Email already exists",
112 }},
113 }, nil
114 }
115 return nil, fmt.Errorf("failed to create user")
116 }
117
118 return &CreateUserPayload{User: user, Errors: []UserError{}}, nil
119}
120```
121
122## Common Mutation Patterns
123
124### Create
125```graphql
126type Mutation {
127 createUser(input: CreateUserInput!): CreateUserPayload!
128}
129
130type CreateUserPayload {
131 user: User
132 errors: [UserError!]!
133}
134```
135
136### Update
137```graphql
138type Mutation {
139 updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
140}
141
142type UpdateUserPayload {
143 user: User
144 errors: [UserError!]!
145}
146```
147
148### Delete
149```graphql
150type Mutation {
151 deleteUser(id: ID!): DeleteUserPayload!
152}
153
154type DeleteUserPayload {
155 deletedUserId: ID
156 errors: [UserError!]!
157}
158```
159
160### State Change (Noun + Verb)
161```graphql
162type Mutation {
163 """
164 Closes a card (marks as closed, not deleted)
165 """
166 closeCard(id: ID!): CloseCardPayload!
167}
168
169type CloseCardPayload {
170 card: Card
171 errors: [UserError!]!
172}
173```
174
175## Error Handling Patterns
176
177| Error Type | Response Pattern |
178|------------|------------------|
179| Validation errors | Return in payload errors field |
180| Duplicate unique key | Return in payload errors field |
181| Not found | Return in payload errors field |
182| Permission denied | Return in payload errors field |
183| Internal server error | Return nil, wrap error (don't expose) |
184
185## HTTP Semantics
186
187| Concern | Guidance |
188|---------|----------|
189| HTTP method | Always POST for mutations |
190| Caching | Mutations are never cached |
191| Idempotency | Design mutations to be safely repeatable |
192| Side effects | Document non-obvious side effects |
193| Async operations | Return payload with job ID, query for status |
194
195## Common Mutation Mistakes
196
197| Mistake | Severity | Fix |
198|---------|----------|-----|
199| Returning just boolean | Medium | Use payload with result |
200| No field-specific errors | High | Add errors array to payload |
201| Too many scalar arguments | Medium | Use input object |
202| Verb + noun naming | Low | Use noun + verb (createUser) |
203| Using GET for mutations | Critical | Always use POST |
204| No validation errors in payload | High | Return validation failures |
205
206## Reference Index
207
208| File | Topics |
209|------|--------|
210| [payloads.md](./references/payloads.md) | Result types, error patterns, response structure |
211| [inputs.md](./references/inputs.md) | Input objects, nested inputs, validation |
212| [errors.md](./references/errors.md) | Field errors, error types, client handling |
213| [naming.md](./references/naming.md) | Conventions, verb selection, consistency |
214
215## Success Criteria
216
217Mutations are well-designed when:
218- All mutations return a payload type
219- Field-specific errors returned in payload
220- Input objects used for complex arguments
221- Noun + verb naming (createUser, deletePost)
222- POST only (never GET)
223- Idempotent where possible
224- Validation errors returned, not thrown
225- No internal errors exposed to clients