GraphQL Mutations Guide: Create, Update & Delete Data with Input Types
Mutations are GraphQL operations that modify server-side data by creating, updating, or deleting resources with sequential execution and explicit return types.
What You’ll Learn
- Defining mutations in the schema
- Input types for mutation arguments
- Mutation resolvers for create, update, delete
- Error handling patterns
- Compared with REST methods (POST, PUT, DELETE)
Why Mutations Matter
APIs aren’t just for reading data — they need to create, update, and delete too. Mutations follow the same GraphQL principles (request only what you need) but with important differences: they execute sequentially and can return any combination of data. DodaTech’s Durga Antivirus Pro uses mutations for submitting threat reports, updating device configurations, quarantining infected devices, and managing user profiles.
sequenceDiagram
participant Client
participant GraphQL
participant Resolver
participant DB
Client->>GraphQL: mutation CreateDevice {...}
GraphQL->>Resolver: createDevice(input, context)
Resolver->>DB: INSERT INTO devices...
DB-->>Resolver: {id: "dev-007", name: "Office-PC"}
Resolver-->>GraphQL: {id: "dev-007", name: "Office-PC", ...}
GraphQL-->>Client: {data: {createDevice: {...}}}
Defining Mutations
Mutations are defined like queries — as fields on the Mutation type:
type Mutation {
createDevice(input: CreateDeviceInput!): Device!
updateDevice(id: ID!, input: UpdateDeviceInput!): Device!
deleteDevice(id: ID!): DeleteDeviceResponse!
scanDevice(id: ID!): ScanResult!
}Input Types
Input types pass structured data to mutations:
input CreateDeviceInput {
name: String!
os: String!
version: String
userId: ID!
}
input UpdateDeviceInput {
name: String
os: String
version: String
}
input ThreatReportInput {
deviceId: ID!
threatName: String!
severity: Severity!
details: String
}Key difference from REST: In REST, POST/PUT bodies are unstructured JSON. In GraphQL, input types enforce validation at the API level — every field has a type and nullability constraint.
Mutation Resolvers
Create
const resolvers = {
Mutation: {
createDevice: async (parent, { input }, context) => {
// Authorization check
if (!context.user) {
throw new AuthenticationError('Not authenticated');
}
// Validation
if (input.name.length < 2) {
throw new UserInputError('Name must be at least 2 characters');
}
// Business logic
const newDevice = {
id: generateId(),
name: input.name,
os: input.os,
version: input.version || '1.0',
userId: context.user.id,
createdAt: new Date().toISOString(),
};
// Persist
await context.db.devices.insert(newDevice);
// Return — only requested fields are sent to client
return newDevice;
},
},
};Update
updateDevice: async (parent, { id, input }, context) => {
const existing = await context.db.devices.findById(id);
if (!existing) {
throw new UserInputError('Device not found', { invalidArgs: ['id'] });
}
// Check ownership
if (existing.userId !== context.user.id) {
throw new ForbiddenError('You can only update your own devices');
}
// Merge updates (only provided fields)
const updated = { ...existing, ...input, updatedAt: new Date().toISOString() };
await context.db.devices.update(id, updated);
return updated;
},Delete
deleteDevice: async (parent, { id }, context) => {
const device = await context.db.devices.findById(id);
if (!device) {
return { success: false, message: 'Device not found' };
}
await context.db.devices.delete(id);
return { success: true, message: 'Device deleted' };
},Return Types for Mutations
Always return the modified data so clients can update their cache:
# ✅ Good — returns the created/updated object
type Mutation {
createDevice(input: CreateDeviceInput!): Device!
}
# ❌ Bad — returns only a boolean
type Mutation {
createDevice(input: CreateDeviceInput!): Boolean!
}For deletions, use a response type with status:
type DeleteResponse {
success: Boolean!
message: String
deletedId: ID
}
type Mutation {
deleteDevice(id: ID!): DeleteResponse!
}Mutations vs REST
| Aspect | GraphQL Mutation | REST |
|---|---|---|
| Create | mutation { createDevice(input: {...}) { id name } } | POST /devices → returns full resource |
| Update | mutation { updateDevice(id: "1", input: {name: "New"}) { id name } } | PUT /devices/1 or PATCH /devices/1 |
| Delete | mutation { deleteDevice(id: "1") { success } } | DELETE /devices/1 → 204 No Content |
| Return | Client specifies return fields | Server decides return shape |
| Execution | Sequential (one mutation at a time) | Parallel (multiple requests) |
Common Mistakes
1. Not Validating Input in Resolvers
Input types in the schema validate types but not business rules. Always validate in resolvers too — check lengths, uniqueness, and referential integrity.
2. Returning Only a Boolean
Clients need the created/updated object to update their cache. Always return the modified resource.
3. Not Handling Partial Failures
In a single mutation, if the DB update succeeds but cache invalidation fails, roll back the mutation. Use transactions when modifying multiple resources.
4. Over-fetching After Mutation
After creating a device, avoid fetching extra data that the client didn’t request. Only return the fields the mutation’s return type specifies.
5. Confusing Mutations with Queries
Mutations should modify data and execute sequentially. Queries should not modify data and can execute in parallel. Don’t put side effects in queries.
Practice Questions
- Why do mutations execute sequentially while queries run in parallel?
- What is the purpose of input types in mutations?
- Why should mutations return the modified resource?
- How do you handle authorization in mutation resolvers?
- What error types does Apollo Server provide for mutations?
Answers:
- Mutations often depend on the state left by previous mutations. Sequential execution prevents race conditions.
- Input types group related fields into structured arguments, providing type validation and better documentation than flat argument lists.
- The client needs the updated data to update its local cache without making an additional query.
- Check
context.userin each mutation resolver. UseAuthenticationErrorfor missing auth andForbiddenErrorfor insufficient permissions. AuthenticationError,ForbiddenError,UserInputError,ValidationError, andApolloErrorfor custom errors.
Challenge: Write a complete mutation for Durga Antivirus Pro: submitThreatReport(input: ThreatReportInput!): ThreatReport! — include input validation (threat name required, severity must be valid enum), auth check (user must be authenticated), device ownership verification, and return the created report with its device details.
FAQ
Try It Yourself
Create a simple mutation server:
const { ApolloServer, gql, UserInputError } = require('apollo-server');
let devices = [];
const typeDefs = gql`
type Device { id: ID! name: String! os: String! }
input CreateDeviceInput { name: String! os: String! }
type Mutation { createDevice(input: CreateDeviceInput!): Device! }
type Query { devices: [Device!]! }
`;
const resolvers = {
Query: { devices: () => devices },
Mutation: {
createDevice: (_, { input }) => {
if (!input.name) throw new UserInputError('Name required');
const device = { id: String(devices.length + 1), ...input };
devices.push(device);
return device;
},
},
};
new ApolloServer({ typeDefs, resolvers }).listen(4000);What’s Next
| Topic | Description |
|---|---|
| GraphQL Architecture | Subscriptions, federation, server setup |
| Queries & Resolvers | Data fetching patterns and DataLoader |
| Types & Schema Design | Review type system fundamentals |
| REST methods comparison | Compare mutations with RESTful endpoints |
What’s Next
Congratulations on completing this Graphql Mutations tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro