5. Resolver chains

📥 Retrieving module details

To retrieve the module details for a given track, we'll need to use the GET track/:id/modules endpoint in our REST API.

Let's try out the endpoint, giving it our track ID c_0 again.

The response we get is an array of modules with the details that we need.

Screenshot of REST API docs with the GET track/:id/modules endpoint

Our track page needs the id, title and length for now. Let's update our TrackAPI to call this endpoint.

💾 Updating the RESTDataSource

In the server/src/datasources folder, open up the track-api.js file.

Let's create a method called getTrackModules. It takes a trackId as a parameter. Inside, it'll make a get call to the track/${trackId}/modules endpoint and return the results.

getTrackModules(trackId) {
return this.get(`track/${trackId}/modules`);

Now we can use this new datasource method in our resolver. Let's hop back to the resolvers.js file in the server/src folder.

First let's determine where exactly we can place our call to get the details of a track's modules. We know that we need that information in our track query. But should we add the call here in the track resolver?

// EXAMPLE ONLY - should we add the getTrackModules call here in the track resolver?
track: async (_, {id}, {dataSources}) => {
// get track details
const track = dataSources.trackAPI.getTrack(id);
// get module details for the track
const modules = await dataSources.trackAPI.getTrackModules(id);
// shape the data in the way that the schema expects it
return {...track, modules};

We certainly could. This is a similar problem to the one we faced in Lift-off II, when we were trying to figure out where to retrieve the author details for each track. We ended up extracting that logic to a different resolver: Track.author.

Which of these are reasons why we extracted author-fetching logic to a different resolver in Lift-off II? (Hint: Keep reading if you need a refresher and come back to this question when you're ready!)

For the exact same reasons, we'll want to do the same here for our module details. Let's dig deeper into why.

⛓️ Resolver chains

We'll see this particular pattern often in GraphQL. When we write a query, we often have nested objects and fields. Take this query as an example:

query track(id: ‘c_0') {
author {

Remember that a resolver is responsible for populating the data for a field in your schema. It retrieves data from a data source. In our case, we have a resolver for our track field that retrieves data from the REST API /track/:id endpoint.

Illustration showing a Query.track resolver retrieving data from the track/:id REST endpoint

It returns some of the properties our query expects, such as title. It doesn't have the author name, but it does have the authorId, which is an ID we can use for another endpoint in our REST API, the author/:id endpoint.

We could tell this same resolver to make the call to the author/:id endpoint, and then put together the results in the shape our query expects.

However, this means that the resolver is a bit overworked! If the query doesn't ask for author information, the resolver would still do all of these unnecessary steps.

Illustration showing a Query.track resolver looking overworked as it retrieves data 2 REST endpoints

So instead of putting all of that work on our poor Query.track resolver, we can create another resolver function for Track.author. This resolver is responsible for retrieving author information for a specific track. With that, we're forming a resolver chain!

Illustration showing a Query.track resolver linked with the Track.author resolver in a resolver chain

And remember the first parameter in a resolver-- the parent? parent refers to the returned data of the preceding resolver function in the chain! That's how we get access to the authorId from the track object, in our Track.author resolver. Additionally, the Track.author resolver will only be called when the query asks for that field!

Illustration showing the Query.track resolver passing the data to the Track.author resolver as a `parent` parameter

This pattern keeps each resolver readable, easy to understand, and more resilient to future changes.


✍️ Adding a new resolver to the chain

Back to our specific problem, let's use this concept of resolver chains to create a resolver for Track.modules.

We'll add this resolver right below Track.author in the resolvers.js file:

modules: ({id}, _, {dataSources}) => {
return dataSources.trackAPI.getTrackModules(id);

We'll destructure the first parameter to retrieve the id property from the parent, that's the id of the track. We don't need the args parameter, so that can be an underscore, and then destructure the third context parameter for the dataSources property.

Inside, we can return the results of calling our dataSources.trackAPI.getTrackModules method, passing in the id for the track.

And that's it for our resolver! With that, we've got our resolvers, our datasource and our schema all updated and ready to take on our new query.