danielrearden / graphql-lazyloader

GraphQL directive that adds Object-level data resolvers.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

graphql-lazyloader 🛋

Travis build status Coveralls NPM version Canonical Code Style Twitter Follow

GraphQL directive that adds Object-level data resolvers.

Motivation

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).

Usage

  1. Register LazyLoaderSchemaDirective schema directive class.
  2. Register @lazyLoad schema directive.
  3. Implement __lazyLoad method for data-types that implement @lazyLoad.

Refer to Apollo Using schema directives guide for additional guidance.

Usage Example

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
  }
});

About

GraphQL directive that adds Object-level data resolvers.

License:Other


Languages

Language:TypeScript 99.7%Language:JavaScript 0.3%