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 GraphQL middleware (see Usage Example).
graphql-lazyloader
is added using graphql-middleware
import {
ApolloServer,
gql,
} from 'apollo-server';
import {
makeExecutableSchema,
} from '@graphql-tools/schema';
import {
applyMiddleware,
} from 'graphql-middleware';
import {
createLazyLoadMiddleware,
} from 'graphql-lazyloader';
const lazyLoadMiddleware = createLazyLoadMiddleware({
Person: ({id}) => {
return getPerson(id);
},
});
const typeDefs = gql`
type Query {
person(id: ID!): Person!
}
type Person {
id: ID!
givenName: String!
familyName: String!
}
`;
const resolvers = {
Query: {
person: () => {
return {
id: '1',
};
},
},
};
const schema = makeExecutableSchema({
resolvers,
typeDefs,
});
const schemaWithMiddleware = applyMiddleware(
schema,
lazyLoadMiddleware,
);
const server = new ApolloServer({
schema: schemaWithMiddleware,
});