NestJS Logo
NestJS 中文文档
v10.0.0
  • 介绍
  • 快速上手
  • 控制器
  • 提供者
  • 模块
  • 中间件
  • 异常过滤器
  • 管道
  • 守卫
  • 拦截器
  • 自定义装饰器
  • 自定义提供者
  • 异步提供者
  • 动态模块
  • 依赖注入作用域
  • 循环依赖
  • 模块引用
  • 懒加载模块
  • 执行上下文
  • 生命周期事件
  • 发现服务
  • 跨平台无关性
  • 测试
迁移指南
API 参考
官方课程
  1. 文档
  2. GraphQL 支持
  3. 联邦

其他功能
网关

联邦

联邦(Federation)为你提供了一种将单体 GraphQL 服务器拆分为独立微服务的方法。它包含两个核心组件:网关和一个或多个联邦微服务(Federated Microservices)。每个微服务持有部分 GraphQL Schema,网关会将这些架构合并为一个可被客户端消费的完整架构。

引用 Apollo 官方文档:

联邦设计遵循以下核心原则:

  • 构建图(Graph)应当是声明式的。通过联邦,你可以在架构内部以声明式方式组合图,而无需编写命令式的架构拼接代码。
  • 代码应按关注点分离,而不是按类型分离。通常,没有单一团队能完全控制像 User 或 Product 这样的重要类型的所有方面,因此这些类型的定义应分布在不同团队和代码库中,而不是集中管理。
  • 图应当对客户端来说简单易用。多个联邦服务可以共同组成一个完整、以产品为中心的图,准确反映客户端的实际使用方式。
  • 这就是 GraphQL,仅使用该语言的规范特性。任何语言(不仅限于 JavaScript)都可以实现联邦。
注意

目前联邦暂不支持订阅(Subscriptions)。

在接下来的章节中,我们将搭建一个演示应用,其中包含一个网关和两个联邦端点:用户服务(Users service)和帖子服务(Posts service)。

使用 Apollo 实现联邦

首先,安装所需依赖:

$ npm install @apollo/subgraph

模式优先(Schema First)

「用户服务(User service)」 提供了一个简单的 schema(模式)。请注意 @key 指令:它告诉 Apollo 查询规划器,只要指定了 User 的 id,就可以获取到对应的用户实例。同时,我们对 Query 类型进行了 extend 扩展。

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

extend type Query {
  getUser(id: ID!): User
}

解析器(Resolver)中额外提供了一个名为 resolveReference() 的方法。每当 Apollo Gateway 需要获取某个关联资源的 User 实例时,就会触发该方法。稍后我们会在 Posts 服务中看到相关示例。请注意,这个方法必须使用 @ResolveReference() 装饰器进行注解。

import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql'
import { UsersService } from './users.service'

@Resolver('User')
export class UsersResolver {
  constructor(private usersService: UsersService) {}

  @Query()
  getUser(@Args('id') id: string) {
    return this.usersService.findById(id)
  }

  @ResolveReference()
  resolveReference(reference: { __typename: string; id: string }) {
    return this.usersService.findById(reference.id)
  }
}

最后,我们通过在配置对象中传入 ApolloFederationDriver 驱动,注册 GraphQLModule,将所有内容串联起来:

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo'
import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'
import { UsersResolver } from './users.resolver'

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      typePaths: ['**/*.graphql'],
    }),
  ],
  providers: [UsersResolver],
})
export class AppModule {}

代码优先(Code first)

首先,为 User 实体类添加一些额外的装饰器。

import { Directive, Field, ID, ObjectType } from '@nestjs/graphql'

@ObjectType()
@Directive('@key(fields: "id")')
export class User {
  @Field(() => ID)
  id: number

  @Field()
  name: string
}

解析器(Resolver)额外提供了一个名为 resolveReference() 的方法。当 Apollo Gateway 需要获取相关资源的 User 实例时,会触发此方法。稍后我们会在 Posts 服务中看到一个相关示例。请注意,该方法必须使用 @ResolveReference() 装饰器进行注解。

import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql'
import { User } from './user.entity'
import { UsersService } from './users.service'

@Resolver(() => User)
export class UsersResolver {
  constructor(private usersService: UsersService) {}

  @Query(() => User)
  getUser(@Args('id') id: number): User {
    return this.usersService.findById(id)
  }

  @ResolveReference()
  resolveReference(reference: { __typename: string; id: number }): User {
    return this.usersService.findById(reference.id)
  }
}

