12. Resolver chains
5m

Overview

We recommend keeping focused on retrieving only the data it's responsible for. We want to reduce duplicate code and keep network calls to a minimum, making requests only when necessary.

In this lesson, we will:

  • Learn about chains
  • Learn about the parent of a

Resolver chains

A resolver chain is the order in which functions are called when resolving a particular . It can contain a sequential path as well as parallel branches.

Let's take an example from our project. This GetPlaylist retrieves the name of a playlist.

query GetPlaylist($playlistId: ID!) {
playlist(id: $playlistId) {
name
}
}

When resolving this , the will first call the Query.playlist() function, then the Playlist.name() function, which returns a string type and ends the chain.

Resolver chain in a diagram

Each passes the value it returns to the next function down, using the resolver's parent . Hence: the chain!

Remember, a has access to a number of parameters. So far, we've used contextValue (to access our SpotifyAPI ) and arg (to get the id for a playlist). parent is another such parameter!

In the example above, Query.playlist() returns a Playlist object, which the Playlist.name() function would receive as its parent .

Let's look at another .

query GetPlaylistTracks($playlistId: ID!) {
playlist(id: $playlistId) {
name
tracks {
uri
}
}
}

This time, we've added more and asked for each playlist's list of tracks, specifically their uri values.

Our chain grows, adding a parallel branch.

Resolver chain in a diagram

Note that since Playlist.tracks returns a list of potentially multiple tracks, this might run more than once to retrieve each track's URI.

Following the trail of the , Playlist.tracks() would have access to Playlist as the parent, Track.uri() would have access to the Track object as the parent.

If our didn't include the tracks (like the first example we showed), then the Playlist.tracks() function would never be called!

Implementing the Playlist.tracks resolver

So far, we've defined functions exclusively for that exist on our Query type. But we can actually define a function for any in our schema.

Let's create a function whose sole responsibility is to return tracks data for a given Playlist object.

Jump into resolvers.ts. Here, we'll add a new entry, just below the Query object, called Playlist.

resolvers.ts
export const resolvers: Resolvers = {
Query: {
// query resolvers, featuredPlaylists and playlist
},
Playlist: {
// TODO
},
};

Inside of the Playlist object, we'll define a new function called tracks. Right away we'll return null so that TypeScript continues to compile as we explore our function's parameters.

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

We know the value of the parent —by following the chain, we know that this resolver will receive the Playlist object that it's attempting to return tracks for. (We won't need the args, contextValue, or info parameters here, so we'll remove them from the function signature.)

Let's log out the value of parent.

resolvers.ts
Playlist: {
tracks: (parent) => {
console.log(parent);
return null;
}
},

Then we'll return to Sandbox to run a that will call this function.

query Playlist($playlistId: ID!) {
playlist(id: $playlistId) {
name
description
tracks {
name
}
}
}

And in the Variables panel:

{ "playlistId": "6LB6g7S5nc1uVVfj00Kh6Z" }

When we run this , the Response panel will show "Cannot return null for non-nullable field Playlist.tracks.", but that's ok, we're more interested in investigating parent right now.

We can see from the value we logged out in the terminal that parent is the Playlist object that we queried for—along with all of its properties. Now within the Playlist.tracks , let's clean up our log and return statements, and destructure parent for its tracks property.

resolvers.ts
Playlist: {
tracks: ({tracks}) => {
// TODO
}
},

Another path

There's another situation we need to consider here: we can just a singular playlist with the Query.playlist , or we can access a list of featured playlists through Query.featuredPlaylists.

However, if we review the result from the GET /browse/featured-playlists endpoint, we'll see that it does not actually contain full-fledged track objects. Instead, each playlist contains a tracks key with just two properties: href and total.

"tracks": {
"href": "string",
"total": 5
}

Instead of a list of track objects (similar to the list that the earlier GET /playlists/{playlist_id} endpoint returns), we get a single object with two properties: total, the total number of tracks available, and href, a URL for the endpoint where we can retrieve the full list of track objects.

This is a common pattern in REST APIs. Imagine if the response did include the full list of track objects. That would make for a very large response, to have a list of playlists and a list of tracks for each playlist.

Instead, if the playlist we're resolving tracks for only contains these two properties, we'll need to make one more additional call to the REST API. In this case, to the GET /playlists/{playlist_id}/tracks endpoint. This means that we'll need to access the playlist's id.

Let's update our Playlist.tracks to destructure the parent for a playlist's id as well.

tracks: ({ id, tracks }) => {
// TODO
};

We need our function to handle these two situations.

  1. If the playlist contains tracks.items, map through items and return each object's track property.
  2. If the playlist does not contain tracks.items, make a follow-up request using the playlist's id to get the list of tracks.

We can handle that first scenario right away. We'll use a ternary to first check if items exists, and if so, we'll map through the tracks and return them.

resolvers.ts
return tracks.items
? tracks.items.map(({track}) => track)
: // make a call to the REST API for tracks

To make a follow-up call to our REST API, we'll first need to give our SpotifyAPI class a new method. We'll call this method getTracks, and give it a playlistId .

We expect this method to return a Promise that resolves to an array of Track types. (Be sure to import the Track type from ./types at the top!)

SpotifyAPI
getTracks(playlistId: string): Promise<Track[]> {
// TODO
}

Let's build our call to the GET /playlists/{playlist_id}/tracks endpoint. We'll pass our this.get call a type which represents the shape that our response will initially take; from the REST API we know that we'll get back an object with an items property, which contains a list of Track types. Because we'll await the results of this call, let's make our function async.

SpotifyAPI
async getTracks(playlistId: string): Promise<Track[]> {
const response = await this.get<{ items: { track: Track }[] }>(`playlists/${playlistId}/tracks`)
}

Now we can dig into the response object for its items and return them.

SpotifyAPI
async getTracks(playlistId: string): Promise<Track[]> {
const response = await this.get<{ items: { track: Track }[] }>(`playlists/${playlistId}/tracks`)
return response?.items?.map(({track}) => track) ?? [];
}

Great - now let's return to resolvers.ts. We'll update our function to destructure its third positional , contextValue, for the dataSources property. Then, we'll complete our ternary in case track.items does not exist: and we'll make a call to the getTracks method, passing in our playlist's id.

resolvers.ts
tracks: ({ tracks, id }, _, { dataSources }) => {
return tracks.items
? tracks.items.map(({ track }) => track)
: dataSources.spotifyAPI.getTracks(id);
};

Unfortunately, there are some errors appearing everywhere we try to access items.

Property 'items' does not exist on type 'Track[]'.

TypeScript is mad about something we've done here. Let's take a closer look in the next lesson.

Practice

Use the following schema and to answer the multiple choice question.

An example schema
type Query {
featuredArtists: [Artist!]!
}
type Artist {
name: String!
album: Album!
}
type Album {
name: String!
totalTracks: Int!
releaseDate: String
}
An example query operation
query GetFeaturedArtistsAlbumNames {
featuredArtists {
album {
name
}
}
}
Which of the following accurately describes the resolver chain for the GetFeaturedArtistsAlbumNames query above?

Key takeaways

  • A chain is the order in which resolver functions are called when resolving a particular .

Up next

Our Track type exists in our schema, but differences between our types and our backend data objects are causing TypeScript to throw some errors. Let's see how we can get around this, and inform TypeScript about what our backend data looks like, in the next lesson.

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.