GraphQL Federation: объединение микросервисов в единый граф
GraphQL Federation (Apollo Federation 2) позволяет разным командам владеть разными частями GraphQL-схемы. Каждый сервис публикует свой subgraph, Router объединяет их в единый federated graph. Клиент видит одну точку входа, один язык запросов — не знает о внутренней микросервисной структуре.
Архитектура
Клиент → Router (Apollo Router) → Users Subgraph
→ Products Subgraph
→ Orders Subgraph
→ Reviews Subgraph
Router принимает GraphQL-запрос, строит план выполнения (query plan) и параллельно или последовательно обращается к нужным subgraph'ам, объединяя результаты.
Subgraph: Users Service
# users-service/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3",
import: ["@key", "@shareable"])
type Query {
me: User
user(id: ID!): User
}
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
createdAt: String!
}
// users-service/server.js
import { ApolloServer } from '@apollo/server'
import { buildSubgraphSchema } from '@apollo/subgraph'
import { gql } from 'graphql-tag'
const typeDefs = gql`...` // schema выше
const resolvers = {
Query: {
me: (parent, args, context) => context.db.users.findById(context.userId),
user: (parent, { id }, context) => context.db.users.findById(id)
},
User: {
// Reference resolver: Router запрашивает User по id из другого сервиса
__resolveReference: async ({ id }, context) => {
return context.db.users.findById(id)
}
}
}
const server = new ApolloServer({
schema: buildSubgraphSchema({ typeDefs, resolvers })
})
Subgraph: Products Service
# products-service/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3",
import: ["@key", "@external", "@requires", "@provides"])
type Query {
products(categoryId: ID, limit: Int): [Product!]!
product(id: ID!): Product
}
type Product @key(fields: "id") {
id: ID!
name: String!
price: Float!
stock: Int!
categoryId: ID!
}
# Расширение типа User из users-service
type User @key(fields: "id") {
id: ID!
# Добавляем поле wishlist к типу User, которым владеет users-service
wishlist: [Product!]!
}
const resolvers = {
Query: {
products: (parent, { categoryId, limit = 20 }, ctx) =>
ctx.db.products.find({ categoryId, limit }),
product: (parent, { id }, ctx) => ctx.db.products.findById(id)
},
Product: {
__resolveReference: ({ id }, ctx) => ctx.db.products.findById(id)
},
User: {
__resolveReference: ({ id }) => ({ id }), // stub — данные User берёт users-service
wishlist: ({ id }, args, ctx) => ctx.db.wishlists.getByUserId(id)
}
}
Subgraph: Orders Service
# orders-service/schema.graphql
extend schema
@link(url: "https://specs.apollo.dev/federation/v2.3",
import: ["@key", "@external", "@requires"])
type Order @key(fields: "id") {
id: ID!
status: OrderStatus!
total: Float!
createdAt: String!
# @external — поле принадлежит users-service
user: User! @external
userId: ID! @external
items: [OrderItem!]!
}
type OrderItem {
product: Product! # ссылка на тип из products-service
quantity: Int!
price: Float!
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
type Query {
order(id: ID!): Order
myOrders: [Order!]!
}
# Расширяем User: добавляем orders к пользователю
type User @key(fields: "id") {
id: ID! @external
orders: [Order!]!
}
# Расширяем Product: добавляем orderCount
type Product @key(fields: "id") {
id: ID! @external
orderCount: Int!
}
Apollo Router: конфигурация
# router.yaml
supergraph:
listen: 0.0.0.0:4000
# Subgraph endpoints
subgraphs:
users:
routing_url: http://users-service:4001/graphql
products:
routing_url: http://products-service:4002/graphql
orders:
routing_url: http://orders-service:4003/graphql
reviews:
routing_url: http://reviews-service:4004/graphql
# Заголовки: прокинуть Authorization во все subgraph'ы
headers:
all:
request:
- propagate:
matching: ^Authorization$
# Корс
cors:
origins:
- https://app.example.com
allow_headers:
- Content-Type
- Authorization
# Трассировка
telemetry:
tracing:
propagation:
request:
header_name: traceparent
exporters:
jaeger:
endpoint: http://jaeger:14268/api/traces
# Limits
limits:
max_depth: 15
max_aliases: 30
max_root_fields: 20
Supergraph composition (CI/CD)
# Установить Rover CLI
curl -sSL https://rover.apollo.dev/nix/latest | sh
# Публикация subgraph в Apollo Studio (или локально)
rover subgraph publish my-graph@prod \
--name users \
--schema ./users-service/schema.graphql \
--routing-url https://users.internal/graphql
# Локальная composición для тестов
rover supergraph compose --config supergraph.yaml > supergraph.graphql
# supergraph.yaml (для локальной разработки)
federation_version: =2.3.5
subgraphs:
users:
routing_url: http://localhost:4001/graphql
schema:
file: ./users-service/schema.graphql
products:
routing_url: http://localhost:4002/graphql
schema:
file: ./products-service/schema.graphql
Пример federated запроса
# Один запрос охватывает данные из 3 сервисов
query OrderDetail($orderId: ID!) {
order(id: $orderId) {
id
status
total
user { # → users-service
name
email
}
items {
quantity
price
product { # → products-service
name
stock
}
}
}
}
Router строит query plan:
- Получить
orderиз orders-service → получитьuserIdиproductId[] - Параллельно: fetch
userиз users-service иproduct[]из products-service - Merge и вернуть клиенту
Entity batching
Router автоматически батчирует запросы к entities через _entities query:
# Router отправляет это в products-service
query {
_entities(representations: [
{ __typename: "Product", id: "1" },
{ __typename: "Product", id: "2" },
{ __typename: "Product", id: "3" }
]) {
... on Product { name stock }
}
}
Поэтому __resolveReference должен поддерживать DataLoader для батчинга:
import DataLoader from 'dataloader'
User: {
__resolveReference: async ({ id }, context) => {
// DataLoader батчирует все __resolveReference вызовы одного запроса
return context.loaders.userById.load(id)
}
}
Срок выполнения
Настройка Apollo Federation 2 с Router, composition pipeline и 3–5 subgraph'ами — 5–8 рабочих дней.