最后,我们通过在配置对象中传入 ApolloFederationDriver 驱动,注册 GraphQLModule,将所有内容串联起来:

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo'
import { Module } from '@nestjs/common'
import { UsersResolver } from './users.resolver'
import { UsersService } from './users.service' // 本示例未包含实现

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      autoSchemaFile: true,
    }),
  ],
  providers: [UsersResolver, UsersService],
})
export class AppModule {}

你可以在这里查看代码优先模式的完整示例,在这里查看 schema 优先模式的示例。

联邦示例:Posts

Post 服务负责通过 getPosts 查询聚合并返回所有帖子,同时还会通过 user.posts 字段扩展我们的 User 类型。

模式优先(Schema First)

「Posts 服务」在其 schema 中通过 extend 关键字引用了 User 类型,并且在 User 类型上声明了一个额外的属性(posts)。请注意,@key 指令用于匹配 User 实例,而 @external 指令则表明 id 字段由其他地方管理。

type Post @key(fields: "id") {
  id: ID!
  title: String!
  body: String!
  user: User
}

extend type User @key(fields: "id") {
  id: ID! @external
  posts: [Post]
}

extend type Query {
  getPosts: [Post]
}

在下方示例中,PostsResolver 提供了 getUser() 方法,该方法返回一个包含 __typename 以及应用可能需要用于解析引用的其他属性(此处为 id)的引用对象。__typename 用于 GraphQL 网关(Gateway)定位负责 User 类型的微服务,并检索对应的实例。上文描述的「Users 服务」会在执行 resolveReference() 方法时被请求。

import { Query, Resolver, Parent, ResolveField } from '@nestjs/graphql'
import { PostsService } from './posts.service'
import { Post } from './posts.interfaces'

@Resolver('Post')
export class PostsResolver {
  constructor(private postsService: PostsService) {}

  @Query('getPosts')
  getPosts() {
    return this.postsService.findAll()
  }

  @ResolveField('user')
  getUser(@Parent() post: Post) {
    return { __typename: 'User', id: post.userId }
  }
}

最后,我们需要像在「Users 服务」部分一样注册 GraphQLModule。

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo'
import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'
import { PostsResolver } from './posts.resolver'

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      typePaths: ['**/*.graphql'],
    }),
  ],
  providers: [PostsResolvers],
})
export class AppModule {}

代码优先(Code First)

首先,我们需要声明一个代表 User 实体的类。虽然该实体本身存在于另一个服务中,但我们将在此处使用它(扩展其定义)。请注意 @extends 和 @external 指令的用法。

import { Directive, ObjectType, Field, ID } from '@nestjs/graphql'
import { Post } from './post.entity'

@ObjectType()
@Directive('@extends')
@Directive('@key(fields: "id")')
export class User {
  @Field(() => ID)
  @Directive('@external')
  id: number

  @Field(() => [Post])
  posts?: Post[]
}

接下来,我们为扩展的 User 实体创建对应的解析器:

import { Parent, ResolveField, Resolver } from '@nestjs/graphql'
import { PostsService } from './posts.service'
import { Post } from './post.entity'
import { User } from './user.entity'

@Resolver(() => User)
export class UsersResolver {
  constructor(private readonly postsService: PostsService) {}

  @ResolveField(() => [Post])
  public posts(@Parent() user: User): Post[] {
    return this.postsService.forAuthor(user.id)
  }
}

我们还需要定义 Post 实体类:

import { Directive, Field, ID, Int, ObjectType } from '@nestjs/graphql'
import { User } from './user.entity'

@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
  @Field(() => ID)
  id: number

  @Field()
  title: string

  @Field(() => Int)
  authorId: number

  @Field(() => User)
  user?: User
}

以及它的解析器:

import { Query, Args, ResolveField, Resolver, Parent } from '@nestjs/graphql'
import { PostsService } from './posts.service'
import { Post } from './post.entity'
import { User } from './user.entity'

@Resolver(() => Post)
export class PostsResolver {
  constructor(private readonly postsService: PostsService) {}

  @Query(() => Post)
  findPost(@Args('id') id: number): Post {
    return this.postsService.findOne(id)
  }

  @Query(() => [Post])
  getPosts(): Post[] {
    return this.postsService.all()
  }

  @ResolveField(() => User)
  user(@Parent() post: Post): any {
    return { __typename: 'User', id: post.authorId }
  }
}

