GraphQL Mutation Input and Response Design
Well-designed mutation inputs and responses make an API easy to use and robust to change. Poorly designed ones force clients to make guesses about what went wrong or what changed. This topic covers the patterns that production GraphQL APIs use.
Always Use an Input Type for Mutations
Passing many individual arguments directly to a mutation works, but it becomes hard to manage when the number of fields grows. Using an input type groups arguments cleanly and makes the schema easier to read and extend.
✗ Loose arguments — hard to extend later:
──────────────────────────────────────────
type Mutation {
registerUser(
name: String!
email: String!
password: String!
phone: String
country: String!
): User!
}
✓ Input type — clean and extensible:
─────────────────────────────────────
input RegisterUserInput {
name: String!
email: String!
password: String!
phone: String
country: String!
}
type Mutation {
registerUser(input: RegisterUserInput!): User!
}
The Payload Pattern for Responses
Instead of returning the raw object, return a payload wrapper type. The payload carries the result, any errors, and extra metadata. This pattern is used by GitHub's GraphQL API and many large APIs.
Without payload: With payload:
──────────────── ─────────────
registerUser: User! registerUser: RegisterUserPayload!
type RegisterUserPayload {
user: User
errors: [UserError!]!
success: Boolean!
}
UserError Type for Business Logic Errors
There are two kinds of errors in GraphQL. System errors (server crashed, network failed) go in the top-level errors array. Business logic errors (email already taken, password too short) should travel inside the payload as structured data, not as system errors.
type UserError {
field: String ← Which field caused the problem
message: String! ← Human-readable explanation
}
type RegisterUserPayload {
user: User
errors: [UserError!]!
}
type Mutation {
registerUser(input: RegisterUserInput!): RegisterUserPayload!
}
Successful mutation response:
──────────────────────────────
{
"data": {
"registerUser": {
"user": { "id": "u9", "name": "Leela" },
"errors": []
}
}
}
Failed mutation (email taken):
───────────────────────────────
{
"data": {
"registerUser": {
"user": null,
"errors": [
{
"field": "email",
"message": "This email address is already registered"
}
]
}
}
}
Why Business Errors Belong in the Payload
System errors Business errors ───────────── ─────────────── Server crash Email already taken Database unavailable Password too short Network timeout Coupon code expired Unhandled exception Insufficient fundsGo in: top-level "errors" Go in: payload.errors Status: HTTP 500 / GraphQL err Status: HTTP 200, data returns Client: catch/error handler Client: reads payload.errors
