Overview
Let's finish up our mutation!
In this lesson, we will:
- Learn about common conventions for mutation arguments and the
input
type - Learn how to use the mutation response to handle successful and failed actions
Mutation arguments & input
We've used a GraphQL argument before in the Query.playlist
field — we passed in one argument called id
.
type Query {playlist(id: ID!): Playlist}
For the addItemsToPlaylist
mutation, we'll need more than one argument.
From the documentation, we need the following parameters:
playlist_id
- The ID of the playlist, as astring
position
- Aninteger
, zero-indexed, where we want to insert the track(s)uris
- A comma-separatedstring
ofuri
values corresponding to the tracks we want to add
We could use all three as arguments, but it's a good practice to use GraphQL input types as arguments for a field.
The input
type in a GraphQL schema is a special object type that groups a set of arguments together, and can then be used as an argument to another field.
As a naming convention, we add the Input
suffix to a type's name and give it the same name as the mutation it's associated with.
In our case, we can name our argument to the addItemsToPlaylist
field as AddItemsToPlaylistInput
. Let's go ahead and bring it to life!
The AddItemsToPlaylistInput
type
Let's create a new file called add_items_playlist_input.py
under the api/types
folder and define a class called AddItemsToPlaylistInput
.
import strawberryclass AddItemsToPlaylistInput:...
We'll also need to define this class as a GraphQL input
type using the @strawberry.input
decorator. Similar to the other Strawberry functions we've used so far, we can provide this function a name
and description
to use for the GraphQL schema.
Let's go ahead and apply the @strawberry.input
decorator to the class.
@strawberry.inputclass 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, 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 GraphQL schema. Remember, your GraphQL API does not need to match your REST API exactly!
playlist_id: strawberry.ID = strawberry.field(description="The ID of the playlist.")uris: list[str] = strawberry.field(description="A list of Spotify URIs to add.")
Updating the resolver
Now let's make sure our resolver knows about this input. Back in api/mutation.py
, we can add it to the parameters of our function. We'll name this resolver argument input
.
Note that the input
parameter can be named anything, like playlist_tracks
or tracks_input
for example! We recommend collaborating with your team to decide on naming conventions. Using input
as the GraphQL argument name is a common convention.
def add_items_to_playlist(self,input: AddItemsToPlaylistInput) -> AddItemsToPlaylistPayload:...
Since we're here, let's also make the function async and add the info
parameter. We'll need this later on to access the spotify_client
.
async def add_items_to_playlist(self,input: AddItemsToPlaylistInput,info: strawberry.Info) -> AddItemsToPlaylistPayload:...
Time to use our service and our input type! In the body of the resolver function, we'll use the add_tracks_to_playlist.asyncio
method of our mock_spotify_rest_api_client
package.
client = info.context["spotify_client"]await add_tracks_to_playlist.asyncio(playlist_id=input.playlist_id, uris=",".join(input.uris), client=client)
Using the method signature as a guide, we can add the corresponding value from the input
argument for playlist_id
and uris
. For the last parameter, we'll need to tweak the format a little. Since input.uris
is a list of str
types and the method expects a str
type of comma-separated values, we'll use ",".join
.
That's just the nature of the REST endpoint we're working with. To retrieve the playlist
object, we do need to make a follow-up call using the get_playlist
method we've used previously.
But where should we make that call? If we include it in this resolver, that means an additional REST call even when the playlist
field isn't included in the GraphQL mutation!
We've already been through this situation before, where we made use of the resolver chain. However, in this case, we're going to keep the REST call included in this resolver. Thinking about the client app's needs, if they are adding tracks to a playlist, they will most likely include the the playlist and its list of tracks in the GraphQL operation! They want to see the results of their mutation after all.
Let's keep going. Inside the add_items_to_playlist
resolver, we'll make a call to the get_playlist
function.
data = await get_playlist.asyncio(playlist_id=input.playlist_id, client=client)playlist = Playlist(id=data.id, name=data.name, description=data.description)
Look familiar? It's the code we used in our Query.playlist
resolver.
Lastly, let's update the return value in our AddItemsToPlaylistPayload
instance, specifically replacing the hard-coded playlist with the playlist
variable instead.
return AddItemsToPlaylistPayload(code=200,success=True,message="Successfully added items to playlist.",playlist=playlist,)
That's the success path taken care of! Now what about when something fails and an error pops up? Let's wrap our code so far in a try
block and except
any Exception
s that get thrown.
try:await add_tracks_to_playlist.asyncio(playlist_id=input.playlist_id, uris=",".join(input.uris), client=client)data = await get_playlist.asyncio(playlist_id=input.playlist_id, client=client)playlist = Playlist(id=data.id, name=data.name, description=data.description)return AddItemsToPlaylistPayload(code=200,success=True,message="Successfully added items to playlist.",playlist=playlist,)except Exception as e:...
If something goes wrong, our return value should look a little different. We'll still return a AddItemsToPlaylistPayload
type, but this time, the code will be 500
, the success status will be false
and we'll return whatever message the exception has. The playlist
parameter will be set to None
.
return AddItemsToPlaylistPayload(code=500,success=False,message=str(e),playlist=None,)
Finally, let's not forget all the imports we need at the top of the file:
from .types.add_items_to_playlist_input import AddItemsToPlaylistInputfrom mock_spotify_rest_api_client.api.playlists import (add_tracks_to_playlist,get_playlist,)
Explorer time!
With the server running with our latest changes, let's start a new workspace tab and build our mutation from scratch. With the addition of the input
argument, when we add the addItemsToPlaylist
field, the GraphQL operation looks a little different:
mutation AddItemsToPlaylist($input: AddItemsToPlaylistInput!) {addItemsToPlaylist(input: $input) {}}
We can also see the Variables section with an input
property in the JSON object:
{"input": null}
Let's start to fill in this input
object. In the Documentation panel, click into the input
field and add the three fields from AddItemsToPlaylistInput
. Explorer will automatically update the variables for you.
{"input": {"playlistId": null,"uris": null}}
All we need to do is update those null
s! We'll use a new playlist ID and add an array with one URI in it. This is all mock data, so you can put whatever you want in there as a placeholder. It doesn't need to actually exist in the Spotify database.
{"input": {"playlistId": "6LB6g7S5nc1uVVfj00Kh6Z","uris": ["ASongAboutGraphQL"]}}
Lastly, let's fill in the rest of the mutation with the fields we need. Add the code
, success
and message
fields. Then for a playlist, add its details and its tracks!
mutation AddItemsToPlaylist($input: AddItemsToPlaylistInput!) {addItemsToPlaylist(input: $input) {codemessagesuccessplaylist {idnametracks {idname}}}}
That's a big mutation, press play to run! We should see our data come back successfully. At the bottom of the tracks
list, we'll see the id
set as the uri
we passed in our variable, and the name as well.
Note: Does your response look a little different? Because the REST API is shared across all Odyssey learners, you may see more or less tracks in your response. This is likely because other learners have added their own tracks to the playlist. We also reset the data regularly to keep things clean.
Amazing! Now check out what happens if you update the playlist ID to a value that doesn't exist.
{"input": {"playlistId": "DoesNotExist","uris": ["ASongAboutGraphQL"]}}
When we run the mutation again, we'll still receive data, but this time we've got an error code, a failure message and a null
playlist.
Our mutation is working! 🎉
Practice
input
type in our schema?Key takeaways
- Mutations in GraphQL often require multiple arguments to perform actions. To group arguments together, we use a GraphQL
input
type for clarity and maintainability. - Strawberry uses
@strawberry.input
to mark Python classes as GraphQL input types. - We can access the
input
argument in the same way as any other GraphQL argument in the resolver function.
Conclusion
Bravo, you've done it, you've built a GraphQL API! You've got a working GraphQL server jam-packed with playlists and tracks using the Spotify REST API as a data source. You've written queries and mutations, and learned some common GraphQL conventions along the way. You've explored how to use GraphQL arguments, variables, and input types in your schema design. Take a moment to celebrate; that's a lot of learning!
And if you've got any requests for what you'd like to see next, let us know below!
Share your questions and comments about this lesson
This course is currently in
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.