14. Mutations
10m

Overview

We've seen the power of specific and expressive queries that let us retrieve exactly the data we're looking for, all at once. But in is just one part of the equation.

When we want to actually change, insert, or delete data, we need to reach for a new tool: .

In this lesson, we will:

  • Explore syntax and write an to add tracks to a playlist
  • Learn about input types
  • Learn about response best practices

Mutations in Spotify

On to the next feature in our MusicMatcher project: adding tracks to an existing playlist.

Let's take a look at the corresponding REST API method that enables this feature: POST /playlists/{playlist_id}/tracks

From the documentation, we need the following parameters:

  • playlist_id - The ID of the playlist, as a string
  • position - An integer, zero-indexed, where we want to insert the track(s)
  • uris - A comma-separated string of uri values corresponding to the tracks we want to add

The method then returns an object with a snapshot_id property that represents the state of the playlist at that point in time.

All right, now how do we enable this functionality in ?

Designing mutations

Much like the Query type, the Mutation type serves as an entry point to our schema. It follows the same syntax as the , or , that we've been using so far.

We declare the Mutation type using the type keyword, then the name Mutation. Inside the curly braces, we have our entry points, the we'll be using to mutate our data.

Let's open up schema.graphql and add a new Mutation type.

schema.graphql
type Mutation {
}

For the of the Mutation, we recommend starting with a verb that describes the specific action of our update (such as add, delete, or create), followed by whatever data the acts on.

We'll explore how we can add items to a playlist, so we'll call this addItemsToPlaylist.

schema.graphql
type Mutation {
"Add one or more items to a user's playlist."
addItemsToPlaylist: #TODO
}

For the return type of the addItemsToPlaylist , we could return the Playlist type; it's the we want the to act upon. However, we recommend following a consistent Response type for responses. Let's see what this looks like in a new type.

The Mutation response type

Return types for Mutation usually end with the word Payload or Response.

Following convention, we'll combine the name of our (addItemsToPlaylist) with Payload. (Don't forget to capitalize!)

schema.graphql
type AddItemsToPlaylistPayload {
}

We should return the that we're mutating (Playlist, in our case), so that clients have access to the updated object.

schema.graphql
type AddItemsToPlaylistPayload {
playlist: Playlist
}

Note: Though our acts upon a single Playlist object, it's also possible for a to change and return multiple objects at once.

Notice that playlist can be null, because our might fail.

To account for any partial errors that might occur and return helpful information to the client, there are a few additional we can include in a response type.

  • code: an Int that refers to the status of the response, similar to an HTTP status code.

  • success: a Boolean flag that indicates whether all the updates the was responsible for succeeded.

  • message: a String to display information about the result of the on the client side. This is particularly useful if the mutation was only partially successful and a generic error message can't tell the whole story.

Let's also add comments for each of these so that it makes our API documentation more useful.

schema.graphql
type AddItemsToPlaylistPayload {
"Similar to HTTP status code, represents the status of the mutation"
code: Int!
"Indicates whether the mutation was successful"
success: Boolean!
"Human-readable message for the UI"
message: String!
"The playlist that contains the newly added items"
playlist: Playlist
}

Lastly, we can set the return type of our to this new AddItemsToPlaylistPayload type, and make it non-nullable. Here's what the addItemsToPlaylist should look like now:

schema.graphql
type Mutation {
"Add one or more items to a user's playlist."
addItemsToPlaylist: AddItemsToPlaylistPayload!
}

The Mutation input

To make any changes to a particular playlist, our needs to receive some input.

Let's think about the kind of input this addItemsToPlaylist would expect. We're potentially adding many new tracks to a singular playlist. This means that whoever is sending this should be able to provide the specific playlist, along with the item(s) to be added. Furthermore, we could let them pass additional customization, specifying where in the playlist the items should be inserted.

We've used a before in the Query.playlist : we passed in a single called id.

type Query {
playlist(id: ID!): Playlist
}

But addItemsToPlaylist takes more than one . One way we could tackle this is to add each argument, one-by-one, to our addItemsToPlaylist . But this approach can become unwieldy and hard to understand. Instead, it's a good practice to use input types as for a .

Exploring the input type

The input type in a is a special that groups a set of together, and can then be used as an argument to another . Using input types helps us group and understand , especially for .

To define an input type, use the input keyword followed by the name and curly braces ({}). Inside the curly braces, we list the and types as usual. Note that fields of an input type can be only a , an enum, or another input type.

schema.graphql
input AddItemsToPlaylistInput {
}

Next, we'll add properties. Remember, we need the ID of the playlist and a list of URIs, at the very minimum. We could also specify the position in the playlist these items get added to, but it's not required for the REST API. By default, tracks will be appended to the end of the playlist, so we're safe to omit it from our . Remember, your GraphQL API does not need to match your REST API exactly!

schema.graphql
input AddItemsToPlaylistInput {
"The ID of the playlist."
playlistId: ID!
"A comma-separated list of Spotify URIs to add."
uris: [String!]!
}

