11. Wrapping up
10m

Overview

In the last lesson, we saw how our successfully responds with the code, success, and message properties on the AddItemsToPlaylistPayload. The finish line is in sight!

In this lesson, we will:

  • Complete our and return data about the Playlist we modified
  • Learn about chains
  • Construct lean datafetcher methods by delegating responsibility for follow-up REST requests

Completing our mutation response

Our runs, but we're still not returning a complete Playlist object—this is because our datafetcher only knows about a particular playlist's id. Even though we do have an actual Playlist instance, most of its properties are currently null!

To get more details about the Playlist we've updated, we need to use the id we have to make a separate call: to the /playlists/{playlist_id} endpoint we explored earlier. But there's an important reason why we won't make this call from inside the same addItemsToPlaylist datafetcher method.

Keeping datafetchers lean

Throughout this course, we've praised for the ability to fetch exactly the data we ask for: nothing more, nothing less. And as we saw in the last lesson, we could very well choose to submit an that asks for just three of the four that exist on the AddItemsToPlaylistPayload type.

mutation AddTracksToPlaylist($input: AddItemsToPlaylistInput!) {
addItemsToPlaylist(input: $input) {
code
success
message
}
}

If we ask our addItemsToPlaylist datafetcher method to make an additional call to the /playlists/{playlist_id} endpoint, it will run this logic every time it's invoked. This could lead to a lot of unnecessary network requests! An like the one above might not ask for any playlist data, yet the datafetcher would still go to all the trouble of fetching those details!

We only want to call out to the /playlists/{playlist_id} endpoint when our actually includes the playlist and any of its subfields. To make this work, we'll take advantage of a concept called resolver chains.

Resolver chains

As we learned in an earlier lesson, datafetcher methods—also called resolver functions in other frameworks and languages—are responsible for fetching data for a or set of fields.

Up to now, we've written just three datafetcher methods:

  1. featuredPlaylists
  2. playlist
  3. addItemsToPlaylist

These three methods are examples of datafetchers for on the Query and Mutation type, which is why we annotated them with @DgsQuery and @DgsMutation, respectively.

But we can also define datafetchers for on other : types such as Playlist and Track. These methods give us more granular control over what to return when a particular field is queried.

The "chain" in " chain" comes from the fact that datafetchers, or resolver functions, are called in a particular order to resolve a . When resolving , the results of one "parent" resolver or datafetcher method can be received by another and used to resolve its data.

Let's use the below as an example.

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

The first datafetcher method to be called will be addItemsToPlaylist, which we defined specifically for the Mutation.addItemsToPlaylist . In our schema we said this field returns an AddItemsToPlaylistPayload type, which means we can include any of that type's sub in our .

We already know that the addItemsToPlaylist datafetcher has no trouble returning the values for code, success, and message. But when it reaches playlist, and its name sub, things get a little bit more complicated.

The only playlist data this method has to work with is its id, and it returns an AddItemsToPlaylistPayload object with a Playlist instance that's mostly empty.

AddItemsToPlaylistPayload{
code='200',
success='true',
message='success',
playlist='Playlist{
id='6LB6g7S5nc1uVVfj00Kh6Z',
name='null',
description='null',
tracks='null'
}'
}

We need some other method to be responsible for filling in the playlist details on the AddItemsToPlaylistPayload type. As we already discussed, this shouldn't be the responsibility of the addItemsToPlaylist datafetcher, since it will require a whole new network call to fetch all those additional details about a playlist!

So to account for the missing data on the AddItemsToPlaylistPayload.playlist , we'll define a new datafetcher method that should run anytime this field is included in a .

This means that our chain will look something like this:

Mutation.addItemsToPlaylist() -> AddItemsToPlaylistPayload.playlist()

This means that the results of calling Mutation.addItemsToPlaylist() will be available to AddItemsToPlaylistPayload.playlist(). We'll be able to use these results to know which playlist we need to request additional details for.

Let's define this new datafetcher, and explore how we get access to the results of the previous datafetcher.

The AddItemsToPlaylistPayload.playlist datafetcher

In PlaylistDataFetcher, we'll define a new method. This datafetcher will be called exclusively when a includes the AddItemsToPlaylistPayload.playlist . We want it to be able to receive the results of the datafetcher that runs before it, and use that data to make an additional network request.

