27 Writing Mutation Schemas in GraphQL
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.
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.
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.
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.
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:
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.
1"""
2Registers a new user with an email and name.
3"""
4createUser(input: CreateUserInput!): CreateUserPayload10. Use the @deprecated Directive
If a new mutation replaces an older one, use @deprecated.
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.
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.
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:
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):
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:
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
| REST | GraphQL | |
|---|---|---|
| Endpoint | /users/{id} | mutation {updateUser} |
| HTTP Verb | PATCH | Custom via schema |
| Payload | JSON body | Param via input object |
| Response | Depends on implementation | Custom payload, can hold errors & data |
| Validation | Manual, usually via middleware | Schema + custom resolver validation |
| Atomicity | Depends on backend | Depends on resolver |
| Version/Depend | Requires API versioning | Backward-compatible schema & deprecation |
27. GraphQL Mutation Flow Diagram
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