Note: You can learn more about the input type, as well as other types and features in Side Quest: Intermediate Schema Design.

Using the input

To use an input type in the schema, we can set it as the type of a . Let's update the addItemsToPlaylist to use the AddItemsToPlaylistInput type.

schema.graphql
type Mutation {
"Add one or more items to a user's playlist."
addItemsToPlaylist(
input: AddItemsToPlaylistInput!
): AddItemsToPlaylistPayload!
}

Notice that the AddItemsToPlaylistInput is non-nullable. To run this , we actually need to require some input!

Building the SpotifyAPI method

As we've done for other requests, we'll add a method to the SpotifyAPI class to manage this call to update data.

Back in datasources/spotify-api.ts, let's add a new method called addItemsToPlaylist. We'll expect this method to receive an input parameter, which is an object containing playlistId (a string type) and uris (an array of string types).

datasources/spotify-api.ts
addItemsToPlaylist(input: { playlistId: string, uris: string[] }) {
// TODO
}

This time, because the endpoint uses the POST method, we'll use the this.post helper method from RESTDataSource instead of this.get.

Next, we'll pass our endpoint to this method. We'll replace the {playlist_id} param with ${playlistId} so we can interpolate it from this method's .

spotify-api.ts
addItemsToPlaylist(input: { playlistId: string, uris: string[] }) {
const { playlistId, uris } = input;
return this.post(`playlists/${playlistId}/tracks`);
}

We need to send a parameter with our call to this endpoint: namely, the uris that we wish to add to the specified playlist. We can pass a second to the this.post method, which is an object with a params key. Inside of the params object, we'll add our uris. Referring to our REST API, we know that uris needs to be a single string of comma-separated uri values, so we'll use the join method to transform our array of strings into a single string.

spotify-api.ts
addItemsToPlaylist(input: { playlistId: string, uris: string[] }) {
const { playlistId, uris } = input;
return this.post(`playlists/${playlistId}/tracks`, {
params: {
uris: uris.join(',')
}
});
}

To give this method its correct return type, let's consider the two possible outcomes from running the .

Glancing back at our REST API documentation, we can see that when a playlist is successfully updated, we get back an object with a "snapshot_id" key that matches the ID of the playlist we updated.

Response object: success
{
"snapshot_id": "string" // this should match our updated playlist's id!
}

And if something goes wrong—like if we try to update a playlist that doesn't exist—we should instead get back an object with an "error" key.

Response object: error
{
"error": "string"
}

Back in models.ts, let's create a type specifically for these possible responses. It will have two optional properties, both string types: snapshot_id and error.

models.ts
export type SnapshotOrError = {
snapshot_id?: string;
error?: string;
};

We'll first import this type into spotify-api.ts.

spotify-api.ts
import { PlaylistModel, SnapshotOrError } from "../models";

Then we can give our addItemsToPlaylist method the appropriate return type of Promise<SnapshotOrError>.

spotify-api.ts
addItemsToPlaylist(input: { playlistId: string, uris: string[] }): Promise<SnapshotOrError> {
// body of method
}

That's it for the call to the endpoint—let's add a function for our Mutation.addItemsToPlaylist , and make sure it's calling this method in SpotifyAPI!

Connecting the dots in the resolvers

Jump back into the resolvers.ts file. We'll add a new entry to our resolvers object called Mutation.

resolvers.ts
Mutation: {
// TODO
},

Let's add our addItemsToPlaylist .

resolvers.ts
Mutation: {
addItemsToPlaylist: (parent, args, contextValue, info) => {
// TODO
}
},

We won't need the parent or info parameters here, so we'll replace parent with _ and remove info altogether.

resolvers.ts
addItemsToPlaylist: (_, args, contextValue) => {
// TODO
},

We know from our Mutation.addItemsToPlaylist schema that this will receive an called input. We'll destructure args for this property. And while we're here, we'll also destructure contextValue for the dataSources property.

resolvers.ts
addItemsToPlaylist: (_, { input }, { dataSources }) => {
// TODO
},

Now we'll call the addItemsToPlaylist method from the spotifyAPI , passing it our input.

resolvers.ts
addItemsToPlaylist: (_, { input }, { dataSources }) => {
dataSources.spotifyAPI.addItemsToPlaylist(input);
},

We're not returning anything from this method just yet, so we might see some errors. In our schema we specified that the Mutation.addItemsToPlaylist should return a AddItemsToPlaylistPayload type.

schema.graphql
type AddItemsToPlaylistPayload {
"Similar to HTTP status code, represents the status of the mutation"
code: Int!
"Indicates whether the mutation was successful"
success: Boolean!
"Human-readable message for the UI"
message: String!
"The playlist that contains the newly added items"
playlist: Playlist
}

This means that the object we return from our needs to match this shape. Let's start by setting up the object that we'll return in the event that our succeeds.

