GraphQL: Federation

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against NestJS 12.1 (npm @nestjs/core 12.1.2, published 2026-09-30), @nestjs/graphql 14.0.3, @nestjs/apollo 14.0.3 and @nestjs/mercurius 14.0.3, @apollo/subgraph 2.15.1, @apollo/gateway 2.14.4, mercurius 16.10.1 with @mercuriusjs/gateway 5.2.0 and @mercuriusjs/federation 5.1.1, Node.js 22 and 24 LTS and TypeScript 6 (6.0.x), all as of 2026-10-02. The examples use a Nest 12 project with each subgraph as its own Node.js process on an ephemeral port, a gateway in front of them, and Vitest plus supertest (or Fastify’s inject()) querying across the whole graph. Federation theory (what an entity is, how a query planner splits a query, federation versus schema stitching) is covered in GraphQL Federation; this page only covers how @nestjs/graphql implements it. The Bookshelf application that grows across this section already has books, authors and reviews as feature modules (see Modules and Project Structure & Workspaces); here each of them becomes a separately deployable subgraph and a gateway merges them into one API.

The moving parts in NestJS

Federation splits one GraphQL API in two roles. A subgraph is an ordinary Nest application with a GraphQL endpoint that also answers two federation-specific fields (_service { sdl } and _entities(representations: …​)). A gateway has no resolvers of its own: it reads the schema of every subgraph, composes them into one supergraph and plans each incoming query as a series of requests to the subgraphs. In @nestjs/graphql the role is chosen by the driver passed to GraphQLModule.forRoot():

Role Apollo Mercurius

Subgraph

ApolloFederationDriver (@nestjs/apollo), needs @apollo/subgraph

MercuriusFederationDriver (@nestjs/mercurius), needs @apollo/subgraph and a Fastify application

Gateway

ApolloGatewayDriver (@nestjs/apollo), needs @apollo/gateway

MercuriusGatewayDriver (@nestjs/mercurius), needs @mercuriusjs/gateway and a Fastify application

Federation version

1 or 2 (autoSchemaFile: { federation: 2 })

1; the Nest guide warns that Mercurius does not fully support Federation 2

Everything else (@ObjectType(), @Field(), @Resolver(), @Query(), @ResolveField()) is the regular code-first or schema-first API described in GraphQL: Code-First & Schema-First. Federation adds two things on top: federation directives attached with @Directive(), and the @ResolveReference() decorator.

