Implementing a subgraph with Apollo Server
This article demonstrates how to create a subgraph for a federated supergraph using Node.js and Apollo Server.
To create a subgraph using a different language and/or framework, see the list of Federation-compatible subgraph implementations. Note that not all listed libraries provide full support for Federation features.
Defining a subgraph
To be part of a supergraph, a subgraph must conform to the Apollo Federation subgraph specification, which exposes the subgraph's capabilities to your graph router, as well as to tools like Apollo Studio.
Converting an existing monolithic graph into a single subgraph is a convenient first step in building a federated supergraph. To start, here's a non-federated Apollo Server setup:
const { ApolloServer, gql } = require('apollo-server');const typeDefs = gql`type Query {me: User}type User {id: ID!username: String}`;const resolvers = {Query: {me() {return { id: "1", username: "@ava" }}}};const server = new ApolloServer({typeDefs,resolvers,});server.listen(4001).then(({ url }) => {console.log(`🚀 Server ready at ${url}`);});
This should look familiar if you've set up Apollo Server before. If you haven't, we recommend you familiarize yourself with the basics before jumping into federation.
Now, let's convert this to a subgraph!
1. Install and import @apollo/subgraph
The first step is to install the @apollo/subgraph
package in our server project:
npm install @apollo/subgraph
We also need to require the buildSubgraphSchema
function from this package in our index.js
file (we'll use it later):
const { buildSubgraphSchema } = require('@apollo/subgraph');
2. Opt in to Federation 2
For a subgraph to use new features in Federation 2, its schema needs to include the following extend schema
definition:
const typeDefs = gql`extend schema@link(url: "https://specs.apollo.dev/federation/v2.0",import: ["@key", "@shareable"])type Query {me: User}type User {id: ID!username: String}`;
This definition enables the schema to use Federation 2 features. Without it, Federation 2 composition assumes that a subgraph is using Federation 1, which sets certain defaults for backward compatibility.
As you begin using more federation-specific directives beyond @key
and @shareable
, you'll need to add those directives to the import
array shown above.
3. Define an entity
Entities aren't required in a subgraph, but they're a core building block of a federated supergraph, so it's good to get some practice defining them.
As part of our federated architecture, we want other subgraphs to be able to contribute fields to the User
type. To enable this, we add the @key
directive to the User
type's definition to designate it as an entity:
const typeDefs = gql`extend schema@link(url: "https://specs.apollo.dev/federation/v2.0",import: ["@key", "@shareable"])type Query {me: User}type User @key(fields: "id") {id: ID!username: String}`;
The @key
directive tells the gateway which field(s) of the User
entity can uniquely identify a particular instance of it. In this case, the gateway can use the single field id
.
Next, we add a reference resolver for the User
entity. A reference resolver tells the gateway how to fetch an entity by its @key
fields:
const resolvers = {Query: {me() {return { id: "1", username: "@ava" }}},User: {__resolveReference(user, { fetchUserById }){return fetchUserById(user.id)}}};
(This example requires defining the fetchUserById
function to obtain the appropriate User
from our backing data store.)
4. Generate the subgraph schema
Finally, we use the buildSubgraphSchema
function from the @apollo/subgraph
package to augment our schema definition with federation support. We provide the result of this function to the ApolloServer
constructor:
const server = new ApolloServer({schema: buildSubgraphSchema({ typeDefs, resolvers })});server.listen(4001).then(({ url }) => {console.log(`🚀 Server ready at ${url}`);});
The server is now ready to act as a subgraph in a federated graph!
Combined example
Here are the snippets above combined (again, note that for this sample to be complete, you must define the fetchUserById
function for your data source):
const { ApolloServer, gql } = require('apollo-server');const { buildSubgraphSchema } = require('@apollo/subgraph');const typeDefs = gql`extend schema@link(url: "https://specs.apollo.dev/federation/v2.0",import: ["@key", "@shareable"])type Query {me: User}type User @key(fields: "id") {id: ID!username: String}`;const resolvers = {Query: {me() {return { id: "1", username: "@ava" }}},User: {__resolveReference(user, { fetchUserById }){return fetchUserById(user.id)}}}const server = new ApolloServer({schema: buildSubgraphSchema({ typeDefs, resolvers })});server.listen(4001).then(({ url }) => {console.log(`🚀 Server ready at ${url}`);});
Securing your subgraphs
Because of the power and flexibility of the Query._entities
and Query._service
fields, your subgraphs should not be directly accessible by clients. Instead, only your graph router should have access to your subgraphs. Clients then communicate with the router:
Make sure to implement any necessary firewall rules, access control lists, or other measures to ensure that individual subgraphs can be accessed only via the router.
Disabling CORS
We recommend that subgraphs do not enable CORS (or at least do not use the wildcard access-control-allow-origin: *
header). This prevents attackers from exploiting a user's browser to access a subgraph directly.
For Apollo Server subgraphs, you need to disable the default wildcard CORS policy (which is appropriate for many publicly-exposed servers but not for inaccessible servers like subgraphs). For details, see the Apollo Server CORS documentation.
Subgraph-specific fields
When you generate your subgraph schema, some federation-specific definitions are automatically added to it. In addition to directive definitions like @key
, the most useful of these definitions for debugging are two fields of the Query
type: _service
and _entities
:
type Query {# ...your field definitions...# Added automatically_service: _Service!_entities(representations: [_Any!]!): [_Entity]!}
Query._service
This field returns a _Service
object with one field of its own: sdl
. You can query it like so:
query GetSubgraphSchema {_service {sdl}}
The sdl
field returns your subgraph's schema as an SDL string. This field has a couple of important differences from a standard introspection query that a tool like Apollo Sandbox uses:
- Unlike introspection, the
sdl
field is not disabled by default in production environments (this is safe if you properly secure your subgraph). - Unlike introspection, the
sdl
field's returned string includes federation-specific directives like@key
.
Query._entities
Learn about entities if you haven't yet.
This field takes a list of entity representations and returns a list of corresponding entities.
Whenever one subgraph references another subgraph's entity, it uses an entity representation to do so. An entity representation is an object that includes only the entity's __typename
and the fields in the entity's @key
.
_entities(representations: [_Any!]!): [_Entity]!
- The
_Any
type is a special scalar that enables you to provide entity representations of any valid shape. - The
_Entity
type is a generated union type that includes every entity defined in your subgraph's schema.
You can query this field like so, providing a value for the $representations
variable as shown:
query ($representations: [_Any!]!) {_entities(representations: $representations) {... on User {idusername}}}
{"representations": [{"__typename": "User","id": "5"}]}
Using in tests and debugging
If you're writing integration tests for your subgraph, you can test the return value of the _entities
field for various entity representations that your other subgraphs use.
If you're developing your subgraph in your local environment, you can mock the return value of the _entities
field for your other subgraphs so you don't have to connect those subgraphs to their respective data stores.
Custom directives in subgraphs
The method for defining custom directives differs slightly for a federated graph, and it also depends on the version of Apollo Server you're using.
⚠️ Important considerations
Before you use directives in a federated graph, make sure to consider the following:
- Custom directives are not included in your graph's composed supergraph schema. The composition process strips all subgraph directives. Only a given subgraph is aware of its own directives.
- Because directives are specific to individual subgraphs, it's valid for different subgraphs to define the same directive with different logic. Composition does not detect or warn about such inconsistencies.
- If multiple subgraphs can resolve a particular field, each subgraph should almost always apply the exact same set of custom directives (with the exact same accompanying logic) to that field. Otherwise, the behavior of that field might vary depending on which subgraph resolves it.
Directives in Apollo Server 3.x
Apollo Server 3 does not provide built-in support for custom directives, but you can install certain @graphql-tools
libraries to enable support. To get started with these libraries in Apollo Server, first read Creating schema directives.
As the linked article describes, in Apollo Server 3 you define a transformer function for each of your subgraph schema's custom directives.
To apply transformer functions to your executable subgraph schema, you first generate the subgraph schema with buildSubgraphSchema
as usual:
let subgraphSchema = buildSubgraphSchema({typeDefs, resolvers});
But instead of passing the result directly to the ApolloServer
constructor, you first apply all of your transformer functions to it:
// Transformer function for an @upper directivesubgraphSchema = upperDirectiveTransformer(subgraphSchema, 'upper');
After applying all transformer functions, you provide your final subgraph schema to the ApolloServer
constructor as usual:
const server = new ApolloServer({schema: subgraphSchema// ...other options...});
Directives in Apollo Server 2.x
Without Apollo Federation, you provide your directive definitions to the constructor of ApolloServer
in the schemaDirectives
argument, like so:
With Apollo Federation, you instead call SchemaDirectiveVisitor.visitSchemaDirectives
, passing in your schema and your directives, before you provide your schema to the constructor of ApolloServer
:
const { ApolloServer, gql, SchemaDirectiveVisitor } = require('apollo-server');const { buildSubgraphSchema } = require ('@apollo/subgraph')// typeDefs and resolvers defined hereclass DeprecatedDirective extends SchemaDirectiveVisitor {public visitFieldDefinition(field: GraphQLField<any, any>) {field.isDeprecated = true;field.deprecationReason = this.args.reason;}}const directives = {deprecated: DeprecatedDirective};let schema = buildSubgraphSchema({ typeDefs, resolvers });SchemaDirectiveVisitor.visitSchemaDirectives(schema, directives);const server = new ApolloServer({schema: schema});
Also make sure to read about the gateway's support for custom directives.