resolvers.ts
addItemsToPlaylist: (_, { input }, { dataSources }) => {
dataSources.spotifyAPI.addItemsToPlaylist(input);
// everything succeeds with the mutation
return {
code: 200,
success: true,
message: "Tracks added to playlist!",
playlist: null, // We don't have this value yet
}
},

Checking the response

Now, let's take a look at the response from our API call. We'll need to make our entire async in order to await the results of calling dataSources.spotifyAPI.addItemsToPlaylist().

resolvers.ts
addItemsToPlaylist: async (_, { input }, { dataSources }) => {
const response = await dataSources.spotifyAPI.addItemsToPlaylist(input);
console.log(response);
// everything succeeds with the mutation
return {
code: 200,
success: true,
message: "Tracks added to playlist!",
playlist: null, // We don't have this value yet
}
},

In Sandbox, let's put together a .

mutation AddTracksToPlaylist($input: AddItemsToPlaylistInput!) {
addItemsToPlaylist(input: $input) {
code
success
message
}
}

And add the following to the Variables panel.

{
"input": {
"playlistId": "6LB6g7S5nc1uVVfj00Kh6Z",
"uris": [
"spotify:track:4iV5W9uYEdYUVa79Axb7Rh",
"spotify:track:1301WleyT98MSxVHPZCA6M"
]
}
}

When we run the , we'll see that this actually works as expected! We can clearly see the values we set for code, success, and message in our happy path.

And in the terminal of our running server, we'll see the response from the API logged out.

{
snapshot_id: "6LB6g7S5nc1uVVfj00Kh6Z";
}

The snapshot_id, we can see, is the playlistId of the playlist we added tracks to!

Let's build out the sad path—when our doesn't go as expected.

Handling the sad path

Returning to our Mutation.addItemsToPlaylist, we'll account for the error state.

We'll start by wrapping everything in a try/catch block.

resolvers.ts
try {
const response = await dataSources.spotifyAPI.addItemsToPlaylist(input);
return {
code: 200,
success: true,
message: "Tracks added to playlist!",
playlist: null, // We don't have this value yet
};
} catch (err) {}

Next, we'll contain the object we return in an if block. We'll check for the existence of the snapshot_id in the response from the REST API. If it doesn't exist, we'll throw an Error.

resolvers.ts
try {
const response = await dataSources.spotifyAPI.addItemsToPlaylist(input);
if (response.snapshot_id) {
return {
code: 200,
success: true,
message: "Tracks added to playlist!",
playlist: null,
};
} else {
throw Error("snapshot_id property not found");
}
} catch (err) {}

Inside of the catch block, we'll set some different properties.

resolvers.ts
try {
// try block body
} catch (err) {
return {
code: 500,
success: false,
message: `Something went wrong: ${err}`,
playlist: null,
};
}

Here's how your entire function should look.

Let's try out a that we know won't work. Back in Sandbox, replace the values in the Variables panel with the input below.

Variables panel input
{
"input": {
"playlistId": "playlist-that-doesnt-exist",
"uris": [
"spotify:track:4iV5W9uYEdYUVa79Axb7Rh",
"spotify:track:1301WleyT98MSxVHPZCA6M"
]
}
}

When we run the , we'll see the values we expect: the failed, and our error message comes through loud and clear. Something went wrong: 404 Not Found!

Great! We have both the happy path and the sad path working, but we'll notice that there's one piece missing: we're still not returning any useful data for the AddItemsToPlaylistPayload.playlist .

This means if we include sub from the Playlist type (as shown in the below), we won't get actual data—we don't have a Playlist object to pull sub from, much less its tracks! We clearly can't yet take advantage of 's ability to traverse from to object type.

mutation AddTracksToPlaylist($input: AddItemsToPlaylistInput!) {
addItemsToPlaylist(input: $input) {
code
success
playlist {
id
name
tracks {
name
}
}
}
}

So how can we use the value we do have—the ID for the playlist we modified—to build out the remainder of our AddItemsToPlaylistPayload response?

schema.graphql
type AddItemsToPlaylistPayload {
"Similar to HTTP status code, represents the status of the mutation"
code: Int!
"Indicates whether the mutation was successful"
success: Boolean!
"Human-readable message for the UI"
message: String!
"The playlist that contains the newly added items"
playlist: Playlist
}

We'll tackle this in the next lesson!

Practice

Which of these are good names for mutations based on the recommended conventions above?
In the mutation response type (AddItemsToPlaylistPayload), why is the modified object's return type (Playlist) nullable?
How can we use the input type in our schema?
When creating an input type for a mutation, what naming convention is commonly used?

Key takeaways

  • are write used to modify data.
  • Naming usually starts with a verb that describes the action, such as "add," "delete," or "create."
  • It's a common convention to create a consistent response type for responses.
  • in often require multiple to perform actions. To group arguments together, we use a GraphQL input type for clarity and maintainability.

Up next

In our final lesson, let's complete our response—and return Playlist data!

Previous

Share your questions and comments about this lesson

This course is currently in

beta
. Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.

You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.