Invoking GraphQL resolvers multiple times
Imagine that you’re building out a web page that allows students to view a list of their assignments. There’s a pre-existing assignments
resolver on the Classroom
type, so it’s easy enough to get all the data you need:
query { me { classes { assignments { ...AssignmentFields } # ... } }}
From there you quickly build out the frontend components necessary for rendering out the data, merge a PR, and move the ticket to the “Done” column. Simple enough until your product manager comes up to you the next day with a change request. Instead of showing a big long list of all assignments, we want to split the page up into two different lists.
The top of the page will have a list showing all overdue assignments, and beneath that will be a separate list showing everything else.
How would you go about implementing this new UI?
Doing more work in the client
I think the most obvious solution here is to leave the GraphQL query untouched, and to handle partitioning the assignments list inside your component. Something like the following would do the job:
function AssignmentsPage() { const { data } = useQuery(/* ... */); const overdueAssignments = []; const otherAssignments = []; for (const assignment of data) { if (assignment.status === 'OVERDUE') { overdueAssignments.push(assignment); } else { otherAssignments.push(assignment); } } // Render the two lists}
It’s not a bad solution at all, and I myself have written code just like this in the past. If nothing else it is simple, and I have a strong bias towards simple code. But there actually are some real benefits to this excerpt.
The main benefit is that we didn’t need to update our backend. By choosing to implement this logic inside our client instead of the backend, we keep our API nicely decoupled from the implementation details of our assignments page.
If you’re unlucky enough to work somewhere with separate frontend and backend repositories, then this also means you were able to ship faster. You only had to open one PR instead of two, and you also didn’t need to worry about correctly sequencing the deployment of those two PRs.
The downside, of course, is that you’ve had to embed a bunch of extra business logic inside your client. While this partitioning logic isn’t particularly complex, it’s still code that needs to be tested and maintained over time, and the cost of that isn’t zero.
It would be ideal if we could have our cake and eat it too. Such a solution would involve us writing no additional business logic inside neither the client nor the backend.
This might be hard to imagine, but it’s actually possible if you’re using GraphQL.
Doing more work on the server
Here’s an alternate solution:
query { me { classes { otherAssignments: assignments(notStatus: OVERDUE) { ...AssignmentFields } overdueAssignments: assignments(status: OVERDUE) { ...AssignmentFields } # ... } }}
This uses a GraphQL feature called aliases to request the assignments
field twice, each time with different arguments. In most codebases I’ve worked on aliasing has only ever been used to remap awkward field names to something friendlier, but they’re actually a lot more powerful than that.
Without aliasing, we’d be forced to either implement the partitioning logic inside our frontend app like we saw earlier or we’d have to bloat our schema;
type Class { overdueAssignments: [Assignment!]! otherAssignments: [Assignment!]!} # ortype Class { assignments: ClassAssignments! # ...}type ClassAssignments { all: [Assignment!]! notOverdue: [Assignment!]! overdue: [Assignment!]! # ...}
It should be apparent from looking at these schemas that neither are as elegant and simple as simply relying on aliases. Aliases allow us to keep our backend implementation minimal, and maximize the client’s flexibility. Flexibility and orientation around product needs instead of technical details is the name of the game with GraphQL.
Note also that this approach gives us the ability to separately define the selection set for each aliased resolver. Perhaps your requirements are such that you don’t need the dueDate
field to render the “overdue assignments” list, but you do want that bit of data for the list containing the other assignments.
That sort of fine-grained data access is possible to do with aliasing, and impossible to do with our initial client-centric implementation. More than anything else, I think aliasing is a really strong example of GraphQL’s value proposition.
It would have been entirely possible for us to use our original client-centric solution with a RESTful backend. Avoiding the fetch of the dueDate
field for overdue assignments, on the other hand, would have necessitated a backend change.
A well-designed GraphQL schema is capable of supporting a product for a remarkably long time even in the face of changing business requirements. If you find yourself constantly tweaking things, or making breaking changes then it’s a strong signal that you’re building on a shoddy foundation and might need to rethink your graph architecture.
What about performance?
Compared to the client-side implementation you’ll lose a little bit of backend performance going with a naive aliased solution. This is because in the client-side implementation we fetch all of our assignments through a single resolver, which almost always means we’ll be able to get away with only one database query.
Aliasing means we’ll need to call the resolver twice (or even more!) times, and because GraphQL resolvers tend to have a very narrow view of the incoming query a naive implementation would result in one database query per aliased resolver.
Candidly, this is unlikely to be an issue.
If it is a problem for your use case, though, then there are escape hatches. Every single GraphQL server implementation I’ve seen provides information about the current operation’s AST, and by looking at the AST it’s possible to optimize query resolution at a more global level.
This technique is called “lookahead.” Some libraries have better built-in support for lookahead than others. The reference JavaScript GraphQL implementation simply hands off an info
parameter to your resolver function and leaves the task of inspecting the AST to you, whereas ruby-graphql
has lookahead built in as a first-class feature.
Regardless of how ergonomically your framework supports this, it will have support for it one way or another. Scalability issues arising from designing your graph around aliasing are therefore a non-issue.
GraphQL is about flexibility
We’ve looked at a pretty simple case here involving only a single filter, but it should be easy to see how this use of aliasing scales. A fairly generic-seeming assignments
resolver could replace multiple different REST endpoints, all while maintaining GraphQL’s promise of allowing clients to request all of the data they need with only one round trip.
Back in 2016 (I couldn’t find anything more recent), Lee Byron claimed that Facebook had never needed to make a breaking change to their four-year-old GraphQL schema. Features like aliasing are a key reason why this degree of schema evolution is possible, but they only add value when you have engineers on your team who understand them and are able to factor them in to the schema design.
There are a lot of engineers out there who believe GraphQL to be an over-engineered solution to fetching data from a backend. And the thing is that they aren’t entirely wrong. I’ve dealt with a lot of bad graphs in my time that absolutely should have just been REST APIs. It’s also worth noting the sheer bloat in the broader GraphQL ecosystem; it’s absurd that the Apollo client—a library that just POSTs strings to a backend—adds 44.3 kB to your gzipped bundle size.
But when you have people on your team who really get GraphQL and understand how to squeeze out every bit of value from it, and aren’t using the less-than-ideal parts of the ecosystem? It’s actually a rather pleasant way to build backends.