What Is GraphQL Federation?
GraphQL Federation is an architecture pattern for building a distributed GraphQL API from multiple smaller GraphQL services (called subgraphs). A gateway (called a router) composes these subgraphs into a single unified schema (the supergraph) that clients query as if it were a single API.
Federation solves the problem of scaling GraphQL APIs across teams. Instead of one monolithic schema maintained by every team, each team owns their own subgraph and can develop, deploy, and scale it independently.
Federation Architecture
- Subgraph: An individual GraphQL service that owns a portion of the schema (e.g., Users service, Posts service)
- Supergraph: The composed schema from all subgraphs — what clients see
- Router: The gateway that receives client queries and distributes them across subgraphs
- Schema Registry: A central store for subgraph schemas used for composition and validation
Federation Directives
# Users Subgraph — owns User type
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
bio: String
avatarUrl: String
}
type Query {
user(id: ID!): User
me: User
}
# Posts Subgraph — owns Post type, extends User
type Post @key(fields: "id") {
id: ID!
title: String!
body: String!
author: User!
publishedAt: String
}
# Extend the User type defined in Users subgraph
type User @key(fields: "id") {
id: ID!
posts: [Post!]! # Added by Posts subgraph
postsCount: Int! # Added by Posts subgraph
}
type Query {
post(id: ID!): Post
posts(limit: Int, offset: Int): [Post!]!
}
# Comments Subgraph — owns Comment type, extends Post and User
type Comment @key(fields: "id") {
id: ID!
text: String!
author: User!
post: Post!
createdAt: String!
}
type User @key(fields: "id") {
id: ID!
recentComments: [Comment!]! # Added by Comments subgraph
}
type Post @key(fields: "id") {
id: ID!
comments: [Comment!]! # Added by Comments subgraph
commentsCount: Int! # Added by Comments subgraph
}
Implementing a Subgraph
// users-subgraph/src/index.ts
import { ApolloServer } from '@apollo/server';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { startStandaloneServer } from '@apollo/server/standalone';
import gql from 'graphql-tag';
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@shareable"])
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
bio: String
avatarUrl: String
createdAt: String!
}
type Query {
user(id: ID!): User
users(limit: Int = 20, offset: Int = 0): [User!]!
me: User
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
}
input CreateUserInput {
name: String!
email: String!
}
input UpdateUserInput {
name: String
bio: String
avatarUrl: String
}
`;
const resolvers = {
Query: {
user: async (_: unknown, { id }: { id: string }, ctx: Context) => {
return ctx.prisma.user.findUnique({ where: { id } });
},
users: async (_: unknown, args: { limit: number; offset: number }, ctx: Context) => {
return ctx.prisma.user.findMany({ take: args.limit, skip: args.offset });
},
me: async (_: unknown, __: unknown, ctx: Context) => ctx.currentUser,
},
User: {
// __resolveReference is called by the router when another subgraph
// references a User entity
__resolveReference: async (ref: { id: string }, ctx: Context) => {
return ctx.prisma.user.findUnique({ where: { id: ref.id } });
},
},
};
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers }),
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4001 },
context: async ({ req }) => createContext(req),
});
console.log(`Users subgraph running at ${url}`);
Posts Subgraph with Entity References
// posts-subgraph/src/index.ts
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0",
import: ["@key", "@external"])
type Post @key(fields: "id") {
id: ID!
title: String!
body: String!
author: User!
publishedAt: String
createdAt: String!
}
# Extend User type from Users subgraph
type User @key(fields: "id") {
id: ID!
posts: [Post!]!
postsCount: Int!
}
type Query {
post(id: ID!): Post
posts(limit: Int = 20, offset: Int = 0): [Post!]!
}
`;
const resolvers = {
Post: {
__resolveReference: async (ref: { id: string }, ctx: Context) => {
return ctx.prisma.post.findUnique({ where: { id: ref.id } });
},
author: (post: Post) => {
// Return a reference — the router will resolve this via Users subgraph
return { __typename: 'User', id: post.authorId };
},
},
User: {
// These fields are added to User by this subgraph
posts: async (user: { id: string }, _: unknown, ctx: Context) => {
return ctx.prisma.post.findMany({
where: { authorId: user.id },
orderBy: { createdAt: 'desc' },
});
},
postsCount: async (user: { id: string }, _: unknown, ctx: Context) => {
return ctx.prisma.post.count({ where: { authorId: user.id } });
},
},
Query: {
post: (_, { id }, ctx) => ctx.prisma.post.findUnique({ where: { id } }),
posts: (_, args, ctx) => ctx.prisma.post.findMany({
take: args.limit, skip: args.offset, orderBy: { createdAt: 'desc' },
}),
},
};
Router Configuration
# router.yaml — Apollo Router configuration
supergraph:
listen: 0.0.0.0:4000
subgraphs:
users:
routing_url: http://users-service:4001/graphql
posts:
routing_url: http://posts-service:4002/graphql
comments:
routing_url: http://comments-service:4003/graphql
cors:
origins:
- https://myapp.com
allow_credentials: true
headers:
all:
request:
- propagate:
named: authorization
Federation Best Practices
- Own your types: Each subgraph should own the types it is responsible for and extend others
- Use @key for entities: Every type that is referenced across subgraphs needs a @key directive
- Schema registry: Use Apollo Studio or a similar registry to manage composition and check for breaking changes
- Independent deployment: Each subgraph should be independently deployable and testable
- Start monolith, migrate to federation: Do not start with federation — migrate when team boundaries require it