Odyssey

Migrating to AS4

IntroductionUpdating dependenciesUpdating the codeWrap-up
3. Updating the code
3m

Updating the code

We've got the packages we need, so let's start swapping them in. A good place to start is src/index.js—where our AS3 server code is defined!

src/index.js
const { ApolloServer } = require("apollo-server");
const typeDefs = require("./schema");
const resolvers = require("./resolvers");
const TrackAPI = require("./datasources/track-api");
async function startApolloServer(typeDefs, resolvers) {
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => {
return {
trackAPI: new TrackAPI(),
};
},
});
const { url, port } = await server.listen();
console.log(`
🚀 Server is running
🔉 Listening on port ${port}
đź“­ Query at ${url}
`);
}
startApolloServer(typeDefs, resolvers);

New imports

First, let's update the line requiring in ApolloServer so that it uses our new @apollo/server package.

src/index.js
- const { ApolloServer } = require("apollo-server");
+ const { ApolloServer } = require("@apollo/server")

As part of the AS4 upgrade, the @apollo/server package consists of a number of new, smaller packages, and we can access those utilities and functions to customize how our server runs. We'll still use the ApolloServer class we've required, but we need an additional function: startStandaloneServer.

We can access startStandaloneServer through a new import, which targets the /standalone directory within the @apollo/server package.

src/index.js
const { ApolloServer } = require("@apollo/server");
const { startStandaloneServer } = require("@apollo/server/standalone");
const typeDefs = require("./schema");
// other imports...

Configuring our server

startStandaloneServer

To integrate the startStandaloneServer function with our existing server implementation, we need to make a few updates.

The first line we can remove is our server.listen() call. We'll no longer listen to our server instance directly, but rather use our startStandaloneServer function instead.

src/index.js
// ... imports
async function startApolloServer(typeDefs, resolvers) {
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => {
return {
trackAPI: new TrackAPI(),
};
},
});
- const { url, port } = await server.listen();
console.log(`
🚀 Server is running
🔉 Listening on port ${port}
đź“­ Query at ${url}
`);
}
startApolloServer(typeDefs, resolvers);

In its place, we can await the results of our startStandaloneServer call, passing it our initialized server constant.

src/index.js
// ... imports
async function startApolloServer(typeDefs, resolvers) {
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources: () => {
return {
trackAPI: new TrackAPI(),
};
},
});
await startStandaloneServer(server);
console.log(`
🚀 Server is running
🔉 Listening on port ${port}
đź“­ Query at ${url}
`);
}
startApolloServer(typeDefs, resolvers);

From the results of this call, we can pull out the url that our server is running on.

src/index.js
const { url } = await startStandaloneServer(server);

Apollo Server 4 introduced the startStandaloneServer function, which accepts an instance of the ApolloServer class and sets many helpful, out-of-the-box configurations to get your server up and running quickly. It's a great option for a basic Apollo Server that doesn't need any additional tooling, which is why we've chosen it for this side quest. We'll continue to explore the updates this function entails in the next section, but you can visit the official Apollo documentation on startStandaloneServer to learn more now.

The context function

startStandaloneServer can take a second argument, which is a configuration object that lets us customize some of our server's features. Let's go ahead and add the curly braces for that configuration object.

src/index.js
const { url } = await startStandaloneServer(server, {
// configuration details
});

Previously, we defined dataSources directly in our ApolloServer class constructor. The data we returned from that function became available to our resolvers on their third positional argument, context.

In AS4, that definition moves to the startStandaloneServer configuration object under a key called context, which is an asynchronous function.

Let's add that key now:

src/index.js
const { url } = await startStandaloneServer(server, {
context: async () => {},
});

Within this function, we'll return an object. This is the object that our resolvers receive on their third argument.

src/index.js
const { url } = await startStandaloneServer(server, {
context: async () => {
return {};
},
});

Note: In AS3, we referred to the third positional argument that resolver functions receive as context. As a matter of convention in AS4, we now refer to that argument as contextValue. This is because when we call the context function in the server configuration, the object it returns arrives on our resolvers' third positional argument.

Now, let's remove the dataSources definition defined in our ApolloServer instantiation. Instead, we can define a dataSources key in the object returned by the context function. We'll set dataSources to an object as well.

