11. Adding the Track type
10m

Overview

We're getting our playlist data back from the REST API, but we haven't met the needs of our mockup. Our playlist objects—as far as we can see—don't actually contain any music! Let's fix that.

In this lesson, we will:

  • Introduce the Track type to our schema
  • for playlist and track details in a single

Building the Track type

As we learned in the lesson on syntax, on types don't have to return a basic type—they can also return other !

For instance, we can add a tracks to our Playlist type—but what's the appropriate return type?

"A curated collection of tracks designed for a specific activity or mood."
type Playlist {
"The ID for the playlist."
id: ID!
"The name of the playlist."
name: String!
"Describes the playlist, what to expect and entices the user to listen."
description: String
tracks: # What type should this be?
}

Putting our business glasses on, we can see how details for a track object—such as name, duration, and whether or not it's explicit—would come in handy. Multiple tracks could appear in multiple playlists, and we might want different views that show us all of the tracks in a single playlist. For these reasons, we need to think of a "track" as a standalone —in other words, we should make it its own type called Track.

Open up the schema.graphql file.

We'll update the Playlist type to include a tracks that returns a non-nullable list of Track types. We'll also add a description for the while we're here.

schema.graphql
"A curated collection of tracks designed for a specific activity or mood."
type Playlist {
"The ID for the playlist."
id: ID!
"The name of the playlist."
name: String!
"Describes the playlist, what to expect and entices the user to listen."
description: String
"The tracks of the playlist."
tracks: [Track!]!
}

Now, let's actually define what a Track looks like.

Mockup design of a playlist's tracks

We'll concern ourselves with just a few properties: id, name, durationMs, explicit, and uri. In the schema.graphql file, add the new Track type shown below:

schema.graphql
"A single audio file, usually a song."
type Track {
"The ID for the track."
id: ID!
"The name of the track"
name: String!
"The track length in milliseconds."
durationMs: Int!
"Whether or not the track has explicit lyrics (true = yes it does; false = no it does not OR unknown)"
explicit: Boolean!
"The URI for the track, usually a Spotify link."
uri: String!
}

And from the schema's perspective, our work is done!

After saving our changes, the codegen process should have run automatically. We can check types.ts to make sure that our new Track type has been added!

types.ts
/** A single audio file, usually a song. */
export type Track = {
__typename?: "Track";
/** The track length in milliseconds. */
durationMs: Scalars["Int"]["output"];
/** Whether or not the track has explicit lyrics (true = yes it does; false = no it does not OR unknown) */
explicit: Scalars["Boolean"]["output"];
/** The ID for the track. */
id: Scalars["ID"]["output"];
/** The name of the track */
name: Scalars["String"]["output"];
/** The URI for the track, usually a Spotify link. */
uri: Scalars["String"]["output"];
};

Testing the Track type

Jump back into the Explorer. We'll try running a that calls for a playlist's tracks details.

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

And make sure that in the Variables panel, our $playlistId is still set.

{ "playlistId": "6LB6g7S5nc1uVVfj00Kh6Z" }

But when we run the ... kaboom! A big error appears in the Response panel rather than the data we want. But what's the problem?

"Expected Iterable, but did not find one for field \"Playlist.tracks\"."

Revisiting the JSON response

To solve this mystery, we need to return to our REST API and take a closer look at the shape of our playlist object. Let's inspect that /playlists/{playlist_id} endpoint, passing in the following ID to get our response.

6LB6g7S5nc1uVVfj00Kh6Z

What properties do you see on the playlist object?

Response from /playlists/{playlist_id}
{
"collaborative": false,
"description": "Infuse flavor into your kitchen. This playlist merges zesty tunes with culinary vibes, creating a harmonious background for your cooking escapades. Feel the synergy between music and the zest of your creations.",
"id": "6LB6g7S5nc1uVVfj00Kh6Z",
"name": "Zesty Culinary Harmony",
// ... other properties
"tracks": { ... }
}

At first glance, it looks like everything we need is there—id, name, description, and even tracks. But when we drill into the tracks property, we'll see something we don't expect—it's not an array of track objects at all, but another object!

Inspecting the 'tracks' key
{
"tracks": {
"href": "https://...",
"limit": 100,
"next": null,
"items": [
{
"track": {
"id": "2epbL7s3RFV81K5UhTgZje",
"name": "Lemon Tree",
"uri": "spotify:track:2epbL7s3RFV81K5UhTgZje"
// other track properties
}
}
/* additional track objects */
]
}
}

We don't find our actual track objects (or at least the data we want!) until we drill even further into this object's items property.

Furthermore, each object contained in items has a track property we need to delve into. How do we get around this mismatch between our REST API responses, and the shape of the data we specified in our schema?

Retrieving tracks data

It's clear that we need to do some digging through our JSON response to grab the tracks for a particular playlist. The only question is: where should that extra logic live?

One option would be to add quite a bit more code to our Query.playlist function. Instead of returning the response from the REST API directly, we'd need to iterate through all of the individual track details, finally returning them on a new object that contained all of the playlist properties we need: id, name, description, and tracks!

One possible implementation to return tracks
// Original implementation
// playlist: (_, { id }, { dataSources }) => {
// return dataSources.spotifyAPI.getPlaylist(id);
// },
playlist: async (_, { id }, { dataSources }) => {
const {
id: playlistId,
name,
description,
tracks: { items = [] } = {},
} = await dataSources.spotifyAPI.getPlaylist(id);
const newTrackItems = items.map(({ track }: { track: Track }) => {
const { id, name, duration_ms, explicit, uri } = track;
return { id, name, durationMs: duration_ms, explicit, uri };
});
return { id: playlistId, name, description, tracks: newTrackItems };
},

This technically works. But with this approach, our playlist is burdened with a lot of extra logic it might not always require. Take, for instance, a that doesn't ask for a playlist's tracks.

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

Even though this doesn't include the playlist.tracks , our function would still go to all the trouble of locating that data, plucking it from nested JSON objects, and returning it.

Furthermore, what happens when we try to featuredPlaylists and all of their tracks? We'd have to duplicate all of the track-specific logic to the featuredPlaylists as well!

query FeaturedPlaylists {
featuredPlaylists {
name
tracks {
name
}
}
}

Now we're duplicating code, and worse, we're executing code even when it's not required.

Fortunately, there's a better approach. It lets us keep our thin and concerned exclusively with the data they need to provide. Let's talk about resolver chains.

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. Then, when a attempts to execute that field, it will consult the function that we've defined for it to understand exactly how to find that data.

To create a function whose sole responsibility it is to return tracks data for a given Playlist object, we can create a new entry in our resolvers 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
}
},

Next, we can include the logic that digs into the JSON and plucks out the properties we want.

resolvers.ts
Playlist: {
tracks: ({ tracks }) => {
const { items = [] } = tracks;
return items.map(({ track }) => track);
},
},

Unfortunately, we'll see another error!

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.

Key takeaways

  • An 's can return types or other object types.
  • When a on an returns another , we can write complex queries that traverse from one object to another—no follow-up queries necessary!
  • 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.