GraphQL directive that adds Object-level data resolvers.
Several years ago I read GraphQL Resolvers: Best Practices (2018), an article written by PayPal team, that changed my view about where / when data resolution should happen.
Let's start with an example GraphQL schema:
type Query {
person(id: ID) Person!
}
type Person {
id: ID!
givenName: String!
familyName: String!
}
A typical GraphQL server uses "top-heavy" (parent-to-child) resolvers, i.e. in the above example, Query.person
is responsible for fetching data for Person
object. It may look something like this:
{
Query: {
person: (root, args) => {
return getPerson(args.id);
},
},
};
PayPal team argues that this pattern is prone to data over-fetching. Instead, they propose to move data fetching logic to every field of Person
, e.g.
{
Query: {
person: (root, args) => {
return {
id: args.id,
};
},
},
Person: {
givenName: async ({id}) => {
const {
givenName,
} = await getPerson(id);
return givenName;
},
familyName: async ({id}) => {
const {
familyName,
} = await getPerson(id);
return givenName;
},
},
};
It is important to note that the above example assume that getPerson
is implemented using a DataLoader pattern, i.e. data is fetched only once.
According to the original authors, this pattern is better because:
- This code is easy to reason about. You know exactly where [givenName] is fetched. This makes for easy debugging.
- This code is more testable. You don't have to test the [person] resolver when you really just wanted to test the [givenName] resolver.
To some, the [getPerson] duplication might look like a code smell. But, having code that is simple, easy to reason about, and is more testable is worth a little bit of duplication.
For this and other reasons, I became a fan ❤️ of this pattern and have since implemented it in multiple projects. However, the particular implementation proposed by PayPal is pretty verbose. graphql-lazyloader
abstracts the above logic into a single @lazyLoad
directive and an Object-level __lazyLoad
resolver (see Usage Example).
- Register
LazyLoaderSchemaDirective
schema directive class. - Register
@lazyLoad
schema directive. - Implement
__lazyLoad
method for data-types that implement@lazyLoad
.
Refer to Apollo Using schema directives guide for additional guidance.
import {
ApolloServer,
gql,
} from 'apollo-server';
import {
LazyLoaderSchemaDirective,
} from 'graphql-lazyloader';
const typeDefs = gql`
directive @lazyLoad on OBJECT
type Query {
person(id: ID!): Person!
}
type Person @lazyLoad {
id: ID!
givenName: String!
familyName: String!
}
`;
const server = new ApolloServer({
typeDefs,
resolvers: {
Person: {
__lazyLoad: ({id}) => {
return getPerson(id);
},
},
Query: {
person: () => {
return {
id: '1',
};
},
},
},
schemaDirectives: {
lazyLoad: LazyLoaderSchemaDirective
}
});