src/index.js
// ... imports
const server = new ApolloServer({
typeDefs,
resolvers,
- dataSources: () => {
- return {
- trackAPI: new TrackAPI(),
- };
- },
});
const { url } = await startStandaloneServer(server, {
context: async () => {
return {
+ dataSources: {}
}
}
})

Finally, we can move the trackAPI key, and the instantiation of the TrackAPI class, to the dataSources object.

src/index.js
const { url } = await startStandaloneServer(server, {
context: async () => {
return {
dataSources: {
trackAPI: new TrackAPI(),
},
};
},
});

Note: In AS3, dataSources was defined as a function that returned an object containing the various data sources we wanted our resolvers to have access to. In AS4, dataSources is set to an object containing these data source properties instead. Make sure that your dataSources object matches the implementation above!

Hooking up the cache

When we used Apollo Server 3, an initialize function was called for each DataSource class created, passing in the Apollo Server's cache and context to make them available for use. In Apollo Server 4, we'll handle this ourselves. We don't need our TrackAPI class to have access to any additional context information (like a user authentication token), so instead we'll make sure we're passing it just the cache from our server initialization.

First, we need to get access to the cache. In AS4, this is made available as a read-only variable on our server. Let's start by destructuring our server to get access to cache.

src/index.js
const { url } = await startStandaloneServer(server, {
context: async () => {
const { cache } = server;
return {
dataSources: {
trackAPI: new TrackAPI(),
},
};
},
});

Next, we'll update our instantiation of the TrackAPI class, passing it an object that contains cache.

src/index.js
const { url } = await startStandaloneServer(server, {
context: async () => {
const { cache } = server;
return {
dataSources: {
trackAPI: new TrackAPI({ cache }),
},
};
},
});

Excellent! That's our data source configured in AS4! Our track data will now be available to our resolver functions.

Clean up

Here's the current state of our server:

src/index.js
const { ApolloServer } = require("@apollo/server");
const { startStandaloneServer } = require("@apollo/server/standalone");
const typeDefs = require("./schema");
const resolvers = require("./resolvers");
const TrackAPI = require("./datasources/track-api");
async function startApolloServer(typeDefs, resolvers) {
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
context: async () => {
const { cache } = server;
return {
dataSources: {
trackAPI: new TrackAPI({ cache }),
},
};
},
});
console.log(`
🚀 Server is running
🔉 Listening on port ${port}
đź“­ Query at ${url}
`);
}
startApolloServer(typeDefs, resolvers);

We've done the bulk of the code updates, but there's another tweak we need to make. Right now, our console.log statement is looking for a port variable. As you can see, we're no longer pulling out this value from our server.

Let's instead define a port variable at the top of our file, and future-proof our implementation to read from process.env.PORT if the environment our server is running in sets one by default. If no PORT variable exists, we can set it to a default value of 4000.

const port = process.env.PORT || 4000;

We can ask our server to read in this port value by defining a listen key inside the startStandaloneServer config object.

As the value of the listen, we'll set an object with a port key. And because the name of the key matches the name of the value we want to pass in, we can use the ES6 shorthand syntax and simply set both the key and the value at once with port.

src/index.js
+ const port = process.env.PORT || 4000;
async function startApolloServer(typeDefs, resolvers) {
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
context: async () => {
const { cache } = server;
return {
dataSources: {
trackAPI: new TrackAPI({ cache }),
},
};
},
+ listen: { port },
});
}

That's it for the server updates... but if you run npm start in your terminal, you'll still see some errors! Other parts of our app are still using the older packages from AS3. Let's tackle those next.

