☰
NestJS GraphQL Federation Schema-First 实战:users-application 子服务完整实现解析
2026/9/30 2:01:26 网站建设 项目流程
  • 后端
  • Web框架

【免费下载链接】nest

A progressive Node.js framework for building efficient, scalable, and enterprise-grade server-side applications with TypeScript/JavaScript 🚀

项目地址:https://gitcode.com/GitHub_Trending/ne/nest
点击查看免费下载

本指南围绕 NestJS 仓库中 GraphQL Federation(Schema-First 模式)示例的 users-application 子服务展开,系统讲解如何用@nestjs/graphql与ApolloFederationDriver构建一个可被联邦网关合并的独立子服务:从 Schema 定义、@key实体标记、@ResolveReference引用解析到模块装配与启动运行。读完你既能照抄一份可运行的联邦子服务骨架,也能理解其在多服务架构中的角色边界。

一、示例在仓库中的位置与整体结构

本示例位于 sample/32-graphql-federation-schema-first/users-application 目录,是 GraphQL Federation(Schema-First)示例套件的用户子服务部分。整个 32 号示例由三个应用组成:

  • gateway:Apollo Gateway,负责合并各子服务的 Schema 并对外统一提供查询入口;
  • users-application:本文主角,提供User实体与getUser查询;
  • posts-application:另一子服务,通过联邦引用(reference)解析User的关联数据。

users-application 的文件布局如下:

users-application/ ├── src/ │ ├── app.module.ts │ ├── main.ts │ └── users/ │ ├── models/user.model.ts │ ├── users.graphql │ ├── users.module.ts │ ├── users.resolver.ts │ ├── users.resolver.spec.ts │ ├── users.service.ts │ └── users.service.spec.ts ├── nest-cli.json ├── package.json ├── tsconfig.json └── vitest.config.mts

子服务本身是标准 NestJS 应用:入口、根模块、业务模块一应俱全,唯一特别之处在于根模块中接入的是GraphQLModule.forRoot且驱动为ApolloFederationDriver(见 users.module.ts)。

二、依赖与脚本:搭建联邦子服务需要什么

package.json 是本子服务的依赖清单,其中与联邦直接相关的核心依赖为:

  • @nestjs/graphql(14.x):NestJS GraphQL 集成层;
  • @nestjs/apollo(14.x):提供ApolloFederationDriver与ApolloFederationDriverConfig;
  • @apollo/subgraph(2.15.x):Apollo 子服务构建工具;
  • @apollo/gateway(2.14.x):网关依赖(此示例中 gateway 与子服务共用同一份依赖约定);
  • graphql(17.x)与graphql-tools(9.x):Schema 解析与工具库;
  • reflect-metadata:Nest 依赖注入与装饰器元数据所需。

项目脚本与 NestJS 标准模板一致,但测试统一走 Vitest:

# 安装依赖 $ npm install # 开发模式 $ npm run start # 监听模式 $ npm run start:dev # 生产模式(先构建再启动) $ npm run start:prod # 单元测试(Vitest) $ npm run test

type: "module"表明该子服务以 ESM 方式运行,这也是main.ts中导入路径带.js后缀的原因(见下文)。vitest.config.mts将测试范围限定为src/**/*.spec.ts并开启全局断言。

三、模块装配:ApolloFederationDriver 的接入方式

3.1 根模块

app.module.ts 只是简单导入UsersModule,不注册任何控制器或全局 provider:

import { Module } from '@nestjs/common'; import { UsersModule } from './users/users.module.js'; @Module({ imports: [UsersModule], controllers: [], providers: [], }) export class AppModule {}

3.2 业务模块:GraphQL 配置

真正的联邦配置在 users.module.ts:

import { ApolloFederationDriver, ApolloFederationDriverConfig, } from '@nestjs/apollo'; import { Module } from '@nestjs/common'; import { GraphQLModule } from '@nestjs/graphql'; import { UsersResolver } from './users.resolver.js'; import { UsersService } from './users.service.js'; @Module({ providers: [UsersResolver, UsersService], imports: [ GraphQLModule.forRoot<ApolloFederationDriverConfig>({ driver: ApolloFederationDriver, typePaths: ['**/*.graphql'], }), ], }) export class UsersModule {}

关键点:

  • driver: ApolloFederationDriver将 GraphQL 执行引擎切换为 Apollo Federation 专用驱动,子服务因此会暴露联邦必需的_service(SDL 查询)与_entities(实体解析)端点;
  • typePaths: ['**/*.graphql']是 Schema-First 的核心配置:NestJS 会按该 glob 自动扫描并合并所有.graphql文件作为 SDL。这正是"Schema-First"与 Code-First(装饰器直接生成 Schema)的区别所在。

四、Schema-First 的 Schema 定义:联邦实体声明

users.graphql 是子服务对外发布的 SDL:

type User @key(fields: "id") { id: ID! name: String! } extend type Query { getUser(id: ID!): User }

逐行解读:

  • @key(fields: "id"):联邦规范的核心指令,声明User类型在本子服务中的主键为id。网关将依据该键把不同子服务中同类型的实体合并为一个完整对象;
  • type User定义了实体字段id(ID!,非空)与name(String!,非空);
  • extend type Query:子服务不拥有全局Query的完整定义,因此用extend把自己的查询getUser(id: ID!): User追加到合并后的根类型上。

Code-First 等价实现对照