最后,将其在模块中整合。请注意 schema 构建选项,其中指定了 User 是一个孤立(外部)类型。

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo'
import { Module } from '@nestjs/common'
import { User } from './user.entity'
import { PostsResolvers } from './posts.resolvers'
import { UsersResolvers } from './users.resolvers'
import { PostsService } from './posts.service' // 示例中未包含

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      autoSchemaFile: true,
      buildSchemaOptions: {
        orphanedTypes: [User],
      },
    }),
  ],
  providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}

一个可用的示例可在这里(Code First 模式)和这里(Schema First 模式)找到。

联邦示例:网关(Gateway)

首先,安装所需的依赖包:

$ npm install @apollo/gateway

网关需要指定一组端点(endpoints),它会自动发现对应的模式。因此,无论是代码优先还是模式优先方式,网关服务的实现方式都是一致的。

import { IntrospectAndCompose } from '@apollo/gateway'
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo'
import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloGatewayDriverConfig>({
      driver: ApolloGatewayDriver,
      server: {
        // ... Apollo 服务器选项
        cors: true,
      },
      gateway: {
        supergraphSdl: new IntrospectAndCompose({
          subgraphs: [
            { name: 'users', url: 'http://user-service/graphql' },
            { name: 'posts', url: 'http://post-service/graphql' },
          ],
        }),
      },
    }),
  ],
})
export class AppModule {}

你可以在以下链接找到可用的示例:

  • 代码优先模式示例
  • 模式优先模式示例

使用 Mercurius 实现联邦

首先,安装所需的依赖包:

$ npm install @apollo/subgraph @nestjs/mercurius
提示

@apollo/subgraph 包是构建子图模式(buildSubgraphSchema、printSubgraphSchema 函数)所必需的。

模式优先(Schema First)

「用户服务」(User service)提供了一个简单的 schema(模式)。请注意 @key 指令:它告诉 Mercurius 查询规划器,只要指定了 User 的 id 字段,就可以获取到对应的用户实例。同时,我们对 Query 类型进行了 extend(扩展)。

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

extend type Query {
  getUser(id: ID!): User
}

解析器额外提供了一个名为 resolveReference() 的方法。每当 Mercurius 网关需要获取某个关联资源的 User 实例时,就会触发该方法。稍后我们会在 Posts 服务中看到相关示例。请注意,该方法必须使用 @ResolveReference() 装饰器进行注解。

import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql'
import { UsersService } from './users.service'

@Resolver('User')
export class UsersResolver {
  constructor(private usersService: UsersService) {}

  @Query()
  getUser(@Args('id') id: string) {
    return this.usersService.findById(id)
  }

  @ResolveReference()
  resolveReference(reference: { __typename: string; id: string }) {
    return this.usersService.findById(reference.id)
  }
}

最后,我们通过在配置对象中传入 MercuriusFederationDriver 驱动,将其注册到 GraphQLModule,完成所有配置:

import {
  MercuriusFederationDriver,
  MercuriusFederationDriverConfig,
} from '@nestjs/mercurius'
import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'
import { UsersResolver } from './users.resolver'

@Module({
  imports: [
    GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
      driver: MercuriusFederationDriver,
      typePaths: ['**/*.graphql'],
      federationMetadata: true,
    }),
  ],
  providers: [UsersResolver],
})
export class AppModule {}

代码优先(Code First)

首先,在 User 实体上添加一些额外的装饰器。

import { Directive, Field, ID, ObjectType } from '@nestjs/graphql'

@ObjectType()
@Directive('@key(fields: "id")')
export class User {
  @Field(() => ID)
  id: number

  @Field()
  name: string
}

解析器额外提供了一个名为 resolveReference() 的方法。每当 Mercurius 网关需要获取某个关联资源的 User 实例时,就会触发该方法。稍后我们会在 Posts 服务中看到相关示例。请注意,该方法必须使用 @ResolveReference() 装饰器进行注解。

import { Args, Query, Resolver, ResolveReference } from '@nestjs/graphql'
import { User } from './user.entity'
import { UsersService } from './users.service'

@Resolver(() => User)
export class UsersResolver {
  constructor(private usersService: UsersService) {}

  @Query(() => User)
  getUser(@Args('id') id: number): User {
    return this.usersService.findById(id)
  }

