4. Writing query resolvers
15m

How a GraphQL query fetches data

We've designed our schema and configured our , but our server doesn't know how to use its to populate schema . To solve this, we'll define a collection of .

A resolver is a function that's responsible for populating the data for a single field in your schema. Whenever a client queries for a particular , the for that field fetches the requested data from the appropriate .

A function returns one of the following:

  • Data of the type required by the 's corresponding schema (string, integer, object, etc.)
  • A promise that fulfills with data of the required type

The resolver function signature

Before we start writing , let's cover what a resolver function's signature looks like. Resolver functions can optionally accept four positional :

fieldName: (parent, args, context, info) => data;
ArgumentDescription
parentThis is the return value of the resolver for this field's parent (the resolver for a parent field always executes before the resolvers for that field's children).
argsThis object contains all GraphQL arguments provided for this field.
contextThis object is shared across all resolvers that execute for a particular operation. Use this to share per-operation state, such as authentication information and access to data sources.
infoThis contains information about the execution state of the operation (used only in advanced cases).

Of these four , the we define will mostly use context. It enables our to share instances of our LaunchAPI and UserAPI . To see how that works, let's get started creating some .

Define top-level resolvers

As mentioned above, the for a parent always executes before the resolvers for that field's children. Therefore, let's start by defining resolvers for some top-level fields: the fields of the Query type.

As src/schema.js shows, our schema's Query type defines three : launches, launch, and me. To define for these , open server/src/resolvers.js and paste the code below:

server/src/resolvers.js
module.exports = {
Query: {
launches: (_, __, { dataSources }) =>
dataSources.launchAPI.getAllLaunches(),
launch: (_, { id }, { dataSources }) =>
dataSources.launchAPI.getLaunchById({ launchId: id }),
me: (_, __, { dataSources }) => dataSources.userAPI.findOrCreateUser(),
},
};

As this code shows, we define our in a map, where the map's keys correspond to our schema's types (Query) and (launches, launch, me).

Regarding the function above:

  • All three functions assign their first positional (parent) to the _ as a convention to indicate that they don't use its value.

  • The launches and me functions assign their second positional (args) to __ for the same reason.

    • (The launch function does use the args , however, because our schema's launch takes an id .)
  • All three functions do use the third positional (context). Specifically, they destructure it to access the dataSources we defined.

  • None of the functions includes the fourth positional (info), because they don't use it and there's no other need to include it.

As you can see, these functions are short! That's possible because most of the logic they rely on is part of the LaunchAPI and UserAPI . By keeping thin as a best practice, you can safely refactor your backing logic while reducing the likelihood of breaking your API.

Add resolvers to Apollo Server

Now that we have some , let's add them to our server. Add the highlighted lines to src/index.js:

server/src/index.js
const { ApolloServer } = require("apollo-server");
const typeDefs = require("./schema");
const { createStore } = require("./utils");
const resolvers = require("./resolvers");
const LaunchAPI = require("./datasources/launch");
const UserAPI = require("./datasources/user");
const store = createStore();
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => ({
launchAPI: new LaunchAPI(),
userAPI: new UserAPI({ store }),
}),
});
server.listen().then(() => {
console.log(`
Server is running!
Listening on port 4000
Explore at https://studio.apollographql.com/sandbox
`);
});

By providing your map to like so, it knows how to call resolver functions as needed to fulfill incoming queries.

Task!

Run test queries

Let's run a test on our server! Start it up with npm start and return to Apollo Sandbox (which we previously used to explore our schema).

Paste the following into the panel:

# We'll cover more about the structure of a query later in the tutorial.
query GetLaunches {
launches {
id
mission {
name
}
}
}

Then, click the Run button. Our server's response appears on the right. See how the structure of the response object matches the structure of the ? This correspondence is a fundamental feature of .

Now let's try a test that takes a GraphQL argument. Paste the following and run it:

query GetLaunchById {
launch(id: "60") {
id
rocket {
id
type
}
}
}

This returns the details of the Launch object with the id 60.

Instead of hard-coding the like the above, these tools let you define variables for your . Here's that same using a instead of 60:

query GetLaunchById($id: ID!) {
launch(id: $id) {
id
rocket {
id
type
}
}
}

Now, paste the following into the tool's panel:

QUERY_VARIABLES
{
"id": "60"
}

Feel free to experiment more with running queries and setting before moving on.

Define other resolvers

You might have noticed that the test queries we ran above included several that we haven't even written for. But somehow those queries still ran successfully! That's because defines a default resolver for any you don't define a custom for.

