Building Federated Subgraphs with Nest.js
Lately, I've found myself doing a lot of work experimenting with building GraphQL APIs and understanding more about how these could be scaled across the enterprise. This journey has led me to start exploring more of the Apollo Federation Ecosystem, and how to effectively build GraphQL solutions that are compatible with Apollo Federation v2 in an efficient way.
Tangentially, as something of a recovering Java and Spring Framework developer who is making his way into the Typescript ecosystem more and more each day, I recently discovered the Nest.js Framework. Suffice to say, Nest.js feels very familiar to me, as it provides dependency injection capabilities for Typescript applications, along with a host of supporting libraries that make integrating various other technologies into your applications as frictionless as possible. I would blasphemously describe it as the Spring Framework for Node.js applications.
So I started thinking, how difficult would it be to build GraphQL applications using Nest.js and Typescript with support for Apollo Federation? Nest.js already provides excellent support for building GraphQL applications, so it seemed that it should be possible. While information on this wasn't super forthcoming, it also turned out to not be incredibly difficult, but I wanted to share, in case you find yourself needing to build Federated GraphQL applications and want to leverage Nest.js to do so.
Configuring GraphQL Support in Nest.js
As you're likely already familiar with, GraphQL support is configured in Nest.js through the @nestjs/graphql
package, as well as a few other packages, depending on the GraphQL implementation you'll be using. For our purposes,
we will be using Apollo Server, which requires the @apollo/server
, @nestjs/apollo
, and graphql
packages to
fully support and configure GraphQL in Nest.js with Apollo.
Since we want to make use of Apollo Federation, our application will be a subgraph application however, and so we'll
need to install the @apollo/subgraph
package as well to add support for Federation directives within our
application. From our Nest application's root directory, we'll install those packages now.
npm install --save @apollo/server @apollo/subgraph @nestjs/apollo @nestjs/graphql graphql
Now, we're ready to configure Nest.js with GraphQL support. This is accomplished by adding the GraphQLModule
in the
@nestjs/graphql
package to the root module of our application and configuring it. Normally, we would configure the
module to use the ApolloDriver
from the @nestjs/apollo
package, but when building a subgraph application, we
swap that for the ApolloFederationDriver
. This, fortunately, is the only configuration change required to enable
Federation support. A complete example of the GraphQL configuration can be seen here.
import { ApolloServerPluginLandingPageLocalDefault } from "@apollo/server/plugin/landingPage/default"
import { ApolloServerPluginInlineTraceDisabled } from "@apollo/server/plugin/disabled"
import { ApolloDriverConfig, ApolloFederationDriver } from "@nestjs/apollo"
import { Module } from "@nestjs/common"
import { GraphQLModule } from "@nestjs/graphql"
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloFederationDriver,
path: "/api/graphql",
playground: false,
plugins: [ApolloServerPluginInlineTraceDisabled(), ApolloServerPluginLandingPageLocalDefault()],
typePaths: ["./**/*.graphql"],
}),
],
})
export class AppModule {}
You'll notice that I've enabled the Apollo Studio plugin for local accessors as well as disabled inline tracing through plugins. This is performed in exactly the same way as when using the `ApolloDriver``, but I wanted to call it out in case you need to use other Apollo Server plugins in your application. You'll also notice that I've configured my application for schema-first semantics, pulling in schemas from each module of the application.
Schema with Federation Directives
Now that we've configured support for the federation driver, we can start using federation directives within our schemas. Since I prefer to This is done by adding the following directive to each GraphQL schema definition language file that you author in your Nest.js application.
extend schema
@link(url: "https://specs.apollo.dev/link/v1.0", import: ["@link"])
@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@shareable"])
This enables us to use the federated schema directives when declaring our types, which we will do now. For a User
type, for example, we might choose to use the following simple example.
type User @key(fields: "id") {
"""
The ID value for the User.
Formatted as a 24 character hexadecimal ObjectId value.
"""
id: ID!
"The first name of the User."
firstName: String!
"The last name of the User."
lastName: String!
"The email address of the User."
emailAddress: String!
"The title of the User."
title: String!
"The image of the User."
image: String
"The notes for the User."
notes: String
}
At a glance, this looks pretty similar to a normal GraphQL type, but the notable exception is the @key
directive
immediately after the type name. This marks the type as representative of a federated Entity
and describes to the
supergraph how this entity is identified. All subgraph schemas that wish to extend the User
type here will need to
include this same @key
directive to indicate that it is a federated entity of the supergraph.
Resolvers for Federated Types
At this point, we would write a resolver class, backed by any number of resolver methods and queries that we've defined
for the User
entity, and we would have a workable, accessible type as a part of our GraphQL application. Federation
requires on additional step, however. Before we get into this, though, it's useful to have a quick understanding of how
a federated entity is looked up.
When you make a query against a supergraph, the subgraph that is responsible for defining that query serves as its entrypoint. The query is forwarded to that subgraph, executed, and the results are returned to the supergraph, and ultimately to the client. If the fields requested by the client can all be resolved from within the subgraph hosting the entrypoint, nothing else needs to be done.
But what if one of our subgraphs was a room reservation system that defined an extension to the User
type,
allowing us to retrieve the user's reservations as a field of the User
type? It might represent this using the
schema:
type User @key(fields: "id") {
"""
The ID value for the User.
Formatted as a 24 character hexadecimal ObjectId value.
"""
id: ID!
"The reservations for the User."
reservations: [Reservation!]
}
The query to retrieve a User
by ID would be provided by the users subgraph, and so it would serve as the entrypoint.
How would we resolve the reservations field? As it turns out, federation provides a number of entrypoints that it uses
internally to look up instances by their key fields. The federation specification refers to these as
representations
, and passes requests for their resolution through the _entities
query of our subgraph, which is
added to our subgraph automatically when we include federation directives in our schema. In order to support access to
an Entity in our federated subgraphs, we'll generally need to provide our runtime with a means to convert these
representations to the entity instance types that they describe.
In Nest.js, we author a special resolver function that we annotate with the @ResolveReference()
annotation. This
function takes a single argument and returns the hydrated object that you would use as the parent object for other
field resolvers of that type. This function can be, and often is asynchronous.
The representation
for any entity in a federated subgraph consists of the fields declared in its @key
directive,
as well as the __typename
field, which is a string representation of the name of the GraphQL type.
A complete example of a reference resolver that uses a Nest.js service to handle retrieval of the referenced object might look something like this.
import { Inject } from "@nestjs/common"
import { Resolver, ResolveReference } from "@nestjs/graphql"
import { UsersService, USERS_SERVICE } from "./users.service"
interface User {
id: string
firstName: string
lastName: string
emailAddress: string
title: string
image?: string
notes?: string
}
interface UserRepresentation {
__typename: string
id: string
}
@Resolver("User")
export class UsersResolver {
constructor(@Inject(USERS_SERVICE) private readonly usersService: UsersService) {}
// Other field resolvers, query resolvers, and mutation resolvers.
@ResolveReference()
async resolveReference(userRepresentation: UserRepresentation): Promise<User | undefined> {
return this.usersService.findById(userRepresentation.id)
}
}
It is important to understand that this resolver function cannot accept any other arguments, such as parameters marked
by @Args()
, @Parent()
, nor @Context()
as this will cause the representation argument to not be passed in.
Nest.js takes care of inspecting the __typename part of the representation and forwarding the request to the correct
type's reference resolver for us.
Conclusion
For each subgraph that contributes fields to an entity type, you'll likely need to provide a way to resolve references
to that entity from within that subgraph, and Nest.js makes this pretty easy. The key thing to remember is that you'll
always need to write a function to do this work within your @Resolver
annotated class that takes a single
representation
compatible parameter for the type, returns the parent type for that resolver's fields, and is
annotated with the @ResolveReference()
annotation.
As always, if you've benefited from this information, and you'd like to reach out, or you have a suggestion for how I can make this solution even better, please feel free to drop me a line at jonah@nerdynarwhal.com. Thanks for reading!