The gateway
After you set up at least one federation-ready subgraph, you can configure a gateway to sit in front of your subgraphs. The gateway executes incoming operations across those subgraphs.
The @apollo/gateway
package extends Apollo Server's functionality, enabling it to act as a gateway for an Apollo Federation architecture.
We recommend against running your gateway in a serverless environment (such as AWS Lambda), because schema composition increases Apollo Server's startup time. If you do run your gateway in a serverless environment, set your function's timeout to at least 10 seconds to prevent unexpected errors.
Setup
First, let's install the necessary packages:
1npm install @apollo/gateway apollo-server
The @apollo/gateway
package includes the ApolloGateway
class. To configure Apollo Server to act as a gateway, you pass an instance of ApolloGateway
to the ApolloServer
constructor, like so:
1const { ApolloServer } = require('apollo-server');
2const { ApolloGateway } = require('@apollo/gateway');
3const { readFileSync } = require('fs');
4
5const supergraphSdl = readFileSync('./supergraph.graphql').toString();
6
7// Initialize an ApolloGateway instance and pass it
8// the supergraph schema
9const gateway = new ApolloGateway({
10 supergraphSdl,
11});
12
13// Pass the ApolloGateway to the ApolloServer constructor
14const server = new ApolloServer({
15 gateway,
16});
17
18server.listen().then(({ url }) => {
19 console.log(`🚀 Server ready at ${url}`);
20});
Composing the supergraph schema
In the above example, we provide the supergraphSdl
option to the ApolloGateway
constructor. This is the string representation of our supergraph schema, which is composed from all of our subgraph schemas.
To learn how to compose your supergraph schema with the Rover CLI, see the Federation quickstart.
In production, we strongly recommend running the gateway in a managed mode with Apollo Studio, which enables your gateway to update its configuration without a restart. For details, see Setting up managed federation.
On startup, the gateway processes your supergraphSdl
, which includes routing information for your subgraphs. It then begins accepting incoming requests and creates query plans for them that execute across one or more subgraphs.
Updating the supergraph schema
In the above example, we provide a static supergraph schema to the gateway. This approach requires the gateway to restart in order to update the supergraph schema. This is undesirable for many applications, so we also provide the ability to update the supergraph schema dynamically.
1const { ApolloServer } = require('apollo-server');
2const { ApolloGateway } = require('@apollo/gateway');
3const { readFile } = require('fs/promises');
4
5let supergraphUpdate;
6const gateway = new ApolloGateway({
7 async supergraphSdl({ update }) {
8 // `update` is a function which we'll save for later use
9 supergraphUpdate = update;
10 return {
11 supergraphSdl: await readFile('./supergraph.graphql', 'utf-8'),
12 }
13 },
14});
15
16// Pass the ApolloGateway to the ApolloServer constructor
17const server = new ApolloServer({
18 gateway,
19});
20
21server.listen().then(({ url }) => {
22 console.log(`🚀 Server ready at ${url}`);
23});
There are a few things happening here. Let's take a look at each of them individually.
Note that supergraphSdl
is now an async
function. This function is called exactly once, when ApolloServer
initializes the gateway. It has the following responsibilities:
It receives the
update
function, which we use to update the supergraph schema.It returns the initial supergraph schema, which the gateway uses at startup.
With the update
function, we can now programatically update the supergraph. Polling, webhooks, and file watchers are all good examples of ways we can go about updating the supergraph.
The code below demonstrates a more complete example using a file watcher. In this example, assume that we're updating the supergraphSdl.graphql
file with the Rover CLI.
1const { ApolloServer } = require('apollo-server');
2const { ApolloGateway } = require('@apollo/gateway');
3const { watch } = require('fs');
4const { readFile } = require('fs/promises');
5
6const server = new ApolloServer({
7 gateway: new ApolloGateway({
8 async supergraphSdl({ update, healthCheck }) {
9 // create a file watcher
10 const watcher = watch('./supergraph.graphql');
11 // subscribe to file changes
12 watcher.on('change', async () => {
13 // update the supergraph schema
14 try {
15 const updatedSupergraph = await readFile('./supergraph.graphql', 'utf-8');
16 // optional health check update to ensure our services are responsive
17 await healthCheck(updatedSupergraph);
18 // update the supergraph schema
19 update(updatedSupergraph);
20 } catch (e) {
21 // handle errors that occur during health check or while updating the supergraph schema
22 console.error(e);
23 }
24 });
25
26 return {
27 supergraphSdl: await readFile('./supergraph.graphql', 'utf-8'),
28 // cleanup is called when the gateway is stopped
29 async cleanup() {
30 watcher.close();
31 }
32 }
33 },
34 }),
35});
36
37server.listen().then(({ url }) => {
38 console.log(`🚀 Server ready at ${url}`);
39});
This example is a bit more complete. Let's take a look at what we've added.
In the supergraphSdl
callback, we also receive a healthCheck
function. This enables us to run a health check against each of the services in our future supergraph schema. This is useful for ensuring that our services are responsive and that we don't perform an update when it's unsafe.
We've also wrapped our call to update
and healthCheck
in a try
block. If an error occurs during either of these, we want to handle this gracefully. In this example, we continue running the existing supergraph schema and log an error.
Finally, we return a cleanup
function. This is a callback that's called when the gateway is stopped. This enables us to cleanly shut down any ongoing processes (such as file watching or polling) when the gateway is shut down via a call to ApolloServer.stop
. The gateway expects cleanup
to return a Promise
and await
s it before shutting down.
Advanced usage
In a more complex application, you might want to create a class that handles the update
and healthCheck
functions, along with any additional state. In this case, you can instead provide an object (or class) with an initialize
function. This function is called just like the supergraphSdl
function discussed above. For an example of this, see the IntrospectAndCompose
source code.
Composing subgraphs with IntrospectAndCompose
Looking for
serviceList
? In@apollo/gateway
version 0.46.0 and later,IntrospectAndCompose
is the new drop-in replacement for theserviceList
option. TheserviceList
option will be removed in an upcoming release of@apollo/gateway
, butIntrospectAndCompose
will continue to be supported. We recommend using the Rover CLI to manage local composition, butIntrospectAndCompose
is still useful for various development and testing workflows.
We strongly recommend against using
IntrospectAndCompose
in production. For details, see below.
Alternatively, you can provide a subgraph
array to the IntrospectAndCompose
constructor, like so:
1const { ApolloGateway, IntrospectAndCompose } = require('@apollo/gateway');
2
3const gateway = new ApolloGateway({
4 supergraphSdl: new IntrospectAndCompose({
5 subgraphs: [
6 { name: 'accounts', url: 'http://localhost:4001' },
7 { name: 'products', url: 'http://localhost:4002' },
8 // ...additional subgraphs...
9 ],
10 }),
11});
Each item in the array is an object that specifies the name
and url
of one of your subgraphs. You can specify any string value for name
, which is used primarily for query planner output, error messages, and logging.
On startup, the gateway fetches each subgraph's schema from its url
and composes those schemas into a supergraph schema. It then begins accepting incoming requests and creates query plans for them that execute across one or more subgraphs.
Additional configuration options can be found in the IntrospectAndCompose
API documentation.
However, IntrospectAndCompose
has important limitations.
Limitations of IntrospectAndCompose
The IntrospectAndCompose
option can sometimes be helpful for local development, but it is strongly discouraged for any other environment. Here are some reasons why:
Composition might fail. With
IntrospectAndCompose
, your gateway performs composition dynamically on startup, which requires network communication with each subgraph. If composition fails, your gateway throws errors and experiences unplanned downtime.With the static or dynamic
supergraphSdl
configuration, you instead provide a supergraph schema that has already been composed successfully. This prevents composition errors and enables faster startup.
Gateway instances might differ. If you deploy multiple instances of your gateway while deploying updates to your subgraphs, your gateway instances might fetch different schemas from the same subgraph. This can result in sporadic composition failures or inconsistent supergraph schemas between instances.
When you deploy multiple instances with
supergraphSdl
, you provide the exact same static artifact to each instance, enabling more predictable behavior.
Updating the gateway
Before updating your gateway's version, check the changelog for potential breaking changes.
We strongly recommend updating your gateway in local and test environments before deploying updates to staging or production.
You can confirm the currently installed version of the @apollo/gateway
library with the npm list
command:
1npm list @apollo/gateway
To update the library, use the npm update
command:
1npm update @apollo/gateway
This updates the library to the most recent version allowed by your package.json
file. Learn more about dependency constraints.
To update to a particular version (including a version that exceeds your dependency constraints), use npm install
instead:
1npm install @apollo/gateway@0.34.0
Customizing requests and responses
The gateway can modify the details of an incoming request before executing it across your subgraphs. For example, your subgraphs might all use the same authorization token to associate an incoming request with a particular user. The gateway can add that token to each operation it sends to your subgraphs.
Similarly, the gateway can modify the details of its response to a client, based on the result returned by each subgraph.
Customizing requests
In the following example, each incoming request to the gateway includes an Authorization
header. The gateway sets the shared context
for an operation by pulling the value of that header and using it to fetch the associated user's ID.
After adding the userId
to the shared context
object, the gateway can then add that value to a header that it includes in its requests to each subgraph.
Expand example
1const { ApolloServer } = require('apollo-server');
2const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway');
3const { readFileSync } = require('fs');
4
5const supergraphSdl = readFileSync('./supergraph.graphql').toString();
6
7class AuthenticatedDataSource extends RemoteGraphQLDataSource {
8 willSendRequest({ request, context }) {
9 // Pass the user's id from the context to each subgraph
10 // as a header called `user-id`
11 request.http.headers.set('user-id', context.userId);
12 }
13}
14
15const gateway = new ApolloGateway({
16 supergraphSdl,
17 buildService({ name, url }) {
18 return new AuthenticatedDataSource({ url });
19 },
20});
21
22const server = new ApolloServer({
23 gateway,
24
25 context: ({ req }) => {
26 // Get the user token from the headers
27 const token = req.headers.authorization || '';
28 // Try to retrieve a user with the token
29 const userId = getUserId(token);
30 // Add the user ID to the context
31 return { userId };
32 },
33});
34
35server.listen().then(({ url }) => {
36 console.log(`🚀 Server ready at ${url}`);
37});
The fields of the object passed to your
context
function differ if you're using middleware besides Express. See the API reference for details.
The buildService
function enables us to customize the requests that are sent to our subgraphs. In this example, we return a custom RemoteGraphQLDataSource
. The datasource allows us to modify the outgoing request with information from the Apollo Server context
before it's sent. Here, we add the user-id
header to pass an authenticated user ID to downstream services.
Customizing responses
Let's say that whenever a subgraph returns an operation result to the gateway, it includes a Server-Id
header in the response. The value of the header uniquely identifies the subgraph in our graph.
When the gateway then responds to a client, we want its Server-Id
header to include the identifier for every subgraph that contributed to the response. In this case, we can tell the gateway to aggregate the various server IDs into a single, comma-separated list.
The flow for processing a single operation from a client application then looks like this:
To implement this flow, we can use the didReceiveResponse
callback of the RemoteGraphQLDataSource
class to inspect each subgraph's result as it comes in. We can add the Server-Id
to the shared context
in this callback, then pull the full list from the context
when sending the final response to the client.
Expand example
1const { ApolloServer } = require('apollo-server');
2const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway');
3const { readFileSync } = require('fs');
4
5const supergraphSdl = readFileSync('./supergraph.graphql').toString();
6
7class DataSourceWithServerId extends RemoteGraphQLDataSource {
8 async didReceiveResponse({ response, request, context }) {
9 // Parse the Server-Id header and add it to the array on context
10 const serverId = response.http.headers.get('Server-Id');
11 if (serverId) {
12 context.serverIds.push(serverId);
13 }
14
15 // Return the response, even when unchanged.
16 return response;
17 }
18}
19
20const gateway = new ApolloGateway({
21 supergraphSdl,
22 buildService({ url }) {
23 return new DataSourceWithServerId({ url });
24 }
25});
26
27const server = new ApolloServer({
28 gateway,
29 context() {
30 return { serverIds: [] };
31 },
32 plugins: [
33 {
34 requestDidStart() {
35 return {
36 willSendResponse({ context, response }) {
37 // Append our final result to the outgoing response headers
38 response.http.headers.set(
39 'Server-Id',
40 context.serverIds.join(',')
41 );
42 }
43 };
44 }
45 }
46 ]
47});
48
49server.listen().then(({ url }) => {
50 console.log(`🚀 Server ready at ${url}`);
51});
In this example, multiple calls to
didReceiveResponse
arepush
ing a value onto the sharedcontext.serverIds
array. The order of these calls cannot be guaranteed. If you write logic that modifies the sharedcontext
object, make sure that modifications are not destructive, and that the order of modifications doesn't matter.
To learn more about buildService
and RemoteGraphQLDataSource
, see the API docs.
Custom directive support
The @apollo/gateway
library supports the use of custom directives in your subgraph schemas. This support differs depending on whether a given directive is a type system directive or an executable directive.
Type system directives
Type system directives are directives that are applied to one of these locations. These directives are not used within operations, but rather are applied to locations within the schema itself.
The @deprecated
directive below is an example of a type system directive:
1directive @deprecated(
2 reason: String = "No longer supported"
3) on FIELD_DEFINITION | ENUM_VALUE
4
5type ExampleType {
6 newField: String
7 oldField: String @deprecated(reason: "Use `newField`.")
8}
At composition time, ApolloGateway
strips all definitions and uses of type system directives from your composed schema. This has no effect on your subgraph schemas, which retain this information.
Effectively, the gateway supports type system directives by ignoring them, making them the responsibility of the subgraphs that define them.
To learn about using custom directives in your subgraph schemas, see Custom directives in subgraphs.
Executable directives
Executable directives are directives that are applied to one of these locations. These directives are defined in your schema, but they're used in operations that are sent by clients.
Although the
@apollo/gateway
library supports executable directives, Apollo Server itself does not. This guidance is provided primarily for architectures that use the@apollo/gateway
library in combination with subgraphs that do not use Apollo Server.
Here's an example of an executable directive definition:
1# Uppercase this field's value (assuming it's a string)
2directive @uppercase on FIELD
And here's an example of a query that uses that directive:
1query GetUppercaseUsernames {
2 users {
3 name @uppercase
4 }
5}
At composition time, ApolloGateway
makes sure that all of your subgraphs define the exact same set of executable directives. If any service is missing a definition, or if definitions differ in their locations, arguments, or argument types, a composition error occurs.
It's strongly recommended that all of your subgraphs also use the exact same logic for a given executable directive. Otherwise, operations might produce inconsistent or confusing results for clients.