15. Wrapping up
10m

Overview

It's clear that our REST API's response doesn't give us everything we need to satisfy the AddItemsToPlaylistPayload return type immediately. We've got a playlist's id value, but we actually need to return the entire playlist object in order to match the return type in our schema.

In this lesson, we will:

  • Delegate responsibility for the AddItemsToPlaylistPayload.playlist to its own function
  • Apply the concept of chains to use data from the parent in follow-up requests
  • Update our TypeScript mappers to keep our type definitions accurate

Revisiting resolver chains

As it is right now, the Mutation.addItemsToPlaylist returns the code, success, and message properties as outlined in our schema.

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 field is still null!
}

To return an actual playlist object, we could have this same make an additional call to our SpotifyAPI's getPlaylist method, passing in the playlist ID that we have.

For fun, (don't copy this) here's what that might look like:

Mutation: {
async addItemsToPlaylist(_, { input }, { dataSources }) {
try {
const response = await dataSources.spotifyAPI.addItemsToPlaylist(input);
if (response.snapshot_id) {
const playlist = await dataSources.spotifyAPI.getPlaylist(response.snapshot_id);
return {
code: 200,
success: true,
message: "Tracks added to playlist!",
playlist
};
} else {
throw Error("snapshot_id property not found");
}
} catch (e) {
return {
code: 500,
success: false,
message: `Something went wrong: ${e}`,
playlist: null
};
}
},
},

But doing so would actually overburden this ; it would always fetch additional playlist information, even if the did not ask for it.

Instead, this is a great opportunity to put our knowledge of chains to work. We can separate the logic we need to fetch a playlist object by its ID into a new resolver: one that takes responsibility specifically for the AddItemsToPlaylistPayload.playlist !

Resolving the AddItemsToPlaylistPayload.playlist field

Here's the plan.

  1. We'll first define a new function, specific to the AddItemsToPlaylistPayload.playlist .
  2. We'll ensure that the Mutation.addItemsToPlaylist passes the updated playlistId to the next in the chain, rather than a null playlist property.
  3. Following the chain, the AddItemsToPlaylistPayload.playlist will receive this playlistId on its parent .
  4. Using the playlistId, the AddItemsToPlaylistPayload.playlist can handle all the special logic needed to reach out to the REST API and retrieve additional playlist details!

Let's get to it!

Step 1: The new AddItemsToPlaylistPayload.playlist resolver

Let's add a new AddItemsToPlaylistPayload entry to our resolvers object, with a property called playlist.

resolvers.ts
AddItemsToPlaylistPayload: {
playlist: (parent, args, contextValue, info) => {
return null;
},
},

By defining this function, we've told our server that it's no longer the Mutation.addItemsToPlaylist 's responsibility to resolve the playlist on the AddItemsToPlaylistPayload object it returns.

Step 2: Passing playlistId to the next resolver

We need to access the playlist's id in the AddItemsToPlaylistPayload.playlist . How do we do that? Using the parent !

A diagram showing the resolver chain between Mutation.addItemsToPlaylist and AddItemsToPlaylistPayload.playlist

The Mutation.addItemsToPlaylist returns the following AddItemsToPlaylistPayload object, which means the AddItemsToPlaylistPayload.playlist can access these properties from its parent .

Mutation.addItemsToPlaylist return object
return {
code: 200,
success: true,
message: "Tracks added to playlist!",
playlist: null, // We don't have this value yet
};

Right now, that playlist property the returns is pretty useless. And because it's no longer the responsibility of this resolver to resolve the playlist , let's pass along some data that the next in the chain can use instead: the playlistId.

Replace the playlist property with playlistId. The response.snapshot_id holds the ID of the playlist we updated, so we'll pass that in here.

resolvers.ts
return {
code: 200,
success: true,
message: "Tracks added to playlist!",
- playlist: null,
+ playlistId: response.snapshot_id
};

Down in our catch block, we'll also change the playlist property to playlistId instead.

resolvers.ts
return {
code: 500,
success: false,
message: `Something went wrong: ${e}`,
- playlist: null,
+ playlistId: null
};

Now, the playlistId value will be passed on into the next in the chain. Here's how our Mutation.addItemsToPlaylist should look now.

Step 3: Retrieving playlistId from parent

Now, inside of our AddItemsToPlaylistPayload.playlist , we can log out the parent to see if all of our values have arrived from the previous in the chain.

resolvers.ts
AddItemsToPlaylistPayload: {
playlist: (parent, args, contextValue, info) {
console.log(parent);
return null;
}
}

Let's try our again, this time adding playlist and a few of its sub: id, name, and description. Jump back into Sandbox and run the following .

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

And in the Variables panel:

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

Back in our terminal, we'll see that the parent we logged out now contains all the values that were returned from the previous in the chain!

{
code: 200,
success: true,
message: 'Tracks added to playlist!',
playlistId: '6LB6g7S5nc1uVVfj00Kh6Z'
}

Step 4: Retrieve playlist data using playlistId

The playlistId property is just what we need. Inside of our AddItemsToPlaylistPayload.playlist , we'll pluck off the playlistId key from the parent . We can also replace args with _, since we won't be using it, and destructure contextValue for its dataSources property. (We can also safely remove the info parameter!)

resolvers.ts
playlist: ({ playlistId }, _, { dataSources }) => {
return null;
},

We've already defined a method in our SpotifyAPI class that accepts a playlist's ID, and returns a playlist, so we can make a new call to our 's getPlaylist method, passing in the playlistId.

resolvers.ts
playlist: ({ playlistId }, _, { dataSources }) => {
return dataSources.spotifyAPI.getPlaylist(playlistId);
},

When we try to run our code, however, we'll see a TypeScript error.

Property 'playlistId' does not exist on type 'Omit<AddItemsToPlaylistPayload, "playlist">
& { playlist?: PlaylistModel; }'

Resolving the type errors

TypeScript knows from our schema that the Mutation.addItemsToPlaylist returns a AddItemsToPlaylistPayload type. So, it expects the Mutation.addItemsToPlaylist resolver function to return exactly that!

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

Following the chain logic, that means it expects the next resolver in the chain—AddItemsToPlaylistPayload.playlist—to receive that same AddItemsToPlaylistPayload as its parent .

So, it's a rude shock for TypeScript to discover that we're not actually returning an object that matches its expectations. Instead of a playlist , it finds playlistId.

{
code: 200,
success: true,
message: 'Tracks added to playlist!',
playlistId: '6LB6g7S5nc1uVVfj00Kh6Z' // ❌ TypeScript doesn't love this property being here!
}

The Mutation.addItemsToPlaylist is no longer responsible for returning the AddItemsToPlaylistPayload.playlist in our schema, but TypeScript doesn't know this. From its perspective, we're making a big mistake: referencing a property that doesn't actually exist.

We ran into this issue earlier when working with playlist and track objects from our REST API that didn't exactly align with the shape the types took in our schema. We can solve the problem here the same way as before: by adding a new mapper for the AddItemsToPlaylistPayload type!

Let's jump back into models.ts. We'll add a new definition, called AddItemsToPlaylistPayloadModel.

models.ts
export type AddItemsToPlaylistPayloadModel = {
// TODO
};

We'll give it the three properties that it has in common with the AddItemsToPlaylistPayload type in our schema—code, success, and message—along with the playlistId string.

models.ts
export type AddItemsToPlaylistPayloadModel = {
code: number;
success: boolean;
message: string;
playlistId: string;
};

Now we can update our codegen.ts file's mappers property to use this type.

codegen.ts
config: {
contextType: "./context#DataSourceContext",
mappers: {
Playlist: "./models#PlaylistModel",
Track: "./models#TrackModel",
AddItemsToPlaylistPayload: "./models#AddItemsToPlaylistPayloadModel",
},
},

Phew! Let's restart our server so we can start completely fresh.

npm run dev

Back in Sandbox, we'll run that same again.

mutation AddTracksToPlaylist($input: AddItemsToPlaylistInput!) {
addItemsToPlaylist(input: $input) {
code
message
success
playlist {
id
name
tracks {
id
name
}
}
}
}
{
"input": {
"playlistId": "6LB6g7S5nc1uVVfj00Kh6Z",
"uris": [
"spotify:track:4iV5W9uYEdYUVa79Axb7Rh",
"spotify:track:1301WleyT98MSxVHPZCA6M"
]
}
}

And with that, we've got all of the data we expect! We can see the same code, success and message from before, along with the new playlist-specific fields.

Key takeaways

  • We can define an individual function for any in our schema. This allows us to execute additional data-fetching logic only when a field is included in a or .
  • By using mappers, we can equip TypeScript with a picture of what the data our are working with actually looks like. This is helpful when response from need manipulation or traversal, or we need to pass objects between that don't match the types in our schema.

Journey's end

Task!

You've built a API! You've got a working jam-packed with playlists and tracks using a REST API as a . You've written queries and , and learned some common GraphQL conventions along the way. You've explored how to use GraphQL , , and input types in your schema design. Take a moment to celebrate; that's a lot of learning!

But the journey doesn't end here! Put your newfound skills to the test in Growing your GraphQL API with TypeScript & Apollo Server, a hands-on lab where you'll implement a new feature from start to finish.

And when you're ready to take your API even further, jump into the next course in this series: Federation with TypeScript & Apollo Server.

Thanks for joining us in this course; we hope to see you in the next one!

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.