Mutations, Inputs and Validation
Learn how GraphQL handles state-changing operations, structured inputs, validation, mutation design, optimistic client behavior, and reliable error handling.
Understanding GraphQL Mutations
Queries are intended for reading data, while mutations represent operations that modify server-side state.
A schema commonly defines a root Mutation type:
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): DeleteUserPayload!
}
A client can call a mutation using:
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
Mutations are not automatically database writes. The schema defines the operation, while the resolver determines what actually happens.
A mutation might write to MongoDB, PostgreSQL, Redis, another API, or several systems.
Mutation naming
Prefer names that describe the business action rather than vague names such as save or process.
Examples:
createOrdercancelOrderpublishArticleresetPasswordaddProductToCart
Good mutation names make an API easier to understand and monitor.
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}- Mutations represent state-changing operations.
- A mutation does not inherently require a database.
- Mutation names should describe meaningful business actions.
- Mutation responses should expose useful result data.
Which statement about GraphQL mutations is correct?
Designing Mutation Inputs
Good input design is one of the most important parts of a maintainable GraphQL API.
Instead of exposing many arguments:
createProduct(
name: String!
description: String
price: Float!
category: ProductCategory!
): Product!
prefer a structured input for complex operations:
input CreateProductInput {
name: String!
description: String
price: Float!
category: ProductCategory!
}
createProduct(input: CreateProductInput!): Product!
This approach creates a clear boundary around client-provided values.
For updates, consider the difference between fields that are omitted and fields explicitly set to null.
For example:
input UpdateUserInput {
name: String
bio: String
}
Depending on the application's semantics:
- Omitted
namemay mean "leave the name unchanged". name: nullmay mean "clear the name" if the schema permits null.
This distinction is important when designing partial updates.
For more complex systems, input objects can be nested:
input AddressInput {
street: String!
city: String!
postalCode: String!
}
input CreateCustomerInput {
name: String!
address: AddressInput!
}
Nested inputs allow domain concepts to remain explicit rather than flattening every property into a huge argument list.
Prefer input objects for complex operationsOften means no updateMay mean clear the value- Input objects create explicit boundaries for client-provided data.
- Complex mutations are usually easier to evolve with input objects.
- Omitted and null values can have different update semantics.
- Nested input types can model complex domain structures.
Why can distinguishing an omitted input field from an explicit null value matter in an update mutation?
GraphQL Validation and Application Validation
GraphQL provides schema-level validation, but schema validation is not enough for most production applications.
Consider:
input RegisterInput {
email: String!
age: Int!
password: String!
}
GraphQL can validate that:
emailis supplied as a string.ageis supplied as an integer.passwordis supplied as a string.- Required fields are present.
But GraphQL does not automatically know that:
- The email must have a valid email format.
- The user must be at least 18.
- The password must contain a particular combination of characters.
- The email must be unique in the database.
Those are business or application validation rules.
A production resolver or service layer should therefore perform additional validation.
Conceptually:
GraphQL validation
↓
Type-safe input
↓
Application validation
↓
Authorization
↓
Business logic
↓
Persistence
Keeping business validation out of the schema where possible prevents the schema from becoming an inappropriate replacement for the domain layer.
At the same time, the schema should still communicate important constraints through types. For example, if a value is always required, representing it as String! is better than leaving it nullable and relying entirely on runtime validation.
- GraphQL validates structure and types.
- Business rules require application-level validation.
- Authorization is a separate concern from input validation.
- Schema nullability should communicate meaningful API constraints.
Which rule generally requires application-level validation rather than only GraphQL type validation?
Mutation Response Design
Mutation responses should be designed intentionally rather than simply returning an arbitrary object.
A simple mutation can return the changed resource:
createPost(input: CreatePostInput!): Post!
For more complex operations, a payload type can be useful:
type CreatePostPayload {
post: Post
errors: [UserError!]!
success: Boolean!
}
type UserError {
field: String
message: String!
code: String
}
The mutation becomes:
createPost(input: CreatePostInput!): CreatePostPayload!
This design gives the client a structured place for operation-specific metadata.
However, payload types should not become a generic dumping ground for every possible server state. Keep them meaningful to the operation.
Another important design decision is whether expected business failures should be represented as normal structured data, GraphQL errors, or both. A consistent application-wide error strategy is more important than blindly choosing one pattern.
For example, invalid credentials might be an expected authentication failure, while an unexpected database outage is an exceptional server failure. Treating both identically can make client behavior and observability harder.
createPost → PostcreatePost → CreatePostPayloadfield + message + code- Mutation response design should match operation complexity.
- Payload types can provide structured operation results.
- Expected business errors and unexpected system failures should be distinguished.
- A consistent error strategy improves client and server behavior.
When is a mutation payload type particularly useful?
Mutation Side Effects and Idempotency
GraphQL mutations often trigger side effects such as database writes, emails, payment operations, messages, or external API calls.
This means mutation design must consider retries.
Suppose a client sends:
mutation {
createPayment(input: $input) {
id
}
}
The server successfully creates the payment, but the network connection fails before the client receives the response. The client may retry.
If the mutation is not designed for retries, the same logical operation might happen twice.
A common solution is an idempotency key:
input CreatePaymentInput {
amount: Float!
currency: String!
idempotencyKey: String!
}
The server stores or otherwise tracks the key and ensures repeated attempts for the same logical operation do not produce duplicate side effects.
Not every mutation needs strict idempotency. But operations involving payments, orders, provisioning, emails, or external side effects deserve careful retry semantics.
GraphQL itself does not automatically make mutations idempotent. Idempotency is an application and business design concern.
A network failure does not necessarily mean that the server failed to perform the mutation. Production systems must consider the possibility that the operation succeeded but its response was lost.
- GraphQL does not automatically make mutations idempotent.
- Network retries can duplicate side effects.
- Idempotency keys are useful for operations where duplicates are costly.
- Payments and other external side effects require explicit retry design.
Why can an idempotency key be important for a payment mutation?