Best practise to combine multiple rest calls to populate 1 graphQL type in apollo-server

Viewed 1128

I have graphql User type that needs information from multiple REST api's and different servers. Basic example: get the user firstname from rest domain 1 and get lastname from rest domain 2. Both rest domain have a common "userID" attribute.

A simplefied example of my resolver code atm:

user: async (_source, args, { dataSources }) => {
  try {
    const datasource1 = await dataSources.RESTAPI1.getUser(args.id);
    const datasource2 = await dataSources.RESTAPI2.getUser(args.id);

    return { ...datasource1, ...datasource2 };
  } catch (error) {
    console.log("An error occurred.", error);
  }
  return [];
}

This works fine for this simplefied version, but I have 2 problems with this solution: first, IRL there is a lot of logic going into merging the 2 json results. Since some field are shared but have different data (or are empty). So it's like cherry picking both results to create a combined result.

My second problem is that this is still a waterfall method. First get the data from restapi1, when thats done call restapi2. Basicly apollo-server is reintroducing rest-waterfall-fetch graphql tries to solve.

Keeping these 2 problems in mind.. Can I optimise this piece of code or rewrite is for better performance or readability? Or are there any packages that might help with this behavior?

Many thanks!

3 Answers

With regard to performance, if the two calls are independent of one another, you can utilize Promise.all to execute them in parallel:

const [dataSource1,dataSource2] = await Promise.all([
  dataSources.RESTAPI1.getUser(args.id),
  dataSources.RESTAPI2.getUser(args.id),
])

We normally let GraphQL's default resolver logic do the heavy lifting, but if you're finding that you need to "cherry pick" the data from both calls, you can return something like this in your root resolver:

return { dataSource1, dataSource2 }

and then write resolvers for each field:

const resolvers = {
  User: {
    someField: ({ dataSource1, dataSource2 }) => {
      return dataSource1.a || dataSource2.b
    },
    someOtherField: ({ dataSource1, dataSource2 }) => {
      return someCondition ? dataSource1.foo : dataSource2.bar
    },
  }
}

Assuming your user resolver returns type User forsake...

type User {
  id: ID!
  datasource1: RandomType
  datasource1: RandomType
}

You can create individual resolvers for each field in type User, this can reduce the complexity of the user Query, to only the requested fields.

query {
 user {
  id
  datasource1 {
    ...
  }
 }
}
const resolvers = {
    Query: {
        user: () => {
            return { id: "..." };
        }
    },
    User: {
        datasource1: () => { ... },
        datasource2: () => { ... } // i wont execute
    }
};

datasource1 & datasource2 resolvers will only execute in parallel, after Query.user executes.

For parallel call.

const users = async (_source, args, { dataSources }) => {
  try {
    const promises = [
      dataSources.RESTAPI1,
      dataSources.RESTAPI2
    ].map(({ getUser }) => getUser(args.id));
    const data = await Promise.all(promises);
    return Object.assign({}, ...data);
  } catch (error) {
    console.log("An error occurred.", error);
  }
  return [];
};
Related