4. Writing query resolvers

How a GraphQL query fetches data

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

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 fetches the requested data from the appropriate .

A function returns one of the following:

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

The resolver function signature

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

fieldName: (parent, args, context, info) => data;
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 s, the s we define will mostly use context. It enables our s to share instances of our LaunchAPI and UserAPI s. To see how that works, let's get started creating some s.

Define top-level resolvers

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

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

module.exports = {
Query: {
launches: (_, __, { dataSources }) =>
launch: (_, { id }, { dataSources }) =>
dataSources.launchAPI.getLaunchById({ launchId: id }),
me: (_, __, { dataSources }) => dataSources.userAPI.findOrCreateUser(),

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

Regarding the function s 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 argument, however, because our schema's launch field takes an id argument.)
  • 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 s. By keeping s 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 s, let's add them to our server. Add the highlighted lines to 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({
dataSources: () => ({
launchAPI: new LaunchAPI(),
userAPI: new UserAPI({ store }),
server.listen().then(() => {
Server is running!
Listening on port 4000
Explore at https://studio.apollographql.com/sandbox

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


Run test queries

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

Paste the following query into the s panel:

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

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 query? This correspondence is a fundamental feature of GraphQL.

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

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

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

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

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

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

"id": "60"

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

Define other resolvers

You might have noticed that the test queries we ran above included several s that we haven't even written s for. But somehow those queries still ran successfully! That's because Apollo Server 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) s of our , a default does exactly what we want it to. Let's define a custom for a 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 query specifies LARGE or SMALL for the size .

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

// 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 , Launch.mission.

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

// 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 (
}) || []

You might be wondering how our server knows the identity 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:

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 query)
  • 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:

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
const launches = paginateResults({
results: allLaunches,
return {
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 query in Apollo Sandbox:

query GetLaunches {
launches(pageSize: 3) {
launches {
mission {

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


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