  @ResolveReference()
  resolveReference(reference: { __typename: string; id: number }): User {
    return this.usersService.findById(reference.id)
  }
}

最后,我们通过在配置对象中传入 MercuriusFederationDriver 驱动,将其注册到 GraphQLModule,完成所有配置:

import {
  MercuriusFederationDriver,
  MercuriusFederationDriverConfig,
} from '@nestjs/mercurius'
import { Module } from '@nestjs/common'
import { UsersResolver } from './users.resolver'
import { UsersService } from './users.service' // 本示例未包含实现

@Module({
  imports: [
    GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
      driver: MercuriusFederationDriver,
      autoSchemaFile: true,
      federationMetadata: true,
    }),
  ],
  providers: [UsersResolver, UsersService],
})
export class AppModule {}

联邦示例:Posts(帖子)

Post 服务负责通过 getPosts 查询返回聚合的帖子数据,同时扩展我们的 User 类型,增加 user.posts 字段。

模式优先(Schema First)

「Posts 服务」在其 schema 中通过 extend 关键字引用了 User 类型,并声明了 User 类型的一个额外属性(posts)。请注意,@key 指令用于匹配 User 实例,@external 指令则表明 id 字段由其他地方管理。

type Post @key(fields: "id") {
  id: ID!
  title: String!
  body: String!
  user: User
}

extend type User @key(fields: "id") {
  id: ID! @external
  posts: [Post]
}

extend type Query {
  getPosts: [Post]
}

在下例中,PostsResolver 提供了 getUser() 方法,该方法返回一个包含 __typename 以及应用可能需要解析引用的其他属性(本例为 id)的对象。__typename 用于 GraphQL 网关(Gateway)定位负责 User 类型的微服务,并获取对应实例。当执行 resolveReference() 方法时,上文描述的「Users 服务」会被请求。

import { Query, Resolver, Parent, ResolveField } from '@nestjs/graphql'
import { PostsService } from './posts.service'
import { Post } from './posts.interfaces'

@Resolver('Post')
export class PostsResolver {
  constructor(private postsService: PostsService) {}

  @Query('getPosts')
  getPosts() {
    return this.postsService.findAll()
  }

  @ResolveField('user')
  getUser(@Parent() post: Post) {
    return { __typename: 'User', id: post.userId }
  }
}

最后,我们需要像在「Users 服务」部分一样注册 GraphQLModule。

import {
  MercuriusFederationDriver,
  MercuriusFederationDriverConfig,
} from '@nestjs/mercurius'
import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'
import { PostsResolver } from './posts.resolver'

@Module({
  imports: [
    GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
      driver: MercuriusFederationDriver,
      federationMetadata: true,
      typePaths: ['**/*.graphql'],
    }),
  ],
  providers: [PostsResolvers],
})
export class AppModule {}

代码优先(Code First)

首先,我们需要声明一个代表 User 实体的类。虽然该实体实际存在于另一个服务中,但我们需要在此处使用(扩展其定义)。请注意 @extends 和 @external 指令的用法。

import { Directive, ObjectType, Field, ID } from '@nestjs/graphql'
import { Post } from './post.entity'

@ObjectType()
@Directive('@extends')
@Directive('@key(fields: "id")')
export class User {
  @Field(() => ID)
  @Directive('@external')
  id: number

  @Field(() => [Post])
  posts?: Post[]
}

接下来,为我们对 User 实体的扩展创建对应的解析器:

import { Parent, ResolveField, Resolver } from '@nestjs/graphql'
import { PostsService } from './posts.service'
import { Post } from './post.entity'
import { User } from './user.entity'

@Resolver(() => User)
export class UsersResolver {
  constructor(private readonly postsService: PostsService) {}

  @ResolveField(() => [Post])
  public posts(@Parent() user: User): Post[] {
    return this.postsService.forAuthor(user.id)
  }
}

我们还需要定义 Post 实体类:

import { Directive, Field, ID, Int, ObjectType } from '@nestjs/graphql'
import { User } from './user.entity'

@ObjectType()
@Directive('@key(fields: "id")')
export class Post {
  @Field(() => ID)
  id: number

  @Field()
  title: string

  @Field(() => Int)
  authorId: number

  @Field(() => User)
  user?: User
}

以及它的解析器:

import { Query, Args, ResolveField, Resolver, Parent } from '@nestjs/graphql'
import { PostsService } from './posts.service'
import { Post } from './post.entity'
import { User } from './user.entity'

