claude-skills 的 GraphQL 架构实践:Apollo Federation 子图拆分、实体键与超图网关完整指南
【免费下载链接】claude-skills67 Specialized Skills for Full-Stack Developers. Transform Claude Code into your expert pair programmer.项目地址: https://gitcode.com/GitHub_Trending/claud/claude-skills
本篇指南基于 claude-skills 项目中graphql-architect技能的references/federation.md参考文档,系统讲解 Apollo Federation v2.5 下的分布式 GraphQL 架构:从子图(Subgraph)搭建、实体键(@key)与跨子图引用解析,到核心指令语义、网关配置、查询计划优化与错误处理,并辅以仓库内 graphql-architect 技能的工作流与配套参考文档进行源码级纵深展开。读完你将掌握一套可直接复制运行的 Federation 最小可验证实现,并理解超图(Supergraph)组合背后"实体如何跨服务被识别与解析"的核心机制。
Federation 认知框架:子图、超图与网关
Apollo Federation 的核心思路是把一个"逻辑上统一的 GraphQL 图"按领域边界拆分为多个子图(Subgraph),每个子图是一个独立部署、独立拥有 schema 片段与数据的 GraphQL 服务;再由一个网关(Gateway)在运行时拉取各子图 schema、执行组合,形成对外统一的超图(Supergraph)。客户端只与网关对话,网关负责把一次查询拆成跨子图的多次请求并重新组装结果。
在 claude-skills 项目中,这一主题归属于 graphql-architect 技能(domain: api-architecture,role: architect,scope: design)。其核心工作流第 1~3 步明确规定了 schema-first 的设计顺序:
- Domain Modeling—— 将业务域映射到 GraphQL 类型系统;
- Design Schema—— 使用 Federation 指令(
@key等)创建类型、接口、联合类型; - Validate Schema—— 运行 schema composition check,确认所有
@key实体都能正确解析;若组合失败,则逐一检查实体@key、跨子图的类型定义一致性以及@external字段不一致问题后重新组合。
也就是说,本仓库把 Federation 视为"架构师角色"的技能输出物,而 federation.md 正是该技能的权威细节参考(references/federation.md,对应 SKILL.md 中的 Reference Guide 表格:主题 Apollo Federation,在"子图、实体、指令"场景下加载)。
搭建你的第一个子图
Federation 中子图与普通 Apollo Server 的差别在于:schema 必须以@link引入 Federation v2.5 规范,类型需要声明@key,服务端需要用buildSubgraphSchema而非普通 schema 来构造。
子图 SDL
以 users 子图为例,federation.md 给出的users-subgraph/schema.graphql如下:
# users-subgraph/schema.graphql extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key", "@shareable"]) type User @key(fields: "id") { id: ID! email: String! username: String! createdAt: DateTime! } type Query { user(id: ID!): User users: [User!]! }几个关键点:
extend schema @link(...)是 Federation v2 的规范引入方式,import数组声明本子图实际使用的指令集合(这里只用到@key与@shareable),未被 import 的指令即使安装了包也不会被启用;@key(fields: "id")把User声明为实体(Entity),id是它在整个超图中的稳定身份标识,其他子图可以引用并扩展这个类型;- 标量
DateTime属于自定义标量,可在 schema 中自行声明(参见配套文档 schema-design.md 中的scalar DateTime定义方式)。
子图服务端与引用解析器
子图服务端代码使用@apollo/server与@apollo/subgraph:
// users-subgraph/resolvers.ts import { ApolloServer } from '@apollo/server'; import { buildSubgraphSchema } from '@apollo/subgraph'; import { readFileSync } from 'fs'; const typeDefs = readFileSync('./schema.graphql', 'utf8'); const resolvers = { User: { __resolveReference: async ( reference: { id: string }, context: Context ): Promise<User> => { return context.dataSources.users.findById(reference.id); }, }, Query: { user: async (parent, args: { id: string }, context: Context) => { return context.dataSources.users.findById(args.id); }, users: async (parent, args, context: Context) => { return context.dataSources.users.findAll(); }, }, }; const server = new ApolloServer({ schema: buildSubgraphSchema([{ typeDefs, resolvers }]), });__resolveReference是子图服务端最重要的联邦专用解析器:当网关需要从其他子图拿到一个User实体的字段时,它只携带{ __typename: 'User', id: "..." }这样的引用(Reference)调用本子图,本子图据此回填完整实体。配合 resolvers.md 中的规范,findById这类数据源调用应走 dataSource + DataLoader 批量缓存,避免跨实体解析时产生 N+1(该主题在 resolvers.md 有专门章节)。
实体键(Entity Keys):单键、复合键与多键
实体键决定了一个类型"如何被其他子图识别"。Federation 支持三种键形态,federation.md 的products-subgraph/schema.graphql完整覆盖了这三种:
# products-subgraph/schema.graphql extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: [ "@key", "@shareable", "@interfaceObject" ]) # Single key field type Product @key(fields: "id") { id: ID! name: String! price: Float! sku: String! @shareable } # Composite key type Variant @key(fields: "productId sku") { productId: ID! sku: String! size: String! color: String! } # Multiple keys (different ways to identify) type Review @key(fields: "id") @key(fields: "productId authorId") { id: ID! productId: ID! authorId: ID! rating: Int! content: String! }三种键的适用场景:
| 键形态 | 写法 | 适用场景 |
|---|---|---|
| 单字段键 | @key(fields: "id") | 类型有唯一主键,最常见的形态 |
| 复合键 | @key(fields: "productId sku") | 单字段不足以唯一标识(如按仓库+SKU 区分的变体) |
| 多键 | @key(fields: "id") @key(fields: "productId authorId") | 存在多套等价的身份体系,网关可按任一组合解析 |
注意复合键中多个字段之间用空格分隔(Federation 的字段集语法,并非逗号)。sku: String! @shareable表明该字段允许被多个子图解析(本例中 products 与 reviews 都可能提供 sku),组合时不会因"多来源冲突"而失败——关于@shareable的详细语义见下文指令章节。
从实现层面看,多键会要求子图为每个@key组合都具备可解析能力;配合@interfaceObject(见后文)可以进一步让不知道具体实现类型的子图也安全地持有实体引用。这一设计思路与 microservices-architect 中"按有界上下文划分服务边界、每个服务独占数据"的原则同源:@key本质上是跨服务身份(Identity)契约。
跨子图扩展类型:extend 与 @external
Federation 的"扩展"是联合 API 的核心能力:一个实体类型在其拥有者子图定义主体字段,其他子图通过extend type ... @key补充新字段,被补充的既有字段必须在扩展子图中标记@external。
federation.md 用 users 与 posts 两个子图演示了完整链路。
SDL 侧
# users-subgraph: owns User type User @key(fields: "id") { id: ID! email: String! username: String! } # posts-subgraph: extends User with posts extend type User @key(fields: "id") { id: ID! @external posts: [Post!]! } type Post @key(fields: "id") { id: ID! title: String! content: String! authorId: ID! author: User! }解析器侧
// posts-subgraph/resolvers.ts const resolvers = { User: { // Reference resolver: fetch User stub by id __resolveReference: async ( reference: { id: string }, context: Context ) => { return { id: reference.id }; }, // Field resolver: resolve posts for User posts: async (user: { id: string }, args, context: Context) => { return context.dataSources.posts.findByAuthor(user.id); }, }, Post: { // Resolve author as User entity reference author: (post: Post) => { return { __typename: 'User', id: post.authorId }; }, }, };这段代码展现了三个相互配合的机制:
__resolveReference返回 stub:posts 子图并不拥有User数据,它只需要{ id }作为占位对象即可挂载posts字段解析器,无需真的查询用户表;- 字段解析器基于引用对象工作:
posts: async (user: { id: string }, ...)直接用引用中的id去查该用户的文章; - 返回引用对象:
author: (post) => ({ __typename: 'User', id: post.authorId })——解析器并不返回完整 User,而是返回"实体引用",网关会拿着这个引用去 users 子图进一步解析客户端请求的email、username等字段。这就是超图跨服务"拼接"的最小闭环。
这一"引用传递 → 目标子图回填"的往返由网关的**查询计划(Query Plan)**驱动,后续"查询计划优化"章节会讨论如何减少这类往返。
Federation 核心指令全解析
federation.md 用一段大而全的 SDL 一次引入了全部常用指令,是理解指令语义的最佳速查表:
extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: [ "@key", "@requires", "@provides", "@external", "@shareable", "@override", "@inaccessible", "@tag" ]) # @key: Define entity with primary key type Product @key(fields: "id") { id: ID! name: String! } # @external: Field defined in another subgraph extend type User @key(fields: "id") { id: ID! @external email: String! @external isVerified: Boolean! @external } # @requires: Field needs external data extend type User @key(fields: "id") { id: ID! @external email: String! @external isVerified: Boolean! @external # Can only compute if we have email and isVerified canPost: Boolean! @requires(fields: "email isVerified") } # @provides: Optimization hint type Post @key(fields: "id") { id: ID! author: User! @provides(fields: "username") } # @shareable: Field can be resolved by multiple subgraphs type Product @key(fields: "id") { id: ID! sku: String! @shareable name: String! } # @override: Migration between subgraphs type Product @key(fields: "id") { id: ID! # Override from legacy-subgraph price: Float! @override(from: "legacy-subgraph") } # @inaccessible: Hide from supergraph type User @key(fields: "id") { id: ID! email: String! internalId: String! @inaccessible } # @tag: Organize schema type Query { products: [Product!]! @tag(name: "public") adminUsers: [User!]! @tag(name: "admin") }逐一说明各指令的职责:
@key(fields: "..."):把类型声明为实体并给出身份字段集,是 Federation 的基石;@external:声明该字段由其他子图拥有/解析,本子图仅"借用"其值参与本地的@requires计算或返回引用;标记了@external的字段在本子图中不会触发解析(组合后的超图中该字段仍由原始拥有者解析);@requires(fields: "email isVerified"):声明本子图计算canPost需要依赖从其他子图拿到的email与isVerified值。网关在路由查询时会先向 users 子图请求这两个字段,再回传到 posts 子图供canPost使用——这是一个显式的跨子图数据依赖契约;@provides(fields: "username"):优化提示。声明本子图在解析author时可以顺带提供username,从而让网关在查询计划阶段决定"User 的 username 是否还需要单独回 users 子图取",减少一跳往返(详见"查询计划优化"章节);@shareable:允许同一字段被多个子图解析且结果一致,组合时消除"字段归属冲突";适合sku、name这类跨服务等价的只读数据;@override(from: "legacy-subgraph"):字段迁移利器。声明本子图的price优先于legacy-subgraph的同名字段,用于服务边界重组或数据所有权交接时平滑切换;@inaccessible:把字段从对外超图 schema 中隐藏,但保留在子图间内部可用的能力(如internalId),防止内部实现细节泄漏到客户端;@tag(name: "..."):为 schema 元素打标签做组织编排(如public/admin),配合网关侧或发布侧的策略使用,不影响解析行为。
从仓库技能约束看,graphql-architect 的 MUST DO 明确要求"正确使用 federation directives",且 schema 采用 schema-first 方式编写——上面这套 SDL 正是该约束的落地范本。
网关配置:把子图组合成超图
网关使用@apollo/gateway,核心是提供子图清单与组合策略。federation.md 给出的gateway/server.ts是自托管组合(IntrospectAndCompose)的标准形态:
// gateway/server.ts import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway'; import { ApolloServer } from '@apollo/server'; const gateway = new ApolloGateway({ supergraphSdl: new IntrospectAndCompose({ subgraphs: [ { name: 'users', url: 'http://localhost:4001/graphql' }, { name: 'posts', url: 'http://localhost:4002/graphql' }, { name: 'products', url: 'http://localhost:4003/graphql' }, ], // Poll for schema updates pollIntervalInMs: 10000, }), // Error handling serviceHealthCheck: true, // Query planning debug debug: process.env.NODE_ENV === 'development', }); const server = new ApolloServer({ gateway, // Context propagation to subgraphs context: async ({ req }) => { const token = req.headers.authorization || ''; return { token }; }, }); await server.listen(4000); console.log('Gateway ready at http://localhost:4000');配置项语义:
subgraphs数组:声明参与组合的子图名称与 HTTP 地址。网关启动时逐一 introspection 拉取子图 SDL,执行**组合(Composition)**生成超图 schema;组合失败会直接导致网关无法启动,因此 CI 中的组合校验(composition check)是上线前的强制关卡(见最佳实践第 7 条);pollIntervalInMs: 10000:每 10 秒轮询子图 schema 是否有更新。轮询到变化后会重新组合并热替换超图 schema,实现"子图先发布、网关自动跟随"的无缝演进;生产环境若要求更高的确定性,可改用下文的 Managed Federation;serviceHealthCheck: true:网关转发前会探测子图健康状态,子图不可用时在查询计划阶段即降级/报错,避免请求打到坏节点;debug: process.env.NODE_ENV === 'development':仅在开发环境输出查询计划等调试信息;- context 传播:网关的
context会注入每个子图请求,上例把Authorization头原样透传,是联邦认证透传的最简实现。若子图还需要其他头(如x-client-type),可在此处统一组装。
需要注意:此例中 context 只传了token一个字段。如果子图解析器依赖完整认证上下文(见 security.md 中的 JWT 校验与requireAuth模式),需要保证网关透传的信息足以让子图完成鉴权,否则会出现"网关已验证、子图未识别"的鉴权空洞。
Managed Federation:由 Apollo Studio 托管组合
当子图数量增长、发布频率提高后,网关侧轮询自组合会带来运维与一致性成本。Managed Federation 把"组合 + 分发"托管给 Apollo Studio(通过 Apollo Uplink),网关不再持有子图 URL 清单,而是从 Uplink 拉取已经组合好的超图 SDL:
// gateway/server.ts with managed federation import { ApolloGateway } from '@apollo/gateway'; import { ApolloServer } from '@apollo/server'; const gateway = new ApolloGateway({ // No subgraph URLs needed - fetched from Apollo Studio // Schema composition happens in Apollo Studio async supergraphSdl({ update }) { // Fetch from Apollo Uplink const supergraphSdl = await fetchSupergraphSdl(); return { supergraphSdl, cleanup: async () => {}, }; }, }); // Subgraph reporting to Apollo Studio import { ApolloServerPluginInlineTrace } from '@apollo/server/plugin/inlineTrace'; const subgraphServer = new ApolloServer({ schema: buildSubgraphSchema([{ typeDefs, resolvers }]), plugins: [ ApolloServerPluginInlineTrace(), ], });两个配套动作:
- 网关侧提供
supergraphSdl回调:从 Uplink 拉取最新组合结果,update回调可用于监听 Uplink 推送的 schema 更新实现热替换;cleanup在轮换时释放资源; - 子图侧上报追踪:通过
ApolloServerPluginInlineTrace把子图内解析耗时内联进响应,配合 Studio 的联邦指标(字段级延迟、实体解析次数)定位跨子图性能瓶颈——对应最佳实践第 9 条"监控查询计划与解析器性能"。
托管模式的价值在于:schema 变更先在 Studio 做组合预检,失败的组合不会推到生产网关,子图发布与网关发布彻底解耦(最佳实践第 8 条"用 managed federation 保证安全部署")。
Value Types 与 Entity:何时用谁
并非所有类型都需要@key。Federation 区分两类类型:
- Value Type(值类型):没有
@key,完全由一个子图拥有和解析,其他子图不能扩展它; - Entity(实体):有
@key,可被其他子图扩展并引用。
federation.md 用Address演示了这层关系:
# Value type: no @key, resolved entirely by one subgraph type Address { street: String! city: String! country: String! postalCode: String! } # Entity: has @key, can be extended by other subgraphs type User @key(fields: "id") { id: ID! email: String! # Value type embedded in entity address: Address } # Another subgraph can extend User but not Address extend type User @key(fields: "id") { id: ID! @external orders: [Order!]! }判据很直接:"是否需要被多个子图共享/扩展"是加@key的唯一理由。Address只是 User 的嵌入数据,随 User 一起被解析,因此保持 value type;User会被 orders 子图扩展出orders字段,所以必须是实体。误把高频嵌入数据设成实体,会无谓增加引用解析的开销和 schema 复杂度。
接口对象(@interfaceObject):不知道实现也能持有接口
跨子图扩展接口类型时存在一个经典难题:扩展方并不认识实现该接口的所有具体类型。@interfaceObject解决这个问题——扩展方用"接口对象"占位持有接口本身。
federation.md 的示例分两个子图:
# accounts-subgraph type User implements Account @key(fields: "id") { id: ID! email: String! role: String! } type AdminUser implements Account @key(fields: "id") { id: ID! email: String! role: String! permissions: [String!]! } interface Account { id: ID! email: String! role: String! } # orders-subgraph (doesn't know about User/AdminUser) extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key", "@interfaceObject"]) type Order @key(fields: "id") { id: ID! account: Account! } # Use @interfaceObject to reference Account without knowing implementations type Account @key(fields: "id") @interfaceObject { id: ID! }accounts-subgraph拥有接口Account及两个实现User/AdminUser;orders-subgraph根本不知道这两个实现类,它通过type Account @key(fields: "id") @interfaceObject声明"我持有的是 Account 接口对象",并把自己的Order.account指向该接口。组合后,网关在 orders 侧返回Account引用时会依据实际运行时类型(__typename为User或AdminUser)路由回 accounts 子图解析实现类字段。@interfaceObject让扩展侧零耦合地参与接口体系,是大型多团队联邦架构下的关键武器。
查询计划优化:减少跨子图往返
网关的查询计划质量直接决定一次客户端查询会触发多少轮子图往返。federation.md 给出了一个"低效 → 优化"的对照:
# Inefficient: requires multiple roundtrips type Query { user(id: ID!): User } type User @key(fields: "id") { id: ID! posts: [Post!]! } extend type Post @key(fields: "id") { id: ID! @external author: User! } # Better: provide data to avoid extra fetch type Post @key(fields: "id") { id: ID! authorId: ID! # Optimization: provide username directly author: User! @provides(fields: "username") } # Gateway can fulfill some User fields from Post subgraph # without fetching from User subgraph低效形态:客户端在 posts 子图取Post.author,又要 User 的username,网关必须拿着User引用再回 users 子图取username,形成额外的往返。
优化形态:posts 子图在解析author时用@provides(fields: "username")声明"我能顺带提供 username",则网关对Post.author { username }这一子查询可直接由 posts 子图完成,不再回 users 子图。
代价与边界:@provides等于子图承诺"这个字段由我保证与拥有者结果一致",因此通常只在字段由 join 数据天然携带时使用(如 posts 表冗余了作者用户名)。使用前应核实数据的时效性与一致性,否则会出现跨子图数据不一致。实践中可先用debug: true观察查询计划(上文的网关配置章节),确认哪些子查询产生了多余往返,再针对性加@provides或调整 schema 边界。
联邦环境下的错误处理
实体引用解析失败有两种等级,federation.md 明确区分了它们:
const resolvers = { User: { __resolveReference: async ( reference: { id: string }, context: Context ) => { try { const user = await context.dataSources.users.findById(reference.id); if (!user) { // Return null for missing entity (soft error) return null; } return user; } catch (error) { // Hard error propagates to client throw new GraphQLError('Failed to resolve user', { extensions: { code: 'USER_RESOLUTION_FAILED', userId: reference.id, }, }); } }, }, };- 软错误(soft error):实体不存在时返回
null,查询继续执行,其他已解析字段照常返回——适合@key引用偶尔失效的弱关联场景; - 硬错误(hard error):底层异常时抛出带
extensions的结构化GraphQLError,错误直接传播到客户端——适合真正不可恢复的失败,code与userId等扩展信息方便客户端与监控侧定位。
这与 resolvers.md 中"抛 GraphQLError 并携带标准 code 与 extensions"的规范一致;若需要返回给客户端额外的 HTTP 状态语义,还可以仿照 security.md 中的http: { status: 404 }扩展字段。需要留意的是:联邦环境下网关会汇总多个子图的错误,客户端看到的是合并后的 errors 数组,因此每个子图的错误 code 应当全局唯一且可枚举,便于跨服务排查。
联邦实践清单:10 条硬规则
federation.md 收尾给出的十条最佳实践,是整个联邦架构的验收标准:
- Entity Design:只为"需要被扩展的类型"使用
@key; - Subgraph Boundaries:子图边界与团队/服务边界对齐(对应 microservices-architect 的有界上下文原则);
- Shared Types:真正跨服务共享且等价的字段才用
@shareable; - Migration:用
@override做子图间的渐进式迁移; - Performance:用
@provides优化查询计划、减少跨子图往返; - Value Types:嵌入数据用普通 value type,别滥设实体;
- Composition:在 CI/CD 中持续跑 schema 组合校验(组合失败 = 构建失败);
- Versioning:用 Managed Federation 保障 schema 变更的安全发布;
- Monitoring:跟踪查询计划与各子图解析器性能(
@provides是否生效、实体引用解析耗时); - Documentation:文档化"实体归谁所有、由谁扩展",避免所有权混乱。
对照 graphql-architect 的 Constraints,这十条与 MUST DO 高度互洽:schema-first 设计、正确使用 federation directives、schema 校验、文档化全部类型与字段。也就是说,一份符合本清单的超图 schema,也同时满足技能对"架构师角色"的输出要求(schema 定义 + resolver 实现 + 示例查询 + 设计决策说明)。
小结与进一步阅读
Apollo Federation 把"一个 GraphQL 图"拆成"多个可独立部署的子图 + 一个组合网关",用@key实体作为跨服务身份契约,用@external/@requires/@provides等指令声明字段归属、数据依赖与优化机会。掌握本文的子图搭建、实体键设计、跨子图扩展、指令语义、网关与托管配置、查询计划优化和错误处理,即可构建一个可演进、可观测、可安全发布的联邦 GraphQL 架构。
如需继续深入,可在当前仓库中按需查阅:
- 技能总纲与约束:skills/graphql-architect/SKILL.md(工作流、MUST DO / MUST NOT DO、DataLoader 示例、查询复杂度校验示例);
- Schema 设计规范:skills/graphql-architect/references/schema-design.md(类型/接口/联合/输入类型、Relay 游标分页、可空性最佳实践);
- Resolver 与 N+1 治理:skills/graphql-architect/references/resolvers.md(DataLoader 批处理、context 设置、分页解析器);
- 安全加固:skills/graphql-architect/references/security.md(深度限制、复杂度分析、鉴权、allowlist);
- REST 迁移与 BFF:skills/graphql-architect/references/migration-from-rest.md;
- 实时订阅:skills/graphql-architect/references/subscriptions.md(
graphql-ws+ Redis PubSub); - 技能选择指南:SKILLS_GUIDE.md(API Design 决策树:GraphQL → GraphQL Architect)。
【免费下载链接】claude-skills67 Specialized Skills for Full-Stack Developers. Transform Claude Code into your expert pair programmer.项目地址: https://gitcode.com/GitHub_Trending/claud/claude-skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考