1

I'm working on a GraphQL API which sits in front of a REST service. This REST service is a single endpoint with a lot of complex parameters - rather than put all the parameters on a single GraphQL field, we've logically broken it down into a tree of GraphQL fields and types.

In particular, the bottom of this tree is somewhat dynamic and unbounded. All in, it looks something like this simplified schema:

type Query {
  outer: OuterWrapper!
}

type OuterWrapper {
  inner: InnerWrapper!
}

type InnerWrapper {
  recurse(name: String = ""): RecursiveType!
}

type RecursiveType {
  recurse(name: String = ""): [RecursiveType!]!
  name: String!
}

Currently, we have just one Apollo resolver at the top of the tree (outer), and use the graphql-fields library to process the info parameter. Essentially, we're 'looking ahead' to the children - a popular pattern to optimise backend queries.

This works quite well for us - we map the child fields and parameters into a single REST request, and then map the response back into the correct structure.

However, it does have two limitations:

  1. The graphql-fields response doesn't include the values of default parameters. If I wrote a proper recurse resolver, Apollo would pass in the schema default value for name if it wasn't in the query. I found an alternative library (graphql-parse-resolve-info) that I'm going to switch to, but that's not an Apollo solution, and...
  2. If we throw an error, the path reflects that it occurred in the outer resolver, rather than further down the tree where it would be more accurate, and useful to the user.

Taken together, I'm concerned that I'm going to continue finding things that don't quite work using this kind of structure, and I'm thinking about how I could move away from it.

Is there a way I could incrementally build my single backend REST request using a traditional/fully specified resolver structure?

I could imagine the resolvers building up the query, and storing it in the context - the outer resolver would create the query, and subsequent resolvers would change it as they see fit. Then each resolver could return a promise that's waiting on the REST response. However, I can't see a good way to know when all my resolvers have been called (and made their changes), and thus to fire the REST request to the backend, or for each resolver to know where it sits in the query structure (and hence where to look for data in the REST response).

Is there another approach I haven't considered?

Oli
  • 582
  • 1
  • 6
  • 18

1 Answers1

2

GraphQL's top-down way of executing requests doesn't really lend itself to resolvers building up a query that would be executed once all resolvers are ran. In part, this is because the parent field's resolver has to complete execution before any child field resolvers are called. After all, they need to be called with the value that the parent resolved to.

If you had multiple calls to your datasource, it might make sense to split them across different resolvers. If you only need to make a single call to your datasource, though, doing so at the top level and using "lookahead" is the best approach. graphql-parse-resolve-info is an excellent library to help with that.

The only improvement over what you're doing now might be to move most of the logic for transforming the REST response into the resolvers for some of your non-root fields. In this way, you can gradually transform the parent value passed to each resolver as the execution moves through each "level" of your query.

Daniel Rearden
  • 80,636
  • 11
  • 185
  • 183
  • Thanks - I guess I already knew that the parent field has to complete, but thanks for reminding me. That does put a stop to anything too clever. Can you think of any way at all I can fix the error path? – Oli Apr 11 '20 at 21:15
  • Well if the errors are coming from some step in transforming the REST response, then moving that logic into the resolvers for non-root fields like `recurse` like I suggested would change the error path to the appropriate field. – Daniel Rearden Apr 11 '20 at 22:21
  • They're more likely coming from setting up the REST request in the first place (ie, known invalid inputs). Storing the response in the context and handing back to Apollo to do the traditional resolution certainly would solve the path issue, but seems like a fair amount of overhead. Anyway - will give it some thought. Thanks Daniel! – Oli Apr 11 '20 at 22:28
  • FWIW, you could potentially use `formatError` or `formatResponse` to modify the path after the fact. – Daniel Rearden Apr 11 '20 at 22:30