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 - Query for playlist and track details in a single operation
Building the Track
type
As we learned in the lesson on SDL syntax, fields on GraphQL types don't have to return a basic scalar type—they can also return other object types!
For instance, we can add a tracks
field 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: Stringtracks: # 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 entity—in other words, we should make it its own GraphQL type called Track
.
Open up the schema.graphql
file.
We'll update the Playlist
type to include a tracks
field that returns a non-nullable list of Track
types. We'll also add a description for the field while we're here.
"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.
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:
"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!
/** 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 query that calls for a playlist's tracks
details.
query Playlist($playlistId: ID!) {playlist(id: $playlistId) {namedescriptiontracks {idname}}}
And make sure that in the Variables panel, our $playlistId
variable is still set.
{ "playlistId": "6LB6g7S5nc1uVVfj00Kh6Z" }
But when we run the query... 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?
{"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!
{"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
resolver 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
!
// 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
resolver is burdened with a lot of extra logic it might not always require. Take, for instance, a query that doesn't ask for a playlist's tracks
.
query Playlist($playlistId: ID!) {playlist(id: $playlistId) {namedescription}}
Even though this query doesn't include the playlist.tracks
field, our resolver 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 query featuredPlaylists
and all of their tracks? We'd have to duplicate all of the track-specific logic to the featuredPlaylists
resolver as well!
query FeaturedPlaylists {featuredPlaylists {nametracks {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 resolvers thin and concerned exclusively with the data they need to provide. Let's talk about resolver chains.
Key takeaways
- An object type's fields can return scalar types or other object types.
- When a field on an object type returns another object type, we can write complex queries that traverse from one object to another—no follow-up queries necessary!
Up next
Let's see how we can keep our resolvers lean and focused in the next lesson.
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.