Lift-off part 3 "Resolver Chains" Help with Using "/module/{id}" Instead of "/track/{id}/modules"

While studying “Lift-off part 3 - Resolver Chains”
For Fetching Modules

type Track{
   ...
   modules: [Module!]!
}

I decided to use dataSource API as

  getModule(moduleId){
    return this.get(`module/${moduleId}`);
  }

Instead of

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

From REST API of track, we get list of moduleId → ex [ "l_0", "l_1", "l_2", ]
So we have to call getModule() for each moduleId

But with above approach I am having difficulty writing resolves.
I manage to make it work with below two solution. But I think this is not the correct way!

Solution 1:

  Track: {
    modules: async ({modules},_,{dataSources}) => {
      const result = [];
      for(moduleId of modules){
        result.push(await dataSources.trackAPI.getModule(moduleId));
      }
      return result;
    }
  },

Problem: Takes more time

Solution 2:

  Track: {
    modules: ({modules}) => {
      return modules.map(m => ({id:m}));
    }
  },
  Module:{
    title: async ({id},_,{dataSources}) => {
      const {title} = await dataSources.trackAPI.getModule(id);
      return title;
    },
    length: async ({id},_,{dataSources}) => {
      const {length} = await dataSources.trackAPI.getModule(id);
      return length;
    }
  }

Problem: For each Module Fields there is code repetition.

Please Help what is the correct way to solve this.

Hi! May I ask why you want to use getModule rather than getTrackModules? With getModule, you will inevitably run into performance issues because you’re opening yourself up to an n+1 problem. If your track has 20 modules, you’ll have to make 20 additional REST requests with getModule (which is what you’re doing with your solution 1). Your solution 2 is actually even more problematic because it makes an additional request per module per field.

So to answer your question, as far as I can tell, there really is no “correct” way to solve the problem using getModule. With getTrackModules however, you’ll need to perform only a single REST request and have all the data needed to furnish your resolvers.

1 Like

I Agree, we have to make REST call for each module instead of only one.
But lets assume we don’t have track/${trackId}/modules , so there no way around it.

About ‘Solution 2’: I don’t know how dataSource cashing works, or it’s making the calls in parallel (solution 1 can also be improved to use parallel REST call), but I found solution 2 was substantially faster than solution 1.

Yeah, in solution 2, many of these requests will be performed simultaneously because the resolvers run concurrently. The REST API is still hit more often, though.

If you’re forced to use getModule, your first solution is the way to go, but you can run your requests simultaneously by wrapping them in a Promise.all.

Track: {
    modules: ({ modules }, variables, { dataSources }) => (
        Promise.all(modules.map((moduleId) => (
            dataSources.trackAPI.getModule(moduleId)
        ))
    ) 
},

To reiterate: This is not a good practice in a real-world application, because of the sheer number of REST requests. The API might be rate-limited, in which case your resolver will stop working at some point. If it’s not, the API will likely become slow or completely unresponsive. You could work around that by caching the results of these API requests on your server and delivering the cached results on subsequent requests to your graphql API, though of course you’d open a whole can of cache management worms.

2 Likes

Thanks, That was helpful
I was hoping there was a way we could, write resolver for whole type instead of individual fields
That might be look like this:

  Track: {
    modules: async ({modules},_,{dataSources}) => {
      return modules.map(m => ({id:m}));
    }
  },
  Module: ({id},_,{dataSources}) => {
    return dataSources.trackAPI.getModule(id);
  }

And dataSource would take care of cashing and repeated calls.