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 |
|
|
Gateway |
|
|
Federation version |
1 or 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.
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
Source: NestJS: GraphQL, Federation
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 |
|
|
authors |
|
|
reviews |
|
|
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:
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.
-
@Directive('@key(fields: "id")')on theBookclass declaresBookan entity: other subgraphs can identify aBookby itsid, and the router may ask this subgraph for aBookby that key. -
@ResolveReference()marks the method the router’sentitiesrequest 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. -
autoSchemaFile: { federation: 2 }switches the schema builder to Federation 2 and makes Nest add the@linkdirective that imports the federation directives. PassingautoSchemaFile: trueproduces a Federation 1 schema instead.
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
authorfield does not load an author. It returns an object withtypenameand the key; the gateway usestypenameto find the subgraph that ownsAuthorand sends it a representation to resolve. This is why the books subgraph has no dependency on the authors data at all. -
Authoris declared here only as a stub.resolvable: falsetells the supergraph composition that this subgraph can reference anAuthorbut never be asked to produce one. A stub class that is not reachable from a query or mutation has to be added tobuildSchemaOptions.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:
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 {}
Source: NestJS: GraphQL, Federation 2
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).
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:
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:
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!]!
}
Source: NestJS: GraphQL, Federation 2
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) |
|
|
Enable (schema-first) |
nothing |
|
Extending an entity |
|
the same |
Root types |
subgraphs |
|
Entity stubs |
none |
|
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):
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:
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:
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:
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):
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 (theAuthorstub, aBookextended by another subgraph) is not reachable from any query unless a field returns it; add it tobuildSchemaOptions.orphanedTypesso it ends up in the subgraph schema. -
Contributing subgraphs need a resolvable key.
resolvable: falseon 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 getsundefined; 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
IntrospectAndComposethe 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
_entitiesrequest, but insideresolveReference()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
driveroption ofGraphQLModule.forRoot()(ApolloFederationDriver,ApolloGatewayDriver,MercuriusFederationDriver,MercuriusGatewayDriver). -
Federation 1 conventions (
extend type Query,@extends,@externalon keys) are still accepted but are the legacy style; new Apollo subgraphs should useautoSchemaFile: { federation: 2 }or the@linkdirective. -
The drivers build the subgraph schema with
@apollo/subgraphfor 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/graphql14,@nestjs/apollo14 and@nestjs/mercurius14 are the releases that pair with NestJS 12 and Apollo Server 5 (@apollo/server5.5.1 as of 2026-10-02); the guide’s examples now use.jsimport 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 byMercuriusGatewayDriver, and@mercuriusjs/federationis an optional peer of@nestjs/mercurius. -
Federation 2 support in code-first is the
autoSchemaFile: { federation: 2 }option; with@nestjs/graphql14.0.3 the generated SDL links federation spec 2.14 (@link(url: "https://specs.apollo.dev/federation/v2.14", …)), which a@apollo/gateway2.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.