Previous
Next

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.

              Apollo Server

              An open-source library for server-side GraphQL operation handling. It can be used as a monolithic GraphQL server or a subgraph server within a supergraph.

              Apollo Server

              An open-source library for server-side GraphQL operation handling. It can be used as a monolithic GraphQL server or a subgraph server within a supergraph.

              argument

              A key-value pair associated with a particular schema field that lets operations pass data to that field's resolver.

              Argument values can be hardcoded as literal values (shown below for clarity) or provided via GraphQL variables (recommended).

              query GetHuman {
              human(id: "200") {
              name
              height(unit: "meters")
              }
              }
              resolvers

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              argument

              A key-value pair associated with a particular schema field that lets operations pass data to that field's resolver.

              Argument values can be hardcoded as literal values (shown below for clarity) or provided via GraphQL variables (recommended).

              query GetHuman {
              human(id: "200") {
              name
              height(unit: "meters")
              }
              }
              resolvers

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              argument

              A key-value pair associated with a particular schema field that lets operations pass data to that field's resolver.

              Argument values can be hardcoded as literal values (shown below for clarity) or provided via GraphQL variables (recommended).

              query GetHuman {
              human(id: "200") {
              name
              height(unit: "meters")
              }
              }
              argument

              A key-value pair associated with a particular schema field that lets operations pass data to that field's resolver.

              Argument values can be hardcoded as literal values (shown below for clarity) or provided via GraphQL variables (recommended).

              query GetHuman {
              human(id: "200") {
              name
              height(unit: "meters")
              }
              }
              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              argument

              A key-value pair associated with a particular schema field that lets operations pass data to that field's resolver.

              Argument values can be hardcoded as literal values (shown below for clarity) or provided via GraphQL variables (recommended).

              query GetHuman {
              human(id: "200") {
              name
              height(unit: "meters")
              }
              }
              resolvers

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              argument

              A key-value pair associated with a particular schema field that lets operations pass data to that field's resolver.

              Argument values can be hardcoded as literal values (shown below for clarity) or provided via GraphQL variables (recommended).

              query GetHuman {
              human(id: "200") {
              name
              height(unit: "meters")
              }
              }
              resolvers

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              Apollo Server

              An open-source library for server-side GraphQL operation handling. It can be used as a monolithic GraphQL server or a subgraph server within a supergraph.

              Apollo Server

              An open-source library for server-side GraphQL operation handling. It can be used as a monolithic GraphQL server or a subgraph server within a supergraph.

              Apollo Server

              An open-source library for server-side GraphQL operation handling. It can be used as a monolithic GraphQL server or a subgraph server within a supergraph.

              variable

              A placeholder for dynamic values in an operation allowing parameterization and reusability in requests. Variables can be used to fill arguments or passed to directives.

              query GetUser($userId: ID!) {
              user(id: $userId) {
              firstName
              }
              }

              In the query above, userId is a variable. The variable and its type are declared in the operation signature, signified by a $. The type of variable is a non-nullable ID. A variable's type must match the type of any argument it's used for.

              resolver

              A function that populates data for a particular field in a GraphQL schema. For example:

              const resolvers = {
              Query: {
              author(root, args, context, info) {
              return find(authors, { id: args.id });
              },
              },
              };
              variable

              A placeholder for dynamic values in an operation allowing parameterization and reusability in requests. Variables can be used to fill arguments or passed to directives.

              query GetUser($userId: ID!) {
              user(id: $userId) {
              firstName
              }
              }

              In the query above, userId is a variable. The variable and its type are declared in the operation signature, signified by a $. The type of variable is a non-nullable ID. A variable's type must match the type of any argument it's used for.

              variable

              A placeholder for dynamic values in an operation allowing parameterization and reusability in requests. Variables can be used to fill arguments or passed to directives.

              query GetUser($userId: ID!) {
              user(id: $userId) {
              firstName
              }
              }

              In the query above, userId is a variable. The variable and its type are declared in the operation signature, signified by a $. The type of variable is a non-nullable ID. A variable's type must match the type of any argument it's used for.

              variable

              A placeholder for dynamic values in an operation allowing parameterization and reusability in requests. Variables can be used to fill arguments or passed to directives.

              query GetUser($userId: ID!) {
              user(id: $userId) {
              firstName
              }
              }

              In the query above, userId is a variable. The variable and its type are declared in the operation signature, signified by a $. The type of variable is a non-nullable ID. A variable's type must match the type of any argument it's used for.

              NEW COURSE ALERT

              Introducing Apollo Connectors

              Connectors are the new and easy way to get started with GraphQL, using existing REST APIs.

              Say goodbye to GraphQL servers and resolvers—now, everything happens in the schema!

              Take the course