GraphQL - Mutation
GraphQL - Mutation
Section titled “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.
Mutation Syntax
Section titled “Mutation Syntax”A mutation request looks very similar to a query, but it must start with the mutation keyword.
# Basic Mutation Syntaxmutation OptionalOperationName { mutationFieldName(argumentName: "value", ...) { # Fields to return after the mutation succeeds field1 field2 ... }}Key differences from queries:
- Keyword: Must use the
mutationkeyword. - 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.
Input Types for Mutations
Section titled “Input Types for Mutations”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 studentinput CreateStudentInput { firstName: String! lastName: String! collegeId: ID! # Assuming college must exist email: String # Optional field}
# Define the Student type that the mutation returnstype Student { id: ID! firstName: String! lastName: String! email: String collegeId: ID!}
# Define the mutation using the input typetype Mutation { createStudent(input: CreateStudentInput!): Student # Returns the created Student}Illustration: Creating a Student
Section titled “Illustration: Creating a Student”Let’s implement the createStudent mutation defined above.
Step 1: Server & Data Setup
Section titled “Step 1: Server & Data Setup”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.
#graphqltype 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.jsimport { 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
collegeIdexists). - 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.
Step 4: Run and Test the Mutation
Section titled “Step 4: Run and Test the Mutation”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 Variablesmutation 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.
Returning Related Data in Mutations
Section titled “Returning Related Data in Mutations”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 ittype 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 datamutation 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.