Overview
So far, we've only been working with one type of GraphQL operation: queries. These are read-only operations to retrieve data. To modify data, we need to use another type of GraphQL operation: mutations, which are write operations.
In this lesson, we will learn about common conventions for designing mutations and mutation responses
Mutations
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 astring
(required)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
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 GraphQL?
Designing mutations
Let's start with our schema.
For mutation names, we recommend starting with a verb that describes the specific action of our update operation (such as add, delete, or create), followed by whatever data the mutation acts on.
For the return type of a mutation, we could return the object type the mutation is acting on. However, we recommend following a consistent Response
type for mutation responses.
Mutation responses
We'll need to account for any partial errors that might occur and return helpful information to the client. We recommend adding three common fields to all mutation responses:
code
: anint
that refers to the status of the response, similar to an HTTP status code.success
: abool
flag that indicates whether all the updates the mutation was responsible for succeeded.message
: astring
to display information about the result of the mutation 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.
Then, we'll also have a field for the object type we're mutating (Playlist
, in our case). In certain mutations, this could be multiple objects!
As for naming conventions, return types usually end with the word Payload
or Response
.
The AddItemsToPlaylistPayload
type
Following the best practices we covered, we'll name our mutation AddItemsToPlaylist
. Let's first create the return type for this mutation: AddItemsToPlaylistPayload
.
Under api/types
, we'll create a new file called api/types/add_items_to_playlist_payload.py
.
Inside, we'll create the AddItemsToPlaylistPayload
class, applying the @strawberry.type
decorator.
import strawberry@strawberry.typeclass AddItemsToPlaylistPayload:...
Then, we'll add the properties for code
, success
and message
.
code: int = strawberry.field(description="Similar to HTTP status code, represents the status of the mutation.")success: bool = strawberry.field(description="Indicates whether the mutation was successful.")message: str = strawberry.field(description="Human-readable message for the UI.")
The object we're mutating in this case is a Playlist
type, which is nullable because it's possible that something can go wrong in the mutation!
playlist: Playlist | None = strawberry.field(description="The playlist that contains the newly added items.")
We'll also need to import the Playlist
type at the top of the file:
from .playlist import Playlist
A new entry point: Mutation
Under api
, we'll create a new file called mutation.py
. In this file, we'll define a new class called Mutation
:
import strawberry@strawberry.typeclass Mutation:...
Next, we'll write the resolver function for our AddItemsToPlaylist
field. It will return the AddItemsToPlaylistPayload
type. We'll add a description for it too.
We'll keep the function in Pythonic snake_case
, but remember that Strawberry will automatically convert it to the GraphQL camelCase
in the schema.
def add_items_to_playlist(self) -> AddItemsToPlaylistPayload:...
To define this as a resolver, we'd usually use the @strawberry.field
decorator, but this time, we're going to use a new function: strawberry.mutation
.
The strawberry.mutation
defines a field for a mutation. It works in the same way as strawberry.field
. We can apply it as a decorator and provide a GraphQL description.
@strawberry.mutation(description="Add one or more items to a user's playlist.")def add_items_to_playlist(self) -> AddItemsToPlaylistPayload:...
We'll hard-code the results of the resolver for now. Small steps! We'll create a new AddItemsToPlaylistPayload
instance, passing in 200
for code
, true
for the success
status, a successful message
and a hard-coded Playlist
object.
return AddItemsToPlaylistPayload(code=200,success=True,message="Successfully added items to playlist.",playlist=Playlist(id="6Fl8d6KF0O4V5kFdbzalfW",name="Sweet Beats & Eats",description=None,),)
Lastly, we'll import the AddItemsToPlaylistPayload
and Playlist
types at the top of our file:
from .types.add_items_to_playlist_payload import AddItemsToPlaylistPayloadfrom .types.playlist import Playlist
Our GraphQL server needs to know about this new entry point to the schema. Much like the Query
type, we'll need to set the mutation type in our schema.
Open up the api/schema.py
file and import the Mutation
class at the top.
from .mutation import Mutation
Then, we'll pass it into strawberry.Schema
.
schema = strawberry.Schema(query=Query, mutation=Mutation)
Save all these changes and restart the server.
Explorer time!
Time to take our mutation for a spin! Back in Sandbox Explorer, we'll create a new workspace tab.
In the Documentation panel, let's navigate back to the root of our schema. Beside Query
, we can see the Mutation
type.
Let's add the addItemsToPlaylist
field and all the subfields inside it. For the playlist
subfield, select the name
for now.
mutation AddItemsToPlaylist {addItemsToPlaylist {codemessagesuccessplaylist {name}}}
Run the mutation.
{"data": {"addItemsToPlaylist": {"code": "200","message": "Added items to playlist successfully","success": true,"playlist": {"name": "Sweet Beats & Eats"}}}}
Much like query responses, our mutation response followed the same shape as our mutation!
Practice
AddItemsToPlaylistPayload
), why is the modified object's return type (Playlist
) nullable?Key takeaways
- Mutations are write operations used to modify data.
- Naming mutations 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 mutation responses.
Up next
We'll learn best practices for mutation inputs and return real data.
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.