flowchart TD Client["Client
one query, one endpoint"] GW["Gateway
ApolloGatewayDriver
composes the supergraph"] Books["books subgraph
Book (key: id)
Query.books, Query.book"] Authors["authors subgraph
Author (key: id)
Query.authors"] Reviews["reviews subgraph
Review, extends Book
Book.reviews, Book.averageRating"] Client -->|"POST /graphql"| GW GW -->|"books { title }"| Books GW -->|"_entities Author"| Authors GW -->|"_entities Book"| Reviews Books -. "returns Author reference {id}" .-> GW

The figure shows the Bookshelf split used on the rest of this page. The client sends a single query; the gateway asks the books subgraph for the books, receives an Author reference for each (just its id), and then calls the authors subgraph’s _entities field to fill in the names while calling the reviews subgraph’s _entities field to attach ratings to each Book. The subgraphs never call each other.

Install the packages for the Apollo flavour (@nestjs/apollo and @apollo/server are the usual GraphQL-over-Apollo requirements, see the code-first page):

npm install @nestjs/graphql @nestjs/apollo @apollo/server graphql @apollo/subgraph @apollo/gateway

The Mercurius flavour needs @nestjs/graphql, @nestjs/mercurius, mercurius, graphql, @apollo/subgraph (the builder for the subgraph schema) and @mercuriusjs/gateway for the gateway process. @nestjs/mercurius also lists @mercuriusjs/federation as an optional peer dependency; the official guide installs only @apollo/subgraph for subgraphs, and the 14.0.3 driver code builds the subgraph schema from @apollo/subgraph without loading @mercuriusjs/federation, so you only need it if you use that package directly.

npm install @nestjs/graphql @nestjs/mercurius mercurius graphql @apollo/subgraph @mercuriusjs/gateway

Splitting Bookshelf into subgraphs

Federation lets the schema be divided by concern rather than by type. For Bookshelf the concerns are the catalogue (books), the people who write the books (authors) and what readers think of them (reviews). Each subgraph owns the data of its concern and exposes it through the type that is the shared vocabulary of the graph:

Subgraph Owns Contributes

books

Book (id, title)

Query.books, Query.book, Book.author (a reference to an Author)

authors

Author (id, name)

Query.authors

reviews

Review (id, rating, text)

Query.latestReviews, plus the fields Book.reviews and Book.averageRating added to a type owned by books

In the monorepo layout of Project Structure & Workspaces (see nestjs-bookshelf-layout.svg there) books/, authors/ and reviews/ are folders of the bookshelf-api app. Splitting them means one nest g app per subgraph (or one repository per team) plus a gateway app; the shared libs/common and libs/database stay libraries. The in-memory data used by every example here is a stand-in for the tables each subgraph would own:

graphql-federation/data.ts
export interface BookRecord {
  id: string;
  title: string;
  authorId: string;
}

export interface AuthorRecord {
  id: string;
  name: string;
}

export interface ReviewRecord {
  id: string;
  bookId: string;
  rating: number;
  text: string;
}

export const AUTHORS: AuthorRecord[] = [
  { id: '1', name: 'Ursula K. Le Guin' },
  { id: '2', name: 'Stanislaw Lem' },
];

export const BOOKS: BookRecord[] = [
  { id: '10', title: 'The Dispossessed', authorId: '1' },
  { id: '11', title: 'A Wizard of Earthsea', authorId: '1' },
  { id: '12', title: 'Solaris', authorId: '2' },
];

export const REVIEWS: ReviewRecord[] = [
  { id: '100', bookId: '10', rating: 5, text: 'Utopia with friction.' },
  { id: '101', bookId: '10', rating: 4, text: 'Dense but rewarding.' },
  { id: '102', bookId: '12', rating: 5, text: 'Unsettling and brilliant.' },
];

A subgraph: @key and reference resolvers

The books subgraph is a regular code-first GraphQL module with three federation-specific parts.

  1. @Directive('@key(fields: "id")') on the Book class declares Book an entity: other subgraphs can identify a Book by its id, and the router may ask this subgraph for a Book by that key.

  2. @ResolveReference() marks the method the router’s entities request ends up in. It receives the _representation the router sent ({ __typename: 'Book', id: '10' } plus any other key fields) and returns the full object, from which the other @ResolveField() methods then resolve their fields.

  3. autoSchemaFile: { federation: 2 } switches the schema builder to Federation 2 and makes Nest add the @link directive that imports the federation directives. Passing autoSchemaFile: true produces a Federation 1 schema instead.

graphql-federation/books.subgraph.ts
import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import {
  Args,
  Directive,
  Field,
  GraphQLModule,
  ID,
  ObjectType,
  Parent,
  Query,
  ResolveField,
  ResolveReference,
  Resolver,
} from '@nestjs/graphql';
import { BOOKS, type BookRecord } from './data.js';

// A stub of the Author entity owned by the authors subgraph: it can be
// referenced (by id) but this subgraph cannot resolve it.
@ObjectType()
@Directive('@key(fields: "id", resolvable: false)')
export class Author {
  @Field(() => ID)
  id: string;
}

@ObjectType()
@Directive('@key(fields: "id")')
export class Book {
  @Field(() => ID)
  id: string;

  @Field()
  title: string;

  @Field(() => Author)
  author: Author;
}

@Resolver(() => Book)
export class BooksResolver {
  @Query(() => [Book])
  books(): BookRecord[] {
    return BOOKS;
  }

  @Query(() => Book, { nullable: true })
  book(@Args('id', { type: () => ID }) id: string): BookRecord | undefined {
    return BOOKS.find((book) => book.id === id);
  }

  // Called by the router when another subgraph hands over { __typename: 'Book', id }
  @ResolveReference()
  resolveReference(reference: { __typename: string; id: string }) {
    return BOOKS.find((book) => book.id === reference.id);
  }

  // Return a reference to an Author: the router fetches the rest from the authors subgraph
  @ResolveField(() => Author)
  author(@Parent() book: BookRecord) {
    return { __typename: 'Author', id: book.authorId };
  }
}

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      autoSchemaFile: { federation: 2 },
      buildSchemaOptions: { orphanedTypes: [Author] },
    }),
  ],
  providers: [BooksResolver],
})
export class BooksModule {}

