Back to Blog
Lesson 34 of the GraphQL: Your First GraphQL Schema & Server course
API ArchitectureAugust 21, 20264 min read

Designing for Deletion: Building GraphQL Remove Mutations

Learn how to implement a GraphQL mutation to safely remove items from your data store and return the deleted ID for client-side state synchronization.

GraphQLMutationsCRUDBackend DevelopmentAPI Design
Close-up of keyboard letters spelling 'DELETE' on a coral background, emphasizing digital concepts.

Previously in this course, we explored handling mutation errors to ensure our API remains robust when things go wrong. Now that we have a solid foundation for state management and error handling, it’s time to complete our CRUD (Create, Read, Update, Delete) cycle by implementing a deletion mutation.

Why Return the ID on Deletion?

When designing for Deletion in GraphQL, a common point of confusion is what the mutation should return. Unlike a "Create" or "Update" mutation, which typically returns the entire object to update the client-side cache, a "Delete" mutation often leaves the client with a "dead" object that no longer exists on the server.

Returning the ID of the deleted item is the industry-standard approach. It allows the client to:

  1. Confirm the action: The client receives a definitive signal that the server processed the request.
  2. Update the UI: The client uses this ID to filter the record out of local lists or store state without needing to re-fetch the entire collection.

Implementing the Delete Mutation

To handle deletion, we need to modify our schema to include the mutation and update our resolver to filter out the target record from our in-memory data store.

1. Defining the Schema

Add the deleteBook mutation to your typeDefs. We use the ID! scalar to ensure the client provides a valid identifier.

GraphQL
type Mutation {
  # Other mutations...
  deleteBook(id: ID!): ID!
}

2. Building the Resolver

In your resolver map, we will use the filter method to remove the item from our array. This is a common pattern when managing mutation state.

JAVASCRIPT
const resolvers = {
  Mutation: {
    deleteBook: (_, { id }, { dataSources }) => {
      const bookIndex = books.findIndex(book => book.id === id);

      if (bookIndex === -1) {
        throw new Error(CE9178">`Book with ID ${id} not found.`);
      }

      // Remove the book from the array
      const deletedBook = books.splice(bookIndex, 1);

      // Return the ID of the deleted item
      return deletedBook[0].id;
    },
  },
};

Hands-on Exercise

Using the project we established in our earlier lessons on implementing a simple mutation, perform the following steps:

  1. Update your SDL: Add the deleteBook(id: ID!): ID! mutation definition.
  2. Implement the logic: Add the corresponding resolver function.
  3. Test in Sandbox: Open Apollo Sandbox and run a mutation to delete an existing book. Observe that the response returns only the ID string.
  4. Verification: After deleting, run a Query to fetch the list of books and verify that the deleted ID is no longer present.

Common Pitfalls

  • Returning null on missing items: If the item doesn't exist, don't just return null. Throw a descriptive error so the client knows why the operation failed, as we covered in our mutation best practices.
  • Mutating the wrong array reference: Ensure your resolver is modifying the actual data source, not a shallow copy that gets garbage collected after the function returns.
  • Over-complicating return types: While returning the whole object is fine for updates, it adds unnecessary payload for a delete operation where the data is no longer valid. Stick to the ID.

FAQ

Q: Should I use a boolean (true/false) instead of an ID for deletion? A: Returning an ID is more descriptive. It confirms exactly which resource was removed, which is helpful if your client is managing multiple lists or complex state.

Q: Does GraphQL handle cascading deletes automatically? A: No. GraphQL is just a contract. You must manually implement the logic to clean up related data (like deleting "Reviews" associated with a "Book") within your resolver.

Q: What if the delete operation takes a long time? A: If you are interacting with a database, your resolver should be asynchronous. We will cover this in detail when we look at asynchronous resolvers.

Recap

We’ve successfully closed the loop on CRUD operations. By defining a clear delete mutation that accepts an ID and returns that same ID upon success, we create a predictable interface for our frontend consumers. This practice ensures that your API remains clean and your client-side state stays perfectly in sync with the server.

Up next: We will look at how to ensure the data passed into your mutations is correct by implementing input validation.

Similar Posts