GraphQL Resolvers: Best Practices

Mark Stuart
Dec 11, 2018 · 9 min read
From graphql.org

What’s a resolver?

Let’s start off at the same baseline. What’s a resolver?

Resolver definition

Executing queries

To better understand resolvers, you need to know how queries are executed.

  1. Validate — The AST is validated against the schema. Checks for correct query syntax and if the fields exist.
  2. Execute — The runtime walks through the AST, starting from the root of the tree, invokes resolvers, collects up results, and emits JSON.
Query for later reference
Query represented as a tree

Looking closer at resolvers

In the next few sections, we will use JavaScript, but GraphQL servers can be written in almost any language.

Resolvers with four arguments — root, args, context, info
  • args — Arguments provided to the field
  • context — a Mutable object that is provided to all resolvers
  • info — Field-specific information relevant to the query (used rarely)

Default resolvers

Before we continue, it’s worth noting that a GraphQL server has built-in default resolvers, so you don’t have to specify a resolver function for every field. A default resolver will look in root to find a property with the same name as the field. An implementation likely looks like this:

Default resolver implementation

Fetching data in resolvers

Where should we fetch data? What are the tradeoffs with our options?

An event field has a required id argument, returns an Event

Passing data between resolvers

context is a mutable Object that is provided to all resolvers. It’s created and destroyed between every request. It is a great place to store common Auth data, common models/fetchers for APIs and databases, etc. At PayPal, we’re a big Node.js shop w/ infrastructure built on Express, so we store Express’ req in there.

Passing data between resolvers using context. This is not recommended!

Passing data from parent-to-child

The root argument is for passing data from parent resolvers to child resolvers.

Event type with two fields: title and photoUrl
Top-level event resolver fetches data, provides results to title and photoUrl field resolvers
id and title are resolved using default resolvers

Scenario #1: Multi-layered data fetching

Let’s say some requirements come in and you need to display an event’s attendees. We start by adding an attendees field to Event.

Event type with an additional attendees field
event resolver calls two APIs, fetching event details and attendees details
attendees resolver fetches attendees details from the Attendees API

Scenario #2: N+1 Problem

Because data is fetched at a field-level, we run the risk of overfetching. Overfetching and the N+1 problem is a popular topic in the GraphQL world. Shopify has a great article that explains N+1 well.

An events field returns all events.
Query for all events w/ their title and attendees

Fetching data at a field-level

Earlier, we saw that it’s easy to get burned by overfetching with “top-heavy” parent-to-child resolvers.

Fields are responsible for their own data fetching.

Best practices

  • Fetching and passing data from parent-to-child should be used sparingly.
  • Use libraries like dataloader to de-dupe downstream requests.
  • Be aware of any pressure you’re causing on your data sources.
  • Don’t mutate “context”. Ensures consistent, less buggy code.
  • Write resolvers that are readable, maintainable, testable. Not too clever.
  • Make your resolvers as thin as possible. Extract out data fetching logic to re-usable async functions.

Stay tuned!

Thoughts? We would love to hear your team’s best practices and learnings with building resolvers. This is a topic that isn’t often discussed but is important to building long-lived GraphQL APIs.

PayPal Engineering

The PayPal Engineering Blog

Mark Stuart

Written by

Principal Engineer. Leading front-end infrastructure @ PayPal. l33t h4x0r.

PayPal Engineering

The PayPal Engineering Blog