Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
27 Jul 2025 · 6 min read ·Article 27 / 125
Go

27 Writing Mutation Schemas in GraphQL

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

27 Writing Mutation Schemas in GraphQL

Ever since GraphQL became the darling of API architecture, terms like query, mutation, and subscription have grown familiar to developers. While queries are used to read data, mutations are the crucial element for writing or modifying data (insert, update, delete). Writing a good mutation schema is not just about following the syntax, but also about prioritizing robustness, ease of scaling, and maintainability.

This article covers 27 practical tips (best practices) for writing mutation schemas in GraphQL, complete with code examples, simulations, and flow diagrams that you can apply to your codebase today.


1. Understand the Purpose of Mutations

Mutations are used for all write operations on data. By default, the response from a GraphQL mutation is the final state of the data after the change—a unique trait compared to REST.

graphql
1mutation {
2  createUser(input: {name: "Budi", email: "budi@example.com"}) {
3    id
4    name
5    email
6  }
7}

2. Use Input Object Types

It’s better to use input rather than many direct arguments on a mutation, so that schema changes stay more structured and extendable.

graphql
1input CreateUserInput {
2  name: String!
3  email: String!
4}
5
6type Mutation {
7  createUser(input: CreateUserInput!): User
8}

3. Always Return a Payload Object

Try to always have your mutation return a payload object rather than just a single field.

graphql
1type CreateUserPayload {
2  user: User
3  errors: [Error!]
4}
5
6type Mutation {
7  createUser(input: CreateUserInput!): CreateUserPayload
8}

4. Return the Data That Was Changed

Return the resource that actually changed so the frontend can easily perform a cache update without a second query.

5. Support Bulk Operations (Batching)

Provide endpoints for batch create, update, and delete to handle large-scale operations.

graphql
1input BulkDeleteUserInput {
2  ids: [ID!]!
3}
4
5type Mutation {
6  bulkDeleteUser(input: BulkDeleteUserInput!): BulkDeletePayload
7}

6. Semi-Standardize Mutation Names

Use the verbNoun pattern, for example: createUser, updateProfile, deleteComment.

7. Include Error and Success State Information

Example payload:

graphql
 1type RegisterUserPayload {
 2  user: User
 3  success: Boolean!
 4  errors: [Error!]
 5}
 6
 7type Error {
 8  field: String
 9  message: String
10}

8. Implement Enumerations for Specific States

Avoid magic strings. Use enums for statuses, for example UserStatus.

9. Document Your Mutation Schema

Always add docstrings to every input and type. This helps your team and documentation tools.

graphql
1"""
2Registers a new user with an email and name.
3"""
4createUser(input: CreateUserInput!): CreateUserPayload

10. Use the @deprecated Directive

If a new mutation replaces an older one, use @deprecated.

graphql
1deletePostOld(input: DeletePostInput!): DeletePostPayload @deprecated(reason: "Use deletePost instead")

11. Optimize Partial Updates with Nullable Input

For updates, use nullable input so you can do partial updates, similar to PATCH in REST.

graphql
1input UpdateUserInput {
2  id: ID!
3  name: String
4  email: String
5}

12. Atomic Transactions

In the backend resolver, make sure the mutation runs atomically (for example, with a database transaction).

13. Chained Mutations (Relay-style)

Each mutation can return a clientMutationId so the client can track the response and the order of operations.

graphql
 1type Mutation {
 2  updateProfile(input: UpdateProfileInput!): UpdateProfilePayload
 3}
 4input UpdateProfileInput {
 5  clientMutationId: String
 6  ...
 7}
 8type UpdateProfilePayload {
 9  clientMutationId: String
10  ...
11}

14. Conditional Responses

Support fields in the payload to convey specific success or error status.

15. Validate Input in the Resolver

Always perform input validation in the resolver layer, even if there is already GraphQL validation at the schema level.

16. Use-case Driven Mutation Granularity

Don’t make mutations too granular or too coarse. For instance, should you update just one profile field? Or update every field at once? Tailor it to the frontend’s use case.

17. Protected Actions (Authentication and Authorization)

Mutations must be secure; implement guards/middleware as needed.

18. Don’t Use Mutations for Query-only Operations

Mutations should not be used for read-only operations.

19. Expose Fields Selectively

Don’t expose every field; for example, the password hash field should remain hidden.


20. Idempotency

Design mutations to be idempotent, especially for sensitive operations (money transfers, etc.).

21. Consistent Error Mapping

Errors should follow the same response pattern across all mutations.


22. Test Mutations Continuously

Mutation = write = bug-prone. Unit/integration tests are a must.

23. Create a MutationStatus Enum If Needed

A mutation response can have an enum: SUCCESS, FAILED, DUPLICATE, and so on.

24. Notify the Client When There Are Side Effects

For example: if the user needs to verify their email, communicate it in the payload.

25. Simulation: Update User Profile

Let’s run a simulation:

Schema:

graphql
 1input UpdateProfileInput {
 2  id: ID!
 3  name: String
 4  email: String
 5  bio: String
 6}
 7type UpdateProfilePayload {
 8  user: User
 9  errors: [Error!]
10  success: Boolean!
11}
12type Mutation {
13  updateProfile(input: UpdateProfileInput!): UpdateProfilePayload
14}

Resolver (Express.js/TypeScript pseudocode):

typescript
 1const resolvers = {
 2  Mutation: {
 3    updateProfile: async (_, {input}, {db, user}) => {
 4      if (!user) return { success: false, errors: [{message: 'Unauthorized'}] };
 5
 6      const profile = await db.User.findById(input.id);
 7      if (!profile) return { success: false, errors: [{message: 'Not found'}] };
 8
 9      if (input.email && !isValidEmail(input.email)) {
10        return { success: false, errors: [{message: 'Invalid email'}] };
11      }
12
13      Object.assign(profile, input);
14
15      await profile.save();
16
17      return { user: profile, success: true, errors: [] };
18    }
19  }
20}

Query:

graphql
 1mutation {
 2  updateProfile(input: {
 3    id: "12", 
 4    name: "Budi Santoso", 
 5    email: "budi@contoh.com"
 6  }) {
 7    user {
 8      id
 9      name
10      email
11    }
12    success
13    errors {
14      message
15    }
16  }
17}

26. Schema Comparison Table: REST vs GraphQL Mutation

RESTGraphQL
Endpoint/users/{id}mutation {updateUser}
HTTP VerbPATCHCustom via schema
PayloadJSON bodyParam via input object
ResponseDepends on implementationCustom payload, can hold errors & data
ValidationManual, usually via middlewareSchema + custom resolver validation
AtomicityDepends on backendDepends on resolver
Version/DependRequires API versioningBackward-compatible schema & deprecation

27. GraphQL Mutation Flow Diagram

MERMAID
flowchart TD
  A[Client] -->|Send Mutation Payload| B[GraphQL Server]
  B --> C{Validation}
  C -- valid --> D[Trigger Resolver]
  D --> E[DB Transaction]
  E --> F[Updated Data]
  F --> G[Return Payload (User, Errors, Success, etc)]
  C -- invalid --> H[Return Error Payload]

Conclusion

Writing the 27 best mutation schemas in GraphQL is not just about syntax, but about blending functional, security, maintainability, and scalability concerns. Treat the mutations in your API as a clear contract between client <> server, and don’t hesitate to refactor to adapt to new requirements.

Happy exploring with GraphQL mutations—because changing data is what makes applications grow! 🚀


References

Related Articles

💬 Comments