Three details are worth reading twice:

  • The author field does not load an author. It returns an object with typename and the key; the gateway uses typename to find the subgraph that owns Author and sends it a representation to resolve. This is why the books subgraph has no dependency on the authors data at all.

  • Author is declared here only as a stub. resolvable: false tells the supergraph composition that this subgraph can reference an Author but never be asked to produce one. A stub class that is not reachable from a query or mutation has to be added to buildSchemaOptions.orphanedTypes, otherwise it is left out of the schema.

  • Every subgraph is a normal Nest application, so guards, interceptors, pipes and the rest of the request lifecycle (see Request Lifecycle) apply to its resolvers, including the @ResolveReference() method.

Besides the schema you wrote, a federated subgraph exposes the schema plumbing the router reads. The SDL it serves through _service { sdl } (abridged here; the real output also lists the full set of federation directive definitions) shows the Federation 2 @link, the entity keys and the added _entities field:

extend schema @link(url: "https://specs.apollo.dev/federation/v2.14", import: ["@key", "@external", "@shareable", ...])

type Author @key(fields: "id", resolvable: false) {
  id: ID!
}

type Book @key(fields: "id") {
  id: ID!
  title: String!
  author: Author!
}

type Query {
  books: [Book!]!
  book(id: ID!): Book
}

union _Entity = Author | Book

extend type Query {
  _entities(representations: [_Any!]!): [_Entity]!
  _service: _Service!
}

The authors subgraph owns Author completely. Its key is resolvable (the default) and its reference resolver looks the author up by id:

graphql-federation/authors.subgraph.ts
import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import {
  Directive,
  Field,
  GraphQLModule,
  ID,
  ObjectType,
  Query,
  ResolveReference,
  Resolver,
} from '@nestjs/graphql';
import { AUTHORS, type AuthorRecord } from './data.js';

@ObjectType()
@Directive('@key(fields: "id")')
export class Author {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;
}

@Resolver(() => Author)
export class AuthorsResolver {
  @Query(() => [Author])
  authors(): AuthorRecord[] {
    return AUTHORS;
  }

  @ResolveReference()
  resolveReference(reference: { __typename: string; id: string }) {
    return AUTHORS.find((author) => author.id === reference.id);
  }
}

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      autoSchemaFile: { federation: 2 },
    }),
  ],
  providers: [AuthorsResolver],
})
export class AuthorsModule {}

A subgraph can be exercised without any gateway by sending the router’s own _entities request to it. This is the quickest way to test a @ResolveReference() method:

{
  _entities(representations: [{ __typename: "Author", id: "2" }]) {
    ... on Author { name }
  }
}
{ "data": { "_entities": [{ "name": "Stanislaw Lem" }] } }

Extending a type owned by another subgraph

The reviews subgraph adds fields to Book without owning it. In Federation 2 that is simply a second declaration of the same entity: same @key, different fields. The type is marked an entity in both subgraphs and the composition merges the fields. In Federation 1 the second declaration had to be marked with @extends and the key field with @external; in Federation 2 neither is needed, and you will only see them in older code (they are covered under Federation 1 and Federation 2).

graphql-federation/reviews.subgraph.ts
import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import {
  Directive,
  Field,
  Float,
  GraphQLModule,
  ID,
  Int,
  ObjectType,
  Parent,
  Query,
  ResolveField,
  ResolveReference,
  Resolver,
} from '@nestjs/graphql';
import { REVIEWS, type ReviewRecord } from './data.js';

@ObjectType()
@Directive('@key(fields: "id")')
export class Review {
  @Field(() => ID)
  id: string;

