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!