GraphQL Federation
As an API grows, keeping one monolithic GraphQL schema becomes a bottleneck: every team ships to the same codebase and a single resolver layer owns every type. Apollo Federation solves this by splitting the graph into independently deployed subgraphs that a gateway composes into one unified supergraph. Each service owns the types and fields it is responsible for, yet clients still query a single endpoint. NestJS supports federation natively through @nestjs/graphql and the Apollo Federation driver, so you build subgraphs the same way you build any code-first schema.
Installing the federation drivers
Federation in NestJS uses dedicated drivers built on top of @apollo/subgraph and @apollo/gateway. Install the packages alongside the standard GraphQL tooling.
npm install @nestjs/graphql @nestjs/apollo @apollo/subgraph @apollo/gateway graphql
Each subgraph is a normal Nest application that registers GraphQLModule with ApolloFederationDriver. The gateway is a separate Nest application that uses ApolloGatewayDriver and never defines types of its own.
Building a subgraph
A subgraph declares the entities it owns and exposes a federated schema. Mark an object type with @Directive('@key(...)') to declare its entity key — the field(s) the gateway uses to identify and merge instances of that type across services. Here is a users subgraph.
// users/user.entity.ts
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType()
@Directive('@key(fields: "id")')
export class User {
@Field(() => ID)
id: string;
@Field()
name: string;
@Field()
email: string;
}
The resolver provides both the normal query fields and a reference resolver. The __resolveReference method is invoked by the gateway when another subgraph references a User by its key — it receives the key fields and returns the full entity.
// users/users.resolver.ts
import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql';
import { UsersService } from './users.service';
import { User } from './user.entity';
@Resolver(() => User)
export class UsersResolver {
constructor(private readonly usersService: UsersService) {}
@Query(() => User, { nullable: true })
user(@Args('id') id: string): User | undefined {
return this.usersService.findById(id);
}
@ResolveReference()
resolveReference(reference: { __typename: string; id: string }): User | undefined {
return this.usersService.findById(reference.id);
}
}
Register the subgraph with the federation driver. Setting autoSchemaFile to true keeps the generated SDL in memory, which is convenient during development.
// users/app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import {
ApolloFederationDriver,
ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { UsersResolver } from './users.resolver';
import { UsersService } from './users.service';
@Module({
imports: [
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: { federation: 2 },
}),
],
providers: [UsersResolver, UsersService],
})
export class AppModule {}
Extending an entity from another subgraph
A second subgraph can add fields to a type it does not own. The posts subgraph references User as an external entity and contributes an author relationship, while extending User with a posts field.
// posts/post.entity.ts
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
@Field(() => ID)
id: string;
@Field()
title: string;
@Field(() => User)
author: User;
}
@ObjectType()
@Directive('@key(fields: "id")')
export class User {
@Field(() => ID)
@Directive('@external')
id: string;
@Field(() => [Post])
posts: Post[];
}
The posts subgraph resolves only the posts field for User; the gateway fetches the rest from the users subgraph using the shared id key.
// posts/posts.resolver.ts
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { PostsService } from './posts.service';
import { Post, User } from './post.entity';
@Resolver(() => User)
export class UserPostsResolver {
constructor(private readonly postsService: PostsService) {}
@ResolveField(() => [Post])
posts(@Parent() user: User): Post[] {
return this.postsService.findByAuthor(user.id);
}
}
Composing with the gateway
The gateway points at each running subgraph and merges their schemas. Use IntrospectAndCompose for local development; in production you would publish to a managed schema registry instead.
// gateway/app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo';
import { IntrospectAndCompose } from '@apollo/gateway';
@Module({
imports: [
GraphQLModule.forRoot<ApolloGatewayDriverConfig>({
driver: ApolloGatewayDriver,
server: { path: '/graphql' },
gateway: {
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://localhost:3001/graphql' },
{ name: 'posts', url: 'http://localhost:3002/graphql' },
],
}),
},
}),
],
})
export class AppModule {}
A client query against the gateway transparently fans out to both subgraphs.
query {
user(id: "1") {
name
posts { title }
}
}
Output:
{
"data": {
"user": {
"name": "Ada Lovelace",
"posts": [{ "title": "On the Analytical Engine" }]
}
}
}
Federation 2 (
autoSchemaFile: { federation: 2 }) is recommended for new projects. It relaxes the strictextend/@externalrules of v1 and supports shared types, so prefer it unless you must interoperate with a v1 supergraph.
| Concept | Directive / API | Owned by |
|---|---|---|
| Entity key | @Directive('@key(fields: "id")') | The defining subgraph |
| External field | @Directive('@external') | Borrowing subgraph |
| Reference resolution | @ResolveReference() | Subgraph that owns the entity |
| Composition | IntrospectAndCompose / registry | Gateway |
Best practices
- Give each entity a stable, minimal
@key(often anID); avoid keys built from mutable business fields. - Keep one owning subgraph per entity. Other subgraphs should only extend it with their own fields, never redefine ownership.
- Always implement
__resolveReferencefor any entity another subgraph references, or cross-service joins will returnnull. - Prefer Federation 2 and a managed schema registry over
IntrospectAndComposein production to catch composition errors before deploy. - Keep reference resolvers cheap and batch lookups (e.g. with DataLoader) since the gateway may resolve many entities per request.
- Version subgraph schemas independently and run composition checks in CI so a breaking change in one service cannot poison the supergraph.