  @Field(() => Int)
  rating: number;

  @Field()
  text: string;
}

// The Book entity belongs to the books subgraph. Here it is extended with two
// fields. The key stays resolvable: the router reaches this subgraph through it.
@ObjectType()
@Directive('@key(fields: "id")')
export class Book {
  @Field(() => ID)
  id: string;

  @Field(() => [Review])
  reviews: Review[];

  @Field(() => Float, { nullable: true })
  averageRating?: number;
}

@Resolver(() => Book)
export class BookReviewsResolver {
  // This subgraph owns no Book data: the reference itself is the parent object
  @ResolveReference()
  resolveReference(reference: { __typename: string; id: string }) {
    return reference;
  }

  @ResolveField(() => [Review])
  reviews(@Parent() book: { id: string }): ReviewRecord[] {
    return REVIEWS.filter((review) => review.bookId === book.id);
  }

  @ResolveField(() => Float, { nullable: true })
  averageRating(@Parent() book: { id: string }): number | null {
    const ratings = REVIEWS.filter((r) => r.bookId === book.id).map(
      (r) => r.rating,
    );
    if (ratings.length === 0) return null;
    return ratings.reduce((sum, value) => sum + value, 0) / ratings.length;
  }
}

@Resolver(() => Review)
export class ReviewsResolver {
  @Query(() => [Review])
  latestReviews(): ReviewRecord[] {
    return REVIEWS;
  }
}

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      autoSchemaFile: { federation: 2 },
      buildSchemaOptions: { orphanedTypes: [Book] },
    }),
  ],
  providers: [BookReviewsResolver, ReviewsResolver],
})
export class ReviewsModule {}

The reference the router passes to resolveReference() already contains the key ({ __typename: 'Book', id: '10' }), and that is all reviews() and averageRating() need, so the method returns the reference as the parent object. When a subgraph has no data of its own for the entity it extends, this "return the key" resolver is the usual shape.

A tempting mistake is to declare the Book key here as resolvable: false, mirroring the stub in the books subgraph. The subgraph then starts normally and the failure only appears when the gateway composes the supergraph (this is the actual output of that variant):

Error: A valid schema couldn't be composed. The following composition errors were found:
	The following supergraph API query:
{
  book(id: "<any id>") {
    reviews {
      ...
    }
  }
}
cannot be satisfied by the subgraphs because:
- from subgraph "books":
  - cannot find field "Book.reviews".
  - cannot move to subgraph "reviews", which has field "Book.reviews", because none of the @key defined on type "Book" in subgraph "reviews" are resolvable (they are all declared with their "resolvable" argument set to false).

A subgraph that contributes fields to an entity must be reachable through a resolvable key, because the router gets into it by sending a representation. resolvable: false is for the opposite situation: a subgraph that only returns references to an entity (the Author stub in books).

The gateway

The gateway is a Nest application with ApolloGatewayDriver. It has no resolvers: gateway.supergraphSdl tells it how to obtain the supergraph. IntrospectAndCompose asks every subgraph for its SDL when the gateway starts and composes them in memory. The subgraph URLs come from ConfigService (see Configuration) rather than being hard-coded, so the same module runs in every environment, including the ephemeral-port test below:

graphql-federation/gateway.module.ts
import { IntrospectAndCompose } from '@apollo/gateway';
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    GraphQLModule.forRootAsync<ApolloGatewayDriverConfig>({
      driver: ApolloGatewayDriver,
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        gateway: {
          supergraphSdl: new IntrospectAndCompose({
            subgraphs: [
              { name: 'books', url: config.getOrThrow<string>('BOOKS_URL') },
              { name: 'authors', url: config.getOrThrow<string>('AUTHORS_URL') },
              { name: 'reviews', url: config.getOrThrow<string>('REVIEWS_URL') },
            ],
          }),
        },
      }),
    }),
  ],
})
export class GatewayModule {}

