Skip to content

GraphQL Mutations Guide: Create, Update & Delete Data with Input Types

DodaTech Updated Jun 6, 2026 7 min read

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: {...}}}
  
Prerequisites: GraphQL, GraphQL Types Schema, and GraphQL Queries Resolvers.

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

AspectGraphQL MutationREST
Createmutation { createDevice(input: {...}) { id name } }POST /devices → returns full resource
Updatemutation { updateDevice(id: "1", input: {name: "New"}) { id name } }PUT /devices/1 or PATCH /devices/1
Deletemutation { deleteDevice(id: "1") { success } }DELETE /devices/1 → 204 No Content
ReturnClient specifies return fieldsServer decides return shape
ExecutionSequential (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

  1. Why do mutations execute sequentially while queries run in parallel?
  2. What is the purpose of input types in mutations?
  3. Why should mutations return the modified resource?
  4. How do you handle authorization in mutation resolvers?
  5. What error types does Apollo Server provide for mutations?

Answers:

  1. Mutations often depend on the state left by previous mutations. Sequential execution prevents race conditions.
  2. Input types group related fields into structured arguments, providing type validation and better documentation than flat argument lists.
  3. The client needs the updated data to update its local cache without making an additional query.
  4. Check context.user in each mutation resolver. Use AuthenticationError for missing auth and ForbiddenError for insufficient permissions.
  5. AuthenticationError, ForbiddenError, UserInputError, ValidationError, and ApolloError for 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

Can I call multiple mutations in one request?
: Yes — but unlike queries, mutations execute sequentially in the order written. You can combine a create and update in one request: mutation { createDevice(...) { id } updateThreat(...) { id } }
How do I handle file uploads in a mutation?
: Use the Upload scalar type and multipart form data. The resolver receives a FileUpload object with createReadStream, filename, and mimetype. Upload to cloud storage and return the URL.
What is the difference between UserInputError and ValidationError?
: UserInputError is for invalid user input (bad format, missing required fields). Use it for arguments that fail validation. ValidationError is more general — use it when data fails business rule validation.
Should mutations always return the full object?
: Return the minimum viable data the client needs to update its cache. Usually that’s the full object, but if the client only needs the id and name, the mutation resolver can optimize by returning those fields (though the resolver typically returns the full object and GraphQL strips unnecessary fields).

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

TopicDescription
GraphQL ArchitectureSubscriptions, federation, server setup
Queries & ResolversData fetching patterns and DataLoader
Types & Schema DesignReview type system fundamentals
REST methods comparisonCompare 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