datafetchers/PlaylistDataFetcher
public void getPayloadPlaylist() {}

This datafetcher is intended for the AddItemsToPlaylistPayload.playlist , so neither the @DgsQuery nor @DgsMutation annotations apply here: instead, we need to introduce a new annotation: @DgsData.

import com.netflix.graphql.dgs.DgsData;

The annotation needs to know two things: the parentType (the specific ) and the field we want the method to be responsible for.

Applied to the AddItemsToPlaylistPayload.playlist , it looks like this:

@DgsData(parentType="AddItemsToPlaylistPayload", field="playlist")
public void getPayloadPlaylist() {}

This basically says: "Anytime you're trying to resolve the AddItemsToPlaylistPayload.playlist , run me and I'll give you the data you need."

While we're here, let's update the return type for our function, and by default return null.

@DgsData(parentType="AddItemsToPlaylistPayload", field="playlist")
public MappedPlaylist getPayloadPlaylist() {
return null;
}

Resolver chain in action

This method will come into action just as soon as our server attempts to resolve the playlist on AddItemsToPlaylistPayload. But we get a bonus: passed into this method is a big bundle of information from the addItemsToPlaylist datafetcher.

Let's take a closer look at this. All datafetcher methods receive an optional of type DgsDataFetchingEnvironment. We'll import this from DGS at the top of the file, and add it as an to our method called dfe.

// other imports
import com.netflix.graphql.dgs.DgsDataFetchingEnvironment;
// class and methods
@DgsData(parentType="AddItemsToPlaylistPayload", field="playlist")
public MappedPlaylist getPayloadPlaylist(DgsDataFetchingEnvironment dfe) {
return null;
}

At the time that getPayloadPlaylist is called, the root datafetcher for the Mutation.addItemsToPlaylist will have just been called.

We can access the results of this function by reaching into the DgsDataFetchingEnvironment and calling a method called getSource. (And because we know what type the addItemsToPlaylist method returns, we can use that same return type here!)

@DgsData(parentType="AddItemsToPlaylistPayload", field="playlist")
public void getPayloadPlaylist(DgsDataFetchingEnvironment dfe) {
AddItemsToPlaylistPayload payload = dfe.getSource();
return null;
}

In this way, we can see the chain at work!

  1. addItemsToPlaylist returns an instance of AddItemsToPlaylistPayload.
  2. The next datafetcher in the chain can receive and use this data to resolve its part of the .

And that's exactly what the AddItemsToPlaylistPayload.playlist datafetcher needs to do; to request data for an individual playlist, its needs access to the playlist's id. Let's start by accessing the actual Playlist instance that was returned by the addItemsToPlaylist datafetcher.

@DgsData(parentType="AddItemsToPlaylistPayload", field="playlist")
public void getPayloadPlaylist(DgsDataFetchingEnvironment dfe) {
AddItemsToPlaylistPayload payload = dfe.getSource();
Playlist playlist = payload.getPlaylist();
return null;
}

Recall that if the fails, the value of the playlist property will be null. So let's add a check here to make sure that it actually exists before we attempt to extract its id property.

@DgsData(parentType="AddItemsToPlaylistPayload", field="playlist")
public void getPayloadPlaylist(DgsDataFetchingEnvironment dfe) {
AddItemsToPlaylistPayload payload = dfe.getSource();
Playlist playlist = payload.getPlaylist();
if (playlist != null) {
String playlistId = playlist.getId();
}
return null;
}

The only thing left to do is actually make that call to the /playlists/{playlist_id} endpoint!

We've encapsulated this logic in the playlistRequest method in our SpotifyClient class. So, we can add a single line of code to make this call.

if (playlist != null) {
String playlistId = playlist.getId();
return spotifyClient.playlistRequest(playlistId);
}

Let's recompile one last time, and return to Explorer to run our .

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

And in the Variables panel, add:

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

And now, we should see the details for our successful including all of our updated playlist details. Bravo, you've done it!

Key takeaways

  • A chain is the order in which resolver functions are called when resolving a particular .
  • Each datafetcher () called in this chain passes their return value to the next datafetcher in line.
  • We should keep datafetchers as lean as possible, and delegate responsibily where possible when follow-up network requests are necessary.

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! When you're ready to take your API even further, jump into the next course in this series: Federation with Java & DGS.

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.