The same gateway module serves code-first and schema-first subgraphs, because it only reads their SDL. The server key of the driver config takes ordinary Apollo Server options (the Nest guide shows it as the place for them), and gateway takes the ApolloGateway options. Start the subgraphs first: with IntrospectAndCompose the gateway fails at startup if a subgraph is not reachable, or if the composition has errors like the one shown above.

A single request now crosses all three subgraphs:

{
  books {
    title
    author { name }
    reviews { rating }
    averageRating
  }
}

This is the response captured from the test run (the gateway planned one request to books, then one _entities request to authors and one to reviews):

{
  "data": {
    "books": [
      {
        "title": "The Dispossessed",
        "author": { "name": "Ursula K. Le Guin" },
        "reviews": [{ "rating": 5 }, { "rating": 4 }],
        "averageRating": 4.5
      },
      {
        "title": "A Wizard of Earthsea",
        "author": { "name": "Ursula K. Le Guin" },
        "reviews": [],
        "averageRating": null
      },
      {
        "title": "Solaris",
        "author": { "name": "Stanislaw Lem" },
        "reviews": [{ "rating": 5 }],
        "averageRating": 5
      }
    ]
  }
}

The root fields of every subgraph are available on the gateway as well, so authors, latestReviews and book(id: "12") { title author { name } } work against the same endpoint. The gateway serves GraphQL at /graphql by default, like a non-federated Nest GraphQL application, and GraphiQL or Apollo Sandbox is available there according to the driver options (see the code-first page).

IntrospectAndCompose is the convenient option for development and small deployments. Apollo’s own documentation recommends composing the supergraph ahead of time (with Rover or managed federation) and running the Apollo Router for production traffic; supergraphSdl accepts a supergraph schema string or a custom supergraph manager as well, so the Nest wiring stays the same when you move to that setup.

Schema-first subgraphs

A subgraph written schema-first declares the entity in SDL and uses the same @ResolveReference() decorator on a resolver class named after the type. The Federation 2 opt-in is the @link directive at the top of the file:

graphql-federation/authors-sdl.graphql
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])

type Author @key(fields: "id") {
  id: ID!
  name: String!
}

type Query {
  authors: [Author!]!
}
graphql-federation/authors-sdl.subgraph.ts
import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import {
  GraphQLModule,
  Query,
  ResolveReference,
  Resolver,
} from '@nestjs/graphql';
import { join } from 'node:path';
import { AUTHORS } from './data.js';

@Resolver('Author')
export class AuthorsSdlResolver {
  @Query()
  authors() {
    return AUTHORS;
  }

  @ResolveReference()
  resolveReference(reference: { __typename: string; id: string }) {
    return AUTHORS.find((author) => author.id === reference.id);
  }
}

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      typePaths: [join(process.cwd(), 'src/graphql-federation/*.graphql')],
    }),
  ],
  providers: [AuthorsSdlResolver],
})
export class AuthorsSdlModule {}

This subgraph answers both authors and a _entities request for Author id 1. The typePaths glob here is built from the process working directory; the Nest guide’s own example uses '*/.graphql'. Adding a glob for the whole src tree would also pick up the SDL of any other schema-first subgraph in the same repository, so keep one folder per subgraph.

Federation 1 and Federation 2

Federation 2 removed the idea that an entity has an originating subgraph. The practical differences in Nest code:

Federation 1 Federation 2

Enable (code-first)

autoSchemaFile: true

autoSchemaFile: { federation: 2 }

Enable (schema-first)

nothing

extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"])

Extending an entity

@Directive('@extends') on the class, @Directive('@external') on the key field, declared with extend type in SDL

the same @key declaration in every subgraph; no @extends, no @external on the key

Root types

subgraphs extend type Query

type Query, no extend

Entity stubs

none

@key(fields: "id", resolvable: false) for reference-only subgraphs

Directives other than the key are attached the same way: every federation or custom directive goes through @Directive('@name(args)') on a class or field (for instance @Directive('@shareable') for a value type that several subgraphs resolve, or @Directive('@external')). The decorator takes the directive exactly as it would be written in SDL, as a string, so TypeScript cannot check it: a mistake in a directive surfaces when the gateway composes the supergraph, not at compile time. See GraphQL: Code-First & Schema-First for @Directive() in general and for custom directives.