A default function uses the following logic:

default resolver logic

For most (but not all) of our schema, a default does exactly what we want it to. Let's define a custom resolver for a schema field that needs one, Mission.missionPatch.

This has the following definition:

type Mission {
# Other field definitions...
missionPatch(size: PatchSize): String
}

The for Mission.missionPatch should return a different value depending on whether a specifies LARGE or SMALL for the size .

Add the following to your map in src/resolvers.js, below the Query property:

server/src/resolvers.js
// Query: {
// ...
// },
Mission: {
// The default size is 'LARGE' if not provided
missionPatch: (mission, { size } = { size: 'LARGE' }) => {
return size === 'SMALL'
? mission.missionPatchSmall
: mission.missionPatchLarge;
},
},

This obtains a large or small patch from mission, which is the object returned by the default for the parent in our schema, Launch.mission.

Now that we know how to add for types besides Query, let's add some for of the Launch and User types. Add the following to your map, below Mission:

server/src/resolvers.js
// Mission: {
// ...
// },
Launch: {
isBooked: async (launch, _, { dataSources }) =>
dataSources.userAPI.isBookedOnLaunch({ launchId: launch.id }),
},
User: {
trips: async (_, __, { dataSources }) => {
// get ids of launches by user
const launchIds = await dataSources.userAPI.getLaunchIdsByUser();
if (!launchIds.length) return [];
// look up those launches by their ids
return (
dataSources.launchAPI.getLaunchesByIds({
launchIds,
}) || []
);
},
},

You might be wondering how our server knows the id of the current user when calling functions like getLaunchIdsByUser. It doesn't yet! We'll fix that in the next chapter.

Paginate results

Currently, Query.launches returns a long list of Launch objects. This is often more information than a client needs at once, and fetching that much data can be slow. We can improve this 's performance by implementing pagination.

Pagination ensures that our server sends data in small chunks. We recommend cursor-based pagination for numbered pages, because it eliminates the possibility of skipping an item or displaying the same item more than once. In cursor-based pagination, a constant pointer (or cursor) is used to keep track of where to start in the data set when fetching the next set of results.

Let's set up cursor-based pagination. In src/schema.js, update Query.launches to match the following, and also add a new type called LaunchConnection like so:

server/src/schema.js
type Query {
launches( # replace the current launches query with this one.
"""
The number of results to show. Must be >= 1. Default = 20
"""
pageSize: Int
"""
If you add a cursor here, it will only return results _after_ this cursor
"""
after: String
): LaunchConnection!
launch(id: ID!): Launch
me: User
}
"""
Simple wrapper around our list of launches that contains a cursor to the
last item in the list. Pass this cursor to the launches query to fetch results
after these.
"""
type LaunchConnection { # add this below the Query type as an additional type.
cursor: String!
hasMore: Boolean!
launches: [Launch]!
}

Now, Query.launches takes in two parameters (pageSize and after) and returns a LaunchConnection object. The LaunchConnection includes:

  • A list of launches (the actual data requested by a )
  • A cursor that indicates the current position in the data set
  • A hasMore boolean that indicates whether the data set contains any more items beyond those included in launches

Open src/utils.js and check out the paginateResults function. This is a helper function for paginating data from the server.

Now, let's update the necessary functions to accommodate pagination. Import paginateResults and replace the launches function in src/resolvers.js with the code below:

server/src/resolvers.js
const { paginateResults } = require("./utils");
module.exports = {
Query: {
launches: async (_, { pageSize = 20, after }, { dataSources }) => {
const allLaunches = await dataSources.launchAPI.getAllLaunches();
// we want these in reverse chronological order
allLaunches.reverse();
const launches = paginateResults({
after,
pageSize,
results: allLaunches,
});
return {
launches,
cursor: launches.length ? launches[launches.length - 1].cursor : null,
// if the cursor at the end of the paginated results is the same as the
// last item in _all_ results, then there are no more results after this
hasMore: launches.length
? launches[launches.length - 1].cursor !==
allLaunches[allLaunches.length - 1].cursor
: false,
};
},
},
};

Let's test the cursor-based pagination we just implemented. Restart your server with npm start and run this in :

query GetLaunches {
launches(pageSize: 3) {
launches {
id
mission {
name
}
}
}
}

Thanks to our pagination implementation, the server should only return three instead of the full list.

Task!

That takes care of the for our schema's queries! Next, let's write resolvers for our schema's .

Previous