@Resolver(() => Post)
export class PostsResolver {
  constructor(private readonly postsService: PostsService) {}

  @Query(() => Post)
  findPost(@Args('id') id: number): Post {
    return this.postsService.findOne(id)
  }

  @Query(() => [Post])
  getPosts(): Post[] {
    return this.postsService.all()
  }

  @ResolveField(() => User)
  user(@Parent() post: Post): any {
    return { __typename: 'User', id: post.authorId }
  }
}

最后,将所有内容整合到一个模块中。请注意 schema 构建选项,其中指定了 User 是一个孤立(外部)类型。

import {
  MercuriusFederationDriver,
  MercuriusFederationDriverConfig,
} from '@nestjs/mercurius'
import { Module } from '@nestjs/common'
import { User } from './user.entity'
import { PostsResolvers } from './posts.resolvers'
import { UsersResolvers } from './users.resolvers'
import { PostsService } from './posts.service' // 示例中未包含

@Module({
  imports: [
    GraphQLModule.forRoot<MercuriusFederationDriverConfig>({
      driver: MercuriusFederationDriver,
      autoSchemaFile: true,
      federationMetadata: true,
      buildSchemaOptions: {
        orphanedTypes: [User],
      },
    }),
  ],
  providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}

联邦示例:网关(Gateway)

网关需要指定一组端点(endpoints),并会自动发现对应的模式。因此,无论是代码优先还是模式优先方式,网关服务的实现方式保持一致。

import {
  MercuriusGatewayDriver,
  MercuriusGatewayDriverConfig,
} from '@nestjs/mercurius'
import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'

@Module({
  imports: [
    GraphQLModule.forRoot<MercuriusGatewayDriverConfig>({
      driver: MercuriusGatewayDriver,
      gateway: {
        services: [
          { name: 'users', url: 'http://user-service/graphql' },
          { name: 'posts', url: 'http://post-service/graphql' },
        ],
      },
    }),
  ],
})
export class AppModule {}

Federation 2

引用 Apollo 官方文档:Federation 2 在原有 Apollo Federation(本文称为 Federation 1)的基础上,进一步提升了开发者体验,并且与大多数原有超级图(supergraph)向后兼容。

注意

Mercurius 尚未完全支持 Federation 2。你可以在这里 查看支持 Federation 2 的库列表。

在接下来的章节中,我们将把前面的示例升级到 Federation 2。

联邦示例:用户服务(Users)

Federation 2 的一个变化是实体不再有原始子图(originating subgraph),因此我们不需要再扩展 Query。更多细节请参考 Apollo Federation 2 文档中的实体主题。

模式优先(Schema First)

我们只需从模式中移除 extend 关键字。

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

type Query {
  getUser(id: ID!): User
}

代码优先(Code First)

要使用 Federation 2,需要在 autoSchemaFile 选项中指定联邦版本。

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo'
import { Module } from '@nestjs/common'
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 {}

联邦示例:帖子服务(Posts)

出于同样的原因,我们不再需要扩展 User 和 Query。

模式优先(Schema First)

我们只需从模式中移除 extend 和 external 指令(Directive)。

type Post @key(fields: "id") {
  id: ID!
  title: String!
  body: String!
  user: User
}

type User @key(fields: "id") {
  id: ID!
  posts: [Post]
}

type Query {
  getPosts: [Post]
}

代码优先(Code First)

由于我们不再扩展 User 实体,因此可以直接移除 extends 和 external 指令。

import { Directive, ObjectType, Field, ID } from '@nestjs/graphql'
import { Post } from './post.entity'

@ObjectType()
@Directive('@key(fields: "id")')
export class User {
  @Field(() => ID)
  id: number

  @Field(() => [Post])
  posts?: Post[]
}

同样地,与用户服务类似,我们需要在 GraphQLModule 中指定使用 Federation 2。

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo'
import { Module } from '@nestjs/common'
import { User } from './user.entity'
import { PostsResolvers } from './posts.resolvers'
import { UsersResolvers } from './users.resolvers'
import { PostsService } from './posts.service' // 本示例未包含该服务实现

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloFederationDriverConfig>({
      driver: ApolloFederationDriver,
      autoSchemaFile: {
        federation: 2,
      },
      buildSchemaOptions: {
        orphanedTypes: [User],
      },
    }),
  ],
  providers: [PostsResolver, UsersResolver, PostsService],
})
export class AppModule {}