One process per subgraph

A subgraph is a separate deployable, and in a test it has to be a separate process too. @nestjs/graphql stores the metadata of every @ObjectType() in process-global storage. Two subgraphs started in the same Node.js process (as one would do with two Test.createTestingModule() calls in one test file) both see all the object types, and the schema build fails because both Author classes and both Book classes collide:

Error: Schema must contain uniquely named types but contains multiple types named "Author".

A small entry point starts one subgraph by name, importing only the module it needs, on the port given as the second argument (0 asks the operating system for a free port, which the process prints for the test to read):

graphql-federation/serve.ts
import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';

// One subgraph per process, as in production: `node serve.js books [port]`.
// Each module is imported on demand because @nestjs/graphql keeps its type
// metadata in process-global storage: two subgraphs cannot share a process.
const subgraphs = {
  books: () => import('./books.subgraph.js').then((m) => m.BooksModule),
  authors: () => import('./authors.subgraph.js').then((m) => m.AuthorsModule),
  reviews: () => import('./reviews.subgraph.js').then((m) => m.ReviewsModule),
  'authors-sdl': () => import('./authors-sdl.subgraph.js').then((m) => m.AuthorsSdlModule),
};
const mercuriusSubgraphs = {
  'm-books': () => import('./mercurius-books.subgraph.js').then((m) => m.MercuriusBooksModule),
  'm-authors': () => import('./mercurius-authors.subgraph.js').then((m) => m.MercuriusAuthorsModule),
};

const [name, port = '0'] = process.argv.slice(2);
const host = '127.0.0.1';

let app;
if (name in subgraphs) {
  const module = await subgraphs[name as keyof typeof subgraphs]();
  app = await NestFactory.create(module, { logger: false });
} else {
  const module = await mercuriusSubgraphs[name as keyof typeof mercuriusSubgraphs]();
  app = await NestFactory.create<NestFastifyApplication>(module, new FastifyAdapter(), { logger: false });
}
await app.listen(Number(port), host);
const address = app.getHttpServer().address();
console.log(`READY http://${host}:${address.port}/graphql`);

In a real repository each subgraph is its own app with its own main.ts, and this is not an issue; it only matters for tests and for "all-in-one" local scripts. The end-to-end test compiles the folder, spawns the subgraphs, starts the gateway in the test process (it has no object types) and queries across them:

test/graphql-federation.e2e-spec.ts (excerpt)
function startSubgraph(name: string): Promise<string> {
  const child = spawn('node', ['dist-federation/src/graphql-federation/serve.js', name], { stdio: ['ignore', 'pipe', 'inherit'] });
  children.push(child);
  return new Promise((resolve, reject) => {
    child.on('exit', (code) => reject(new Error(`${name} exited with ${code}`)));
    child.stdout!.on('data', (chunk: Buffer) => {
      const match = /READY (\S+)/.exec(chunk.toString());
      if (match) resolve(match[1]);
    });
  });
}

beforeAll(async () => {
  booksUrl = await startSubgraph('books');
  authorsUrl = await startSubgraph('authors');
  reviewsUrl = await startSubgraph('reviews');
  process.env.BOOKS_URL = booksUrl;
  process.env.AUTHORS_URL = authorsUrl;
  process.env.REVIEWS_URL = reviewsUrl;
  [gateway] = await startExpress(GatewayModule);
}, 60_000);

it('the gateway composes one query across three subgraphs', async () => {
  const res = await request(gateway.getHttpServer()).post('/graphql').send({
    query: '{ books { title author { name } reviews { rating } averageRating } }',
  });
  expect(res.body.errors).toBeUndefined();
  expect(res.body.data.books[0]).toEqual({
    title: 'The Dispossessed',
    author: { name: 'Ursula K. Le Guin' },
    reviews: [{ rating: 5 }, { rating: 4 }],
    averageRating: 4.5,
  });
});

Source: NestJS: Testing

startExpress() is the usual Test.createTestingModule() followed by app.listen(0, '127.0.0.1'), and the afterAll hook closes the gateway and kills the child processes; remember to do that in your own harness or the subgraphs outlive the test run.

