Overview
It's time to introduce the functions that make it possible to fetch data for fields in our schema.
In this lesson, we will:
- Explore what resolvers are, and what parameters they accept
- Learn how to access data sources from a resolver function
Introducing resolvers
A resolver's mission is to populate the data for a field in your schema.
What exactly is a resolver? A resolver is a function. It has the same name as the field that it populates data for. It can fetch data from any data source, then transforms that data into the shape your client requires.
In the src
directory, we'll start by creating a new resolvers.ts
file.
π srcβ£ π datasourcesβ£ π graphql.d.tsβ£ π helpers.tsβ£ π index.tsβ£ π resolvers.tsβ π schema.graphql
In that file, we'll declare a resolvers
constant, assigning it an empty object for now. Let's export it, because we'll need it in our server config options.
export const resolvers = {};
Our resolvers
object's keys will correspond to our schema's types and fields.
To create a resolver for the featuredListings
field, we'll first add a Query
key to our resolvers
object. The value of that key will be another object that contains the featuredListings
key.
export const resolvers = {Query: {featuredListings: () => {},},};
Notice how our resolver object follows the structure of our schema? featuredListings
is a field on the Query
type, so we define it as a property on the Query
key.
The shape of a resolver
How will our resolvers interact with our data source? This is where a resolver's parameters come in. Resolver functions have a specific signature with four optional parameters: parent
, args
, contextValue
, and info
.
featuredListings: (parent, args, contextValue, info) => {},
Let's go over each parameter briefly to understand what they're responsible for:
- parent:
parent
is the returned value of the resolver for this field's parent. This will be useful when dealing with resolver chains. - args:
args
is an object that contains all GraphQL arguments that were provided for the field by the GraphQL operation. When querying for a specific item (such as a specific listing instead of all listings), in client-land we'll make a query with anid
argument that will be accessible via thisargs
parameter in server-land. - contextValue:
contextValue
is an object shared across all resolvers that are executing for a particular operation. The resolver needs this argument to share state, like authentication information, a database connection, or in our case theRESTDataSource
. - info:
info
contains information about the operation's execution state, including the field name, the path to the field from the root, and more. It's not used as frequently as the others, but it can be useful for more advanced actions like setting cache policies at the resolver level.
We'll explore the first three parameters in this course, starting with contextValue
, the third positional parameter.
Accessing data sources in resolvers
As mentioned above, contextValue
is an object that is shared across all resolvers. It's how resolvers get access to the same shared state, such as data sources.
Let's see this in action by adding parameters to our featuredListings
resolver.
featuredListings: (parent, args, contextValue, info) => {},
The order of the parameters matters here. If we only added one parameter, then the function would consider it as the first optional parameter: parent
. We need the contextValue
, which is the third parameter, to access our data sources.
We don't need the first two parameters, so as a convention, we'll name them with underscores: one underscore for the first (parent
) and two underscores for the second (args
). For the contextValue
, we'll destructure it to access its child object dataSources
. And we can omit the fourth parameter, info
, as we won't use it.
featuredListings: (_, __, { dataSources }) => {},
The dataSources
property is where an instance of our ListingAPI
class will live.
Accessing ListingAPI
We'll hook up the ListingAPI
class to our GraphQL server in the next lesson. For now, we'll assume that dataSources
will contain a property called listingAPI
(written in lowercase by convention, as it's an instance of the ListingAPI
class.) We can call this instance's getFeaturedListings
method right here in our resolverβand the method does the rest of the work for us!
Query: {featuredListings: (_, __, { dataSources }) => {return dataSources.listingAPI.getFeaturedListings();},}
Tip: As a best practice, when working on your resolvers and data sources, try to keep resolver functions as focused as possible. By doing so, you make your API more resilient to future changes. You can safely refactor your data fetching code, or change the source entirely from a REST API to a database, without breaking your API. This also keeps your resolvers readable and easier to understand, which comes in handy as you define more and more of them!
Right about now, you're likely seeing some red squiggly errors in your IDE. TypeScript is complaining that we've given a number of parameters to our featuredListings
resolver, but we haven't specified what type of data they are. Instead, we see that almost everything is inferred as an any
type.
Parameter '_' implicitly has an 'any' type.Parameter '__' implicitly has an 'any' type.Binding element 'dataSources' implicitly has an 'any' type.
Let's find out how to fix these type errors in the next lesson.
Practice
contextValue
parameter useful for?Write a resolver function for the Query.popularPlanets
field. Define the resolver inside the resolvers
object. Use the resolver's contextValue
argument to access the server's dataSources
property, and return the results of calling planetService.getPopularPlanets()
.
Key takeaways
- Resolvers are functions that can be defined for each field in our schema. They accept four optional parameters,
parent
,args
,contextValue
, andinfo
, and they're responsible for returning the data for a particular field when it's queried.
Up next
Our data source and resolver functions are still missing something: type annotations! This means we're seeing some TypeScript errors in our code. We could write these types by hand, but there's a more efficient way. In the next lesson, we'll explore how to generate TypeScript types from our GraphQL schema.
Share your questions and comments about this lesson
Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.