Skip to content

GraphQL - Mutation

While Queries are used for fetching data, Mutations are used for modifying server-side data. This includes operations like creating new data, updating existing data, or deleting data. Think of mutations as the GraphQL equivalent of POST, PUT, PATCH, or DELETE requests in REST.

Mutations are defined as fields under the root Mutation type in your GraphQL schema. Like queries, they can accept arguments (typically grouped within an input object type) and return specific data, often the data that was just modified.

A mutation request looks very similar to a query, but it must start with the mutation keyword.

# Basic Mutation Syntax
mutation OptionalOperationName {
mutationFieldName(argumentName: "value", ...) {
# Fields to return after the mutation succeeds
field1
field2
...
}
}

Key differences from queries:

  • Keyword: Must use the mutation keyword.
  • Execution: While query fields are often executed in parallel by the server, top-level mutation fields are typically executed serially (one after another) to ensure predictable side effects and avoid race conditions.
  • Return Value: It’s crucial for mutations to return data reflecting the state change. This allows clients (like Apollo Client) to update their cache accurately.

It’s a strong convention and best practice to define a single, dedicated input object type for the arguments of each mutation. This makes the mutation easier to evolve (add optional fields later) and keeps the argument list clean.

# Schema Definition with Input Type
# Input object type for creating a student
input CreateStudentInput {
firstName: String!
lastName: String!
collegeId: ID! # Assuming college must exist
email: String # Optional field
}
# Define the Student type that the mutation returns
type Student {
id: ID!
firstName: String!
lastName: String!
email: String
collegeId: ID!
}
# Define the mutation using the input type
type Mutation {
createStudent(input: CreateStudentInput!): Student # Returns the created Student
}

Let’s implement the createStudent mutation defined above.

Assume a basic Apollo Server setup and a mock data source (db.js) with a way to add students.

// db.js (Mock Data - Example with create function)
let students = [
{ id: 'S1001', firstName: 'Mohtashim', lastName: 'Mohammad', collegeId: 'col-102' },
{ id: 'S1002', firstName: 'Kannan', lastName: 'Sudhakaran', collegeId: 'col-101' },
];
let nextStudentId = 1003; // Simple ID generation for mock
export const db = {
students: {
list: () => students,
get: (id) => students.find(s => s.id === id),
create: (input) => {
const newStudent = {
id: `S${nextStudentId++}`,
...input // Spread properties from the input object
};
students.push(newStudent);
console.log("Current students:", students); // Log current state
return newStudent; // Return the newly created student object
}
},
// Assume colleges data exists for validation if needed
colleges: {
get: (id) => { /* ... logic to find college ... */ return { id: id, name: 'Some College'}; },
}
};

Step 2: Define Schema (schema.graphql or in server.js)

Section titled “Step 2: Define Schema (schema.graphql or in server.js)”

Use the schema defined previously, including CreateStudentInput, Student, and the Mutation type.

#graphql
type Query { # Need at least one Query field for a valid schema
_: Boolean
}
input CreateStudentInput {
firstName: String!
lastName: String!
collegeId: ID!
email: String
}
type Student {
id: ID!
firstName: String!
lastName: String!
email: String
collegeId: ID!
}
type Mutation {
createStudent(input: CreateStudentInput!): Student
}

Step 3: Create Resolver for the Mutation (resolvers.js)

Section titled “Step 3: Create Resolver for the Mutation (resolvers.js)”

Implement the resolver function for the createStudent field within the Mutation type resolver map.

// resolvers.js
import { db } from './db.js';
import { GraphQLError } from 'graphql';
const resolvers = {
Query: {
_: () => true, // Resolver for placeholder query
},
Mutation: {
createStudent: (parent, args, context, info) => {
// The input object is available in args.input
const { input } = args;
console.log('Resolving: Mutation.createStudent with input:', input);
// --- Optional Validation ---
// Example: Check if collegeId exists (in a real app)
const collegeExists = db.colleges.get(input.collegeId);
if (!collegeExists) {
throw new GraphQLError(`College with ID ${input.collegeId} not found.`, {
extensions: { code: 'BAD_USER_INPUT' }
});
}
// Add other validations (email format, etc.) if needed
// --- End Validation ---
// Call the data source function to create the student
try {
const newStudent = db.students.create(input);
// Return the newly created student object
// This matches the 'Student' return type defined in the schema
return newStudent;
} catch (error) {
// Handle potential errors during creation
console.error("Error creating student:", error);
throw new GraphQLError('Failed to create student.', {
extensions: { code: 'INTERNAL_SERVER_ERROR' }
});
}
},
},
// Optional: Add Student resolvers if needed (e.g., for resolving college object)
// Student: {
// college: (parent) => db.colleges.get(parent.collegeId)
// }
};
export default resolvers;

Key points:

  • The resolver accesses the arguments via args.input.
  • It performs optional validation (checking if the collegeId exists).
  • It calls the appropriate data source method (db.students.create) to perform the write operation.
  • Crucially, it returns the newly created student object. This allows the client to get the ID generated by the server and update its state/cache.

Integrate the schema and resolvers into your Apollo Server setup and run the server (npm start).

Use Apollo Sandbox (http://localhost:9000/graphql) to execute the mutation. Remember to use the mutation keyword.

# Mutation Request using Variables
mutation AddNewStudent($studentData: CreateStudentInput!) {
createStudent(input: $studentData) {
# Specify which fields of the created student you want back
id
firstName
lastName
email
collegeId
}
}

In the ‘Variables’ panel, provide the input data:

{
"studentData": {
"firstName": "Tim",
"lastName": "George",
"collegeId": "col-101",
"email": "tim.g@example.com"
}
}

Execute the mutation. The response should contain the data for the newly created student, including its server-generated ID:

{
"data": {
"createStudent": {
"id": "S1003", // Example generated ID
"firstName": "Tim",
"lastName": "George",
"email": "tim.g@example.com",
"collegeId": "col-101"
}
}
}

You can verify the creation by running a students or studentById query afterwards, or by checking your mock data console log.

Because mutations can return any type defined in your schema, you can design them to return related data as well, saving the client an extra fetch.

Example: If you wanted the createStudent mutation to return the student and their associated college details:

# 1. Modify Schema: Add College type and link it
type College {
id: ID!
name: String!
}
type Student {
id: ID!
# ... other fields
college: College # Add relationship field
}
type Mutation {
createStudent(input: CreateStudentInput!): Student # Return type is still Student
}
# 2. Add Resolver for Student.college (as shown in Query chapter)
# In resolvers.js
# Student: {
# college: (parent) => db.colleges.get(parent.collegeId)
# }
# 3. Modify Mutation Request to ask for nested data
mutation AddStudentAndGetCollege($studentData: CreateStudentInput!) {
createStudent(input: $studentData) {
id
firstName
college { # Request nested college data
id
name
}
}
}

The createStudent mutation resolver itself doesn’t change significantly (it still just creates and returns the student object). The magic happens because when the client requests the nested college field in the mutation’s return selection set, the Student.college resolver will automatically run for the newly created student object before the final response is sent.