Federation with Mercurius

Mercurius federation is the Fastify-based alternative: subgraphs use MercuriusFederationDriver and the gateway uses MercuriusGatewayDriver, which registers @mercuriusjs/gateway on the Fastify instance. The applications must use FastifyAdapter (see Fastify & HTTP Platforms); both drivers throw No support for current HttpAdapter on Express. The Nest guide documents Federation 1 for Mercurius and warns that Federation 2 is not fully supported, so the example uses the Federation 1 conventions (@extends, @external, autoSchemaFile: true).

The authors subgraph is the same entity as before, with the Mercurius driver:

graphql-federation/mercurius-authors.subgraph.ts
import { Module } from '@nestjs/common';
import {
  Directive,
  Field,
  GraphQLModule,
  ID,
  ObjectType,
  Query,
  ResolveReference,
  Resolver,
} from '@nestjs/graphql';
import {
  MercuriusFederationDriver,
  MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { AUTHORS, type AuthorRecord } from './data.js';

@ObjectType()
@Directive('@key(fields: "id")')
export class Author {
  @Field(() => ID)
  id: string;

  @Field()
  name: string;
}

@Resolver(() => Author)
export class AuthorsResolver {
  @Query(() => [Author])
  authors(): AuthorRecord[] {
    return AUTHORS;
  }

  @ResolveReference()
  resolveReference(reference: { __typename: string; id: string }) {
    return AUTHORS.find((author) => author.id === reference.id);
  }
}

@Module({
  imports: [
    GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
      driver: MercuriusFederationDriver,
      autoSchemaFile: true,
    }),
  ],
  providers: [AuthorsResolver],
})
export class MercuriusAuthorsModule {}

The books subgraph extends Author with a books field, the Federation 1 way: @extends on the class and @external on the key. @ObjectType() without a name uses the class name, so the class is called Author to match the type defined by the authors subgraph:

graphql-federation/mercurius-books.subgraph.ts
import { Module } from '@nestjs/common';
import {
  Directive,
  Field,
  GraphQLModule,
  ID,
  ObjectType,
  Parent,
  Query,
  ResolveField,
  Resolver,
} from '@nestjs/graphql';
import {
  MercuriusFederationDriver,
  MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { BOOKS, type BookRecord } from './data.js';

@ObjectType()
@Directive('@key(fields: "id")')
export class Book {
  @Field(() => ID)
  id: string;

  @Field()
  title: string;
}

// Federation 1 style: the Author entity lives in the authors subgraph and is
// extended here with a `books` field.
@ObjectType()
@Directive('@extends')
@Directive('@key(fields: "id")')
export class Author {
  @Field(() => ID)
  @Directive('@external')
  id: string;

  @Field(() => [Book])
  books?: Book[];
}

@Resolver(() => Book)
export class BooksResolver {
  @Query(() => [Book])
  books(): BookRecord[] {
    return BOOKS;
  }
}

@Resolver(() => Author)
export class AuthorBooksResolver {
  @ResolveField(() => [Book])
  books(@Parent() author: { id: string }): BookRecord[] {
    return BOOKS.filter((book) => book.authorId === author.id);
  }
}

@Module({
  imports: [
    GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
      driver: MercuriusFederationDriver,
      autoSchemaFile: true,
      buildSchemaOptions: { orphanedTypes: [Author] },
    }),
  ],
  providers: [BooksResolver, AuthorBooksResolver],
})
export class MercuriusBooksModule {}

The Mercurius gateway takes a list of services (the Mercurius counterpart of IntrospectAndCompose.subgraphs):

graphql-federation/mercurius-gateway.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
import {
  MercuriusGatewayDriver,
  MercuriusGatewayDriverConfig,
} from '@nestjs/mercurius';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    GraphQLModule.forRootAsync<MercuriusGatewayDriverConfig>({
      driver: MercuriusGatewayDriver,
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        gateway: {
          services: [
            { name: 'books', url: config.getOrThrow<string>('BOOKS_URL') },
            { name: 'authors', url: config.getOrThrow<string>('AUTHORS_URL') },
          ],
        },
      }),
    }),
  ],
})
export class MercuriusGatewayModule {}

