Implementing a Simple Mutation: Modifying State in GraphQL
Learn how to implement your first mutation resolver. We'll cover modifying local state, returning the updated object, and ensuring your GraphQL API is responsive.

Previously in this course, we explored Introduction to Mutations to understand the conceptual difference between read-only queries and data-modifying operations. Now, we’ll move from theory to practice by implementing a concrete mutation to modify our server's local state.
The Anatomy of a Mutation Resolver
In GraphQL, a mutation is just a function—a resolver—that performs a side effect. Unlike a query, which is designed to be idempotent (fetching the same data repeatedly should yield the same result), a mutation is expected to change the state of your application.
To implement a mutation, you follow three steps:
- Define the Mutation type in your schema.
- Implement the resolver function in your
resolversmap. - Return the modified object so the client can immediately update its cache.
Worked Example: Updating a Book Title
Imagine we are managing a list of books. We want to be able to rename a book. We'll start with a simple in-memory array to simulate our database.
1. Define the Schema
In your typeDefs, you must explicitly declare the Mutation type.
GraphQLtype Book { id: ID! title: String! } type Query { books: [Book] } type Mutation { updateBookTitle(id: ID!, newTitle: String!): Book }
2. Implement the Resolver
Now, we create the logic. In our resolvers object, we add a Mutation key that mirrors the structure of our schema.
JAVASCRIPTconst books = [ { id: CE9178">'1', title: CE9178">'The Great Gatsby' }, { id: CE9178">'2', title: CE9178">'1984' } ]; const resolvers = { Query: { books: () => books, }, Mutation: { updateBookTitle: (parent, args) => { const book = books.find(b => b.id === args.id); if (!book) return null; // Handle case where ID doesn't exist book.title = args.newTitle; // Perform the modification return book; // Return the updated object }, }, };
By returning the book object, we allow the client to request the new title immediately without needing a separate follow-up query. This is a core pattern in Implementing Minimal Code: The Key to Simple, Clean Systems — keep your resolvers focused and your return values predictable.
Hands-on Exercise
Using the code above as your foundation, add a second mutation called addBook to your server.
- Update your
typeDefsto includeaddBook(title: String!): Book. - Implement the resolver to create a new object with a generated ID, push it into the
booksarray, and return the new object. - Test your implementation using Apollo Sandbox (as discussed in Using Apollo Sandbox: Testing Your Local GraphQL API).
Common Pitfalls
- Forgetting to return the object: If your mutation returns
nullorundefined, the client cannot update its local cache. Always return the modified data. - Mutating the wrong reference: Ensure you are modifying the object within your data source, not just a local variable copy.
- Ignoring errors: If the ID passed to the mutation doesn't exist, don't just return nothing. In a production system, you should throw an error (we will cover this in later lessons).
FAQ
Why must I return the modified object? Returning the object allows GraphQL clients (like Apollo Client) to automatically update the cache for that specific object. It keeps the UI consistent with the server state without requiring a full page refresh.
Can I perform multiple modifications in one mutation? Yes, but it is generally better to keep mutations granular. A single mutation should ideally represent a single "action" in your system to keep the API predictable.
How is this different from Filtering Data with Arguments?
When we were Filtering Data with Arguments: Dynamic GraphQL Resolvers, we were only changing the view of the data. Mutations actually alter the underlying data structure.
Recap
We have successfully implemented a mutation that modifies our server's memory. We defined the operation in the schema, wrote the resolver to update the local array, and returned the object to complete the cycle. You now have the fundamental building blocks to move from a read-only API to a fully interactive one.
Up next: Learn how to clean up your arguments and improve type safety by using Input Types.
Work with me

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app — built with FilamentPHP so you can manage everything without touching the database.