本示例同时提供了 Code-First 的联邦版本(见 sample/31-graphql-federation-code-first)。在 Schema-First 中@key写在.graphql文件里,而 Code-First 则用装饰器表达:user.model.ts 展示了如何在 NestJS 中把 SDL 映射为 TypeScript 类:

import { Directive, Field, ID, ObjectType } from '@nestjs/graphql'; @ObjectType() @Directive('@key(fields: "id")') export class User { @Field((type) => ID) id: number; @Field() name: string; }

@Directive('@key(fields: "id")')与.graphql文件中的@key(fields: "id")一一对应,说明无论哪种模式,联邦语义(键与引用解析)都是通过同一套指令体系表达的。该模型文件在本 Schema-First 示例中主要供 Resolver 与 Service 引用类型,Schema 本体仍以 SDL 为准。

五、Resolver:普通查询与联邦引用解析

users.resolver.ts 是子服务的解析层,同时承担两类职责:

import { Args, ID, Query, Resolver, ResolveReference } from '@nestjs/graphql'; import { UsersService } from './users.service.js'; @Resolver('User') export class UsersResolver { constructor(private usersService: UsersService) {} @Query() getUser(@Args({ name: 'id', type: () => ID }) id: number) { return this.usersService.findById(id); } @ResolveReference() resolveReference(reference: { __typename: string; id: number }) { return this.usersService.findById(reference.id); } }

5.1 普通查询:getUser

@Query()对应 SDL 中的getUser(id: ID!): User,参数id声明为ID类型。@Resolver('User')将 Resolver 与User类型绑定,使方法返回值具备联邦实体语义。

5.2 联邦引用解析:resolveReference

@ResolveReference()是联邦子服务的关键钩子:当网关跨子服务合并实体时,会向本子服务发送一个"引用对象"(包含__typename与主键字段id),本方法据此加载并返回完整实体。这里接收的reference结构为{ __typename: string; id: number },与联邦规范中_entities解析器收到的参数一致。

可以推断,网关在posts-application中遇到对User的引用(例如帖子关联作者)时,正是调用这里的方法把id解析回{ id, name }完整对象——这是联邦"单实体、多子服务分工"的数据流核心。

六、Service:数据存取层

users.service.ts 使用内存数组模拟数据源,便于示例零依赖运行:

import { Injectable } from '@nestjs/common'; @Injectable() export class UsersService { private users = [ { id: 1, name: 'John Doe' }, { id: 2, name: 'Richard Roe' }, ]; findById(id: number) { return this.users.find((user) => user.id === Number(id)); } }
  • findById同时服务于getUser查询与resolveReference引用解析,一条数据路径两处复用;
  • Number(id)将字符串/数字形式的ID统一转换为数字比较,规避 GraphQLID标量序列化带来的类型差异;
  • 生产环境中该内存数组可替换为 TypeORM、Prisma 或远程 API,Resolver/Service 的分层使替换成本极低。

七、启动入口与运行验证

main.ts 与普通 NestJS 应用无异:

import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module.js'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); } await bootstrap();

注意两点:

  1. 顶层 await:因package.json声明"type": "module",文件采用 ESM 语法,bootstrap()直接以顶层await执行,无需包裹在void bootstrap()中;
  2. 导入后缀.js:ESM 下 TypeScript 编译产物为.js,导入路径必须显式写.js才能在 Node 侧正确解析。

运行:

$ cd sample/32-graphql-federation-schema-first/users-application $ npm install $ npm run start

启动后服务监听 3000 端口。若配合同一示例中的gateway应用(默认端口 4000)一起启动,网关会通过子服务的联邦端点拉取 SDL 并合并 Schema;直接访问子服务的 GraphQL 端点,可用如下查询验证getUser:

query { getUser(id: 1) { id name } }

预期返回{ "data": { "getUser": { "id": "1", "name": "John Doe" } } }。

八、测试:Resolver 与 Service 的单元验证

仓库为子服务配备了两份 Vitest 单元测试(配置见 vitest.config.mts,将include限定为src/**/*.spec.ts):

  • users.resolver.spec.ts:mockUsersService后验证getUser与resolveReference的返回;
  • users.service.spec.ts:验证findById的数据查找逻辑。

运行测试:

$ npm run test # vitest run $ npm run test:watch # 监听模式 $ npm run test:cov # 覆盖率

九、小结:Schema-First 联邦子服务的四要素

综合以上源码,一个可被联邦网关合并的 NestJS 子服务由四要素构成:

  1. 驱动接入:GraphQLModule.forRoot使用ApolloFederationDriver(users.module.ts);
  2. SDL 声明:.graphql文件定义实体并以@key(fields: "id")标注主键,用extend type Query追加查询(users.graphql);
  3. 引用解析:@ResolveReference()方法根据网关下发的引用对象还原完整实体(users.resolver.ts);
  4. 数据服务:将查询与解析统一委托给 Service 层,保持数据源可替换(users.service.ts)。

如需对比装饰器风格的联邦写法,可参考同仓库的 sample/31-graphql-federation-code-first;若要查看网关如何合并本子服务的 Schema,参见 32-graphql-federation-schema-first/gateway 目录及其 README.md。

  • 后端
  • Web框架

【免费下载链接】nest

A progressive Node.js framework for building efficient, scalable, and enterprise-grade server-side applications with TypeScript/JavaScript 🚀

项目地址:https://gitcode.com/GitHub_Trending/ne/nest
点击查看免费下载
上一篇:Netmiko自动检测黑科技:智能识别网络设备类型的完整教程
下一篇:打造专属音乐云:洛雪音乐数据同步服务全攻略

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询