Querying the Fastify gateway (through app.inject() in the test) walks from the authors subgraph to the books subgraph:

{ authors { name books { title } } }
{"data":{"authors":[{"name":"Ursula K. Le Guin","books":[{"title":"The Dispossessed"},{"title":"A Wizard of Earthsea"}]},{"name":"Stanislaw Lem","books":[{"title":"Solaris"}]}]}}

In the test the gateway application is initialised with await app.getHttpAdapter().getInstance().ready() before the first inject(), so that the gateway has finished starting before the request is sent.

Pitfalls

  • Subscriptions. The Nest guide states that federation does not support subscriptions. Keep subscription resolvers (see GraphQL: Subscriptions, Security & Plugins) in a regular GraphQL service that clients connect to directly.

  • Stubs need orphanedTypes. A type that exists only to be referenced (the Author stub, a Book extended by another subgraph) is not reachable from any query unless a field returns it; add it to buildSchemaOptions.orphanedTypes so it ends up in the subgraph schema.

  • Contributing subgraphs need a resolvable key. resolvable: false on the key of an entity that a subgraph contributes fields to makes composition fail at gateway startup, as shown above.

  • Representations only carry key fields. resolveReference() receives { __typename, …​keyFields }. A reference resolver that dereferences anything else gets undefined; fetch the missing data inside the resolver.

  • One process per subgraph. Type metadata is process-global, so two subgraphs in one process collide.

  • Start order. With IntrospectAndCompose the gateway introspects the subgraphs when it boots, so subgraphs must already be listening.

  • Cross-cutting concerns. Authentication usually belongs at the gateway (verify the token, forward user headers or claims to the subgraphs in a data source or plugin); each subgraph must still authorize its own fields because it is also reachable on its own URL. See Authorization and GraphQL: Subscriptions, Security & Plugins.

  • N+1 across subgraphs. The router batches entity lookups into one _entities request, but inside resolveReference() you still query your own database once per representation unless you batch with DataLoader (see Performance and the N+1 problem).

  • Mercurius and Federation 2. Mercurius is documented as supporting Federation 1; do not mix Mercurius subgraphs with a Federation 2 Apollo supergraph.

Where the book is outdated

The book treats federation conceptually (a chapter on GraphQL and Apollo Federation with no federated code), which leaves the NestJS-specific parts to the official guide. Two things from older tutorials do not carry over to the current packages:

  • Federation is selected through the mandatory driver option of GraphQLModule.forRoot() (ApolloFederationDriver, ApolloGatewayDriver, MercuriusFederationDriver, MercuriusGatewayDriver).

  • Federation 1 conventions (extend type Query, @extends, @external on keys) are still accepted but are the legacy style; new Apollo subgraphs should use autoSchemaFile: { federation: 2 } or the @link directive.

  • The drivers build the subgraph schema with @apollo/subgraph for you (it is the package the guide tells you to install); you never build or print the federated schema by hand.

What changed recently

  • @nestjs/graphql 14, @nestjs/apollo 14 and @nestjs/mercurius 14 are the releases that pair with NestJS 12 and Apollo Server 5 (@apollo/server 5.5.1 as of 2026-10-02); the guide’s examples now use .js import extensions to suit ESM, as the examples on this page do.

  • The Mercurius packages are separate: the gateway is @mercuriusjs/gateway (5.x), loaded on demand by MercuriusGatewayDriver, and @mercuriusjs/federation is an optional peer of @nestjs/mercurius.

  • Federation 2 support in code-first is the autoSchemaFile: { federation: 2 } option; with @nestjs/graphql 14.0.3 the generated SDL links federation spec 2.14 (@link(url: "https://specs.apollo.dev/federation/v2.14", …​)), which a @apollo/gateway 2.14.4 composition accepted.

  • For production composition Apollo steers users toward the Router and managed or pre-composed supergraphs rather than IntrospectAndCompose; the Nest wiring (supergraphSdl) accommodates both.