Mutations with Apollo Client

Queries are read-only operations. Mutations are write-only operations.

Just like the Query type, the Mutation type serves as an entrypoint to the schema.

Naming convention

  • Use verb: add, create, delete
  • Refer to the datatype

For example addSpaceCat(){}

Demonstration mutation

We are going to create a mutation that increments the numberOfViews field on the Track type:

Updating the schema

type Mutation {
    incrementTrackViews(id: ID!): IncrementTrackViewsResponse!
}


// Define a specific response type that specifically matches our needs
type IncrementTrackViewsResponse {
    code: Int! // status code
    success: Boolean! // whether mutation was successful
    message: String! // what to say if mutation successful
    track: Track   // not nullable because might error
}

Based on this schema, the mutation will recieve a Track id and increment the specified Track. It will return an object comprising the newly updated Track and a bundle of properties that provide feedback on the status of the operations: a status code, whether it succeeded, and a message.

Updating the data source

Remember that our sole data source in the demonstration project is a REST API. We handle it within GraphQL using Apollos RESTDataSource class. We need to add a method to this class that will increment the track views. We wil use the PATCH REST method:

class TrackAPI extends RESTDataSource {
  constructor() {...}
  getTracksForHome() {...}
  getAuthor(authorId) {...}
  getTrack(trackId){...};

  incrementTrackViews(trackId) {
    return this.patch(`track/${trackId}/numberOfViews`);
  }
}

The patch() method is procided by the RESTDataSouce class that TrackAPI inherits from

Adding resolver

Next we need a resolver that corresponds to the mutation we have defined in the schema. We will need to handle successful responses as well as errors.

Success case

As always we match the shape of the schema:

const resolvers = {
  Query: {
    // ...query resolvers
  },
  Mutation: {
    // increments a track's numberOfViews property
    incrementTrackViews: async (_, { id }, { dataSources }) => {
      const track = await dataSources.trackAPI.incrementTrackViews(id);
      return {
        code: 200,
        success: true,
        message: `Successfully incremented number of views for track ${id}`,
        track,
      };
    },
  },
};

There’s more going on with this resolver than the previous one. As is standard, we call the API using the TrackAPI class. However we don’t just immediately return this when it executes. This is because the schema specifies that the return type IncrementTrackViewsResponse requires more than just the updated Track. So we wait this and return it with the cluster of metadata about the mutation response (code, success, and message).

Error case

We can extend the Mutation resolver to allow for errors. We’ll do this by refactoring the resolver into a try...catch block and adding the error handling in the catch.

We’ll harness the details that are provided by Apollos’ own err object which is returned by the RESTDataSource class that our resolver ultimately traces back to:

const resolvers = {

    Query: {
        // ...query resolvers
    }

    Mutation: {
        incrementTrackViews: async (_, {id}, {dataSources}) => {
            try {
                const track = await dataSources.trackAPI.incrementTrackViews(id);
                return {
                    code: 200,
                    success: true,
                    message: `Successfully incremented number of views for track ${id}`,
                    track
                };
            } catch (err) {
                return {
                    code: err.extensions.response.status,
                    success: false,
                    message: err.extensions.response.body,
                    track: null
                };
            }
        },
    }
}

The useMutation hook

We invoke the useMutation hook to issue mutations from the client-side.

As with queries and query constants we wrap our mutation in a `gql` template string:

const INCREMENT_TRACK_VIEWS = gql`
  mutation IncrementTrackViews($incrementTrackViewsId: ID!) {
    incrementTrackViews(id: $incrementTrackViewsId) {
      code
      success
      message
      track {
        id
        numberOfViews
      }
    }
  }
`;

We then pass it to the useMutation hook including an options object with our variables. (This time the specific variable is named):

import { gql, useMutation } from "@apollo/client";

useMutation(INCREMENT_TRACK_VIEWS, {
  variables: { incrementTrackViewsId: id },
});

useMutation returns an array of two elements:

  1. The mutation function that actually executes
  2. An object comprising (loading, error, data) - this is the same as is the return value of useQuery.

So we can destructure like so (we don’t always need the second element);

const [incrementTrackViews, dataObject] = useMutation(INCREMENT_TRACK_VIEWS...)

Given that we can isolate the mutation function as the first destructured element of the array, we could then attach incrementTrackViews to a button or other frontend interaction.