HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
5 Topics
Chapter•145 Minutes

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:

  • createOrder
  • cancelOrder
  • publishArticle
  • resetPassword
  • addProductToCart

Good mutation names make an API easier to understand and monitor.

Create User Mutation
mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
    email
  }
}
💡 The mutation accepts a strongly typed input object and returns the newly created user. The client controls which fields of the returned user it needs.
Key Takeaways
  • 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.
Knowledge Check

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 name may mean "leave the name unchanged".
  • name: null may 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.

Input Design Decisions
Structured input: Prefer input objects for complex operations
Groups related values into a named, reusable contract.
Omitted field: Often means no update
Application logic can interpret absence as keeping the existing value.
Explicit null: May mean clear the value
Only valid when the input field is nullable and the application defines this semantic.
Key Takeaways
  • 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.
Knowledge Check

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:

  • email is supplied as a string.
  • age is supplied as an integer.
  • password is 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.

Validation Layers
Key Takeaways
  • 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.
Knowledge Check

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.

Mutation Response Patterns
Direct resource: createPost → Post
Simple operations can return the created or updated resource directly.
Payload object: createPost → CreatePostPayload
Useful when the operation needs structured metadata, warnings, errors, or related objects.
Structured user errors: field + message + code
Can help clients map expected validation failures to UI fields.
Key Takeaways
  • 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.
Knowledge Check

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.

Retries can duplicate side effects

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.

Key Takeaways
  • 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.
Knowledge Check

Why can an idempotency key be important for a payment mutation?