☰ Mục lục series
Tài liệu NestJS tiếng Việt
  1. Nhập môn (Overview)
    1. Giới thiệu
    2. First steps
    3. Controllers
    4. Providers
    5. Modules
    6. Middleware
    7. Exception filters
    8. Pipes
    9. Guards
    10. Interceptors
    11. Custom decorators
  2. Nền tảng (Fundamentals)
    1. Custom providers
    2. Asynchronous providers
    3. Dynamic modules
    4. Injection scopes
    5. Circular dependency
    6. Module reference
    7. Lazy-loading modules
    8. Execution context
    9. Lifecycle events
    10. Discovery service
    11. Platform agnosticism
    12. Testing
  3. Kỹ thuật (Techniques)
    1. Configuration
    2. Database (SQL)
    3. MongoDB
    4. Validation
    5. Caching
    6. Serialization
    7. Versioning
    8. Task scheduling
    9. Queues
    10. Logging
    11. Cookies
    12. Events
    13. Compression
    14. File upload
    15. Streaming files
    16. HTTP module
    17. Session
    18. MVC
    19. Performance (Fastify)
    20. Server-Sent Events
  4. Bảo mật (Security)
    1. Authentication
    2. Authorization
    3. Encryption & Hashing
    4. Helmet
    5. CORS
    6. CSRF
    7. Rate limiting
    8. Vòng đời request
  5. OpenAPI (Swagger)
    1. Introduction
    2. Types and parameters
    3. Operations
    4. Security
    5. Mapped types
    6. Decorators
    7. CLI plugin
    8. Other features
  6. Recipes
    1. REPL
    2. CRUD generator
    3. SWC (fast compiler)
    4. Passport (auth)
    5. Hot reload
    6. MikroORM
    7. Router module
    8. Health checks (Terminus)
    9. CQRS
    10. Prisma
    11. Serve static
    12. Commander
    13. Async local storage
  7. WebSockets
    1. Gateways
    2. Exception filters
    3. Pipes
    4. Guards
    5. Interceptors
    6. Adapters
  8. Microservices
    1. Overview
    2. Redis
    3. MQTT
    4. NATS
    5. RabbitMQ
    6. Kafka
    7. gRPC
    8. Custom transporters
    9. Exception filters
    10. Pipes
    11. Pre-request hooks
    12. Guards
    13. Interceptors
  9. GraphQL
    1. Quick start
    2. Resolvers
    3. Mutations
    4. Subscriptions
    5. Scalars
    6. Directives
    7. Interfaces
    8. Unions and enums
    9. Field middleware
    10. Mapped types
    11. Plugins
    12. Complexity
    13. Extensions
    14. CLI plugin
    15. Generating SDL
    16. Sharing models
    17. Other features
    18. Federation
  10. CLI & triển khai
    1. CLI overview
    2. Workspaces
    3. Libraries
    4. Usage
    5. Scripts
    6. Deployment
    7. Standalone apps
  11. FAQ
    1. Serverless
    2. HTTP adapter
    3. Keep-Alive connections
    4. Global path prefix
    5. Raw body
    6. Hybrid application
    7. HTTPS & multiple servers
    8. Common errors
  12. Phụ lục
    1. Bảng thuật ngữ
GraphQL

Federation

Bản dịch từ docs.nestjs.com/graphql/federation

Federation (liên hợp) cung cấp một cách để chia nhỏ GraphQL server dạng monolithic của bạn thành các microservice độc lập. Nó gồm hai thành phần: một gateway (cổng vào) và một hoặc nhiều microservice federated. Mỗi microservice giữ một phần của schema, và gateway sẽ hợp nhất (merge) các schema này thành một schema duy nhất mà client có thể sử dụng.

Trích dẫn từ tài liệu Apollo, Federation được thiết kế theo các nguyên tắc cốt lõi sau:

  • Việc xây dựng một graph nên mang tính khai báo (declarative). Với federation, bạn tổng hợp một graph theo cách khai báo ngay trong schema, thay vì viết code schema stitching (ghép schema) theo kiểu mệnh lệnh (imperative).
  • Code nên được tách theo mối quan tâm (concern), chứ không phải theo type. Thường thì không có team nào một mình kiểm soát mọi khía cạnh của một type quan trọng như User hay Product, nên định nghĩa của các type này nên được phân tán qua nhiều team và codebase, thay vì tập trung một chỗ.
  • Graph nên đơn giản để client sử dụng. Khi kết hợp lại, các federated service tạo thành một graph hoàn chỉnh, hướng theo sản phẩm, phản ánh đúng cách nó được client tiêu thụ.
  • Đây đơn giản là GraphQL, chỉ dùng các tính năng tuân theo spec của ngôn ngữ này. Bất kỳ ngôn ngữ nào, không riêng gì JavaScript, đều có thể triển khai federation.

Cảnh báo Federation hiện chưa hỗ trợ subscription.

Trong các phần dưới đây, ta sẽ dựng một ứng dụng demo gồm một gateway và hai endpoint federated: Users service và Posts service.

Federation với Apollo

Bắt đầu bằng cách cài đặt các package cần thiết:

$ npm install --save @apollo/subgraph

Schema first

“User service” cung cấp một schema đơn giản. Lưu ý directive @key: nó chỉ cho query planner của Apollo biết rằng một instance cụ thể của User có thể được lấy về nếu bạn chỉ định id của nó. Ngoài ra, lưu ý rằng ta extend (mở rộng) type Query.

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

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

Resolver cung cấp thêm một method tên là resolveReference(). Method này được Apollo Gateway kích hoạt bất cứ khi nào một resource liên quan cần một instance User. Ta sẽ thấy ví dụ về việc này ở Posts service sau. Lưu ý method này phải được gắn decorator @ResolveReference().

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

@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);
  }
}

Cuối cùng, ta ghép mọi thứ lại bằng cách đăng ký GraphQLModule, truyền driver ApolloFederationDriver trong object cấu hình:

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

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

Code first

Bắt đầu bằng cách thêm vài decorator vào entity 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 cung cấp thêm một method tên là resolveReference(). Method này được Apollo Gateway kích hoạt bất cứ khi nào một resource liên quan cần một instance User. Ta sẽ thấy ví dụ về việc này ở Posts service sau. Lưu ý method này phải được gắn decorator @ResolveReference().

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

@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);
  }
}

Cuối cùng, ta ghép mọi thứ lại bằng cách đăng ký GraphQLModule, truyền driver ApolloFederationDriver trong object cấu hình:

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver.js';
import { UsersService } from './users.service.js'; // Not included in this example

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

Bạn có thể xem ví dụ hoạt động thực tế tại đây cho cách tiếp cận code first, và tại đây cho cách tiếp cận schema first.

Ví dụ federated: Posts

Posts service được thiết kế để phục vụ danh sách post tổng hợp qua query getPosts, đồng thời mở rộng type User với field user.posts.

Schema first

“Posts service” tham chiếu đến type User trong schema của nó bằng cách đánh dấu bằng từ khóa extend. Nó cũng khai báo thêm một thuộc tính trên type User (posts). Lưu ý directive @key dùng để khớp các instance của User, và directive @external cho biết field id được quản lý ở nơi khác.

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]
}

Trong ví dụ dưới đây, PostsResolver cung cấp method getUser() trả về một reference (tham chiếu) chứa __typename cùng một số thuộc tính bổ sung mà ứng dụng của bạn cần để resolve reference đó, trong trường hợp này là id. __typename được GraphQL Gateway dùng để xác định microservice nào chịu trách nhiệm cho type User và lấy về instance tương ứng. “Users service” đã mô tả ở trên sẽ được gọi tới khi method resolveReference() được thực thi.

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

@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 };
  }
}

Cuối cùng, ta cần đăng ký GraphQLModule, tương tự như đã làm ở phần “Users service”.

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

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

Code first

Trước tiên, ta cần khai báo một class đại diện cho entity User. Mặc dù entity này thực sự sống ở một service khác, ta vẫn sẽ sử dụng nó (mở rộng định nghĩa của nó) ở đây. Lưu ý các directive @extends và @external.

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

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

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

Bây giờ hãy tạo resolver tương ứng cho phần mở rộng của ta trên entity User, như sau:

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

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

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

Ta cũng cần định nghĩa class entity Post:

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

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

  @Field()
  title: string;

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

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

Và resolver của nó:

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

@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 };
  }
}

Và cuối cùng, ghép mọi thứ lại trong một module. Lưu ý phần schema build options, nơi ta chỉ định rằng User là một orphaned type (type “mồ côi”, tức type bên ngoài).

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { User } from './user.entity.js';
import { PostsResolver } from './posts.resolvers.js';
import { UsersResolver } from './users.resolvers.js';
import { PostsService } from './posts.service.js'; // Not included in example

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

Bạn có thể xem ví dụ hoạt động thực tế tại đây cho cách tiếp cận code first, và tại đây cho cách tiếp cận schema first.

Ví dụ federated: Gateway

Bắt đầu bằng cách cài đặt package cần thiết:

$ npm install --save @apollo/gateway

Gateway yêu cầu chỉ định một danh sách endpoint, và nó sẽ tự động dò (auto-discover) các schema tương ứng. Do đó, phần triển khai gateway service sẽ giống nhau cho cả hai cách tiếp cận code first và schema first.

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 server options
        cors: true,
      },
      gateway: {
        supergraphSdl: new IntrospectAndCompose({
          subgraphs: [
            { name: 'users', url: 'http://user-service/graphql' },
            { name: 'posts', url: 'http://post-service/graphql' },
          ],
        }),
      },
    }),
  ],
})
export class AppModule {}

Bạn có thể xem ví dụ hoạt động thực tế tại đây cho cách tiếp cận code first, và tại đây cho cách tiếp cận schema first.

Ghi chú người dịch So với microservice REST truyền thống — nơi mỗi service tự expose API riêng và một API Gateway (kiểu Kong, Express gateway) chỉ đơn thuần route request tới đúng service — Apollo Gateway ở đây làm nhiều hơn: nó thực sự hợp nhất schema thành một graph logic duy nhất, rồi tự lập kế hoạch truy vấn (query plan) để gọi song song/tuần tự tới nhiều subgraph và ráp kết quả lại, tất cả trong một request GraphQL. Đây là điểm khác biệt cốt lõi so với việc chỉ forward request như API Gateway thông thường.

Federation với Mercurius

Bắt đầu bằng cách cài đặt các package cần thiết:

$ npm install --save @apollo/subgraph @nestjs/mercurius

Lưu ý Package @apollo/subgraph là bắt buộc để build subgraph schema (các hàm buildSubgraphSchema, printSubgraphSchema).

Schema first

“User service” cung cấp một schema đơn giản. Lưu ý directive @key: nó chỉ cho query planner của Mercurius biết rằng một instance cụ thể của User có thể được lấy về nếu bạn chỉ định id của nó. Ngoài ra, lưu ý rằng ta extend type Query.

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

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

Resolver cung cấp thêm một method tên là resolveReference(). Method này được Mercurius Gateway kích hoạt bất cứ khi nào một resource liên quan cần một instance User. Ta sẽ thấy ví dụ về việc này ở Posts service sau. Lưu ý method này phải được gắn decorator @ResolveReference().

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

@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);
  }
}

Cuối cùng, ta ghép mọi thứ lại bằng cách đăng ký GraphQLModule, truyền driver MercuriusFederationDriver trong object cấu hình:

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

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

Code first

Bắt đầu bằng cách thêm vài decorator vào entity 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 cung cấp thêm một method tên là resolveReference(). Method này được Mercurius Gateway kích hoạt bất cứ khi nào một resource liên quan cần một instance User. Ta sẽ thấy ví dụ về việc này ở Posts service sau. Lưu ý method này phải được gắn decorator @ResolveReference().

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

@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);
  }
}

Cuối cùng, ta ghép mọi thứ lại bằng cách đăng ký GraphQLModule, truyền driver MercuriusFederationDriver trong object cấu hình:

import {
  MercuriusFederationDriver,
  MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver.js';
import { UsersService } from './users.service.js'; // Not included in this example

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

Ví dụ federated: Posts

Posts service được thiết kế để phục vụ danh sách post tổng hợp qua query getPosts, đồng thời mở rộng type User với field user.posts.

Schema first

“Posts service” tham chiếu đến type User trong schema của nó bằng cách đánh dấu bằng từ khóa extend. Nó cũng khai báo thêm một thuộc tính trên type User (posts). Lưu ý directive @key dùng để khớp các instance của User, và directive @external cho biết field id được quản lý ở nơi khác.

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]
}

Trong ví dụ dưới đây, PostsResolver cung cấp method getUser() trả về một reference chứa __typename cùng một số thuộc tính bổ sung mà ứng dụng của bạn cần để resolve reference đó, trong trường hợp này là id. __typename được GraphQL Gateway dùng để xác định microservice nào chịu trách nhiệm cho type User và lấy về instance tương ứng. “Users service” đã mô tả ở trên sẽ được gọi tới khi method resolveReference() được thực thi.

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

@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 };
  }
}

Cuối cùng, ta cần đăng ký GraphQLModule, tương tự như đã làm ở phần “Users service”.

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

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

Code first

Trước tiên, ta cần khai báo một class đại diện cho entity User. Mặc dù entity này thực sự sống ở một service khác, ta vẫn sẽ sử dụng nó (mở rộng định nghĩa của nó) ở đây. Lưu ý các directive @extends và @external.

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

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

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

Bây giờ hãy tạo resolver tương ứng cho phần mở rộng của ta trên entity User, như sau:

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

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

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

Ta cũng cần định nghĩa class entity Post:

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

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

  @Field()
  title: string;

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

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

Và resolver của nó:

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

@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 };
  }
}

Và cuối cùng, ghép mọi thứ lại trong một module. Lưu ý phần schema build options, nơi ta chỉ định rằng User là một orphaned type.

import {
  MercuriusFederationDriver,
  MercuriusFederationDriverConfig,
} from '@nestjs/mercurius';
import { Module } from '@nestjs/common';
import { User } from './user.entity.js';
import { PostsResolver } from './posts.resolvers.js';
import { UsersResolver } from './users.resolvers.js';
import { PostsService } from './posts.service.js'; // Not included in example

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

Ví dụ federated: Gateway

Gateway yêu cầu chỉ định một danh sách endpoint, và nó sẽ tự động dò các schema tương ứng. Do đó, phần triển khai gateway service sẽ giống nhau cho cả hai cách tiếp cận code first và schema first.

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

Trích dẫn từ tài liệu Apollo, Federation 2 cải thiện trải nghiệm dev so với Apollo Federation nguyên bản (trong tài liệu này gọi là Federation 1), và vẫn tương thích ngược với phần lớn các supergraph cũ.

Cảnh báo Mercurius chưa hỗ trợ đầy đủ Federation 2. Bạn có thể xem danh sách các thư viện hỗ trợ Federation 2 tại đây.

Trong các phần dưới đây, ta sẽ nâng cấp ví dụ trước đó lên Federation 2.

Ví dụ federated: Users

Một thay đổi trong Federation 2 là entity không còn subgraph “gốc” (originating subgraph), nên ta không cần extend Query nữa. Để biết thêm chi tiết, xem chủ đề về entity trong tài liệu Apollo Federation 2.

Schema first

Ta chỉ cần bỏ từ khóa extend khỏi schema.

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

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

Code first

Để dùng Federation 2, ta cần chỉ định phiên bản federation trong tùy chọn autoSchemaFile.

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { UsersResolver } from './users.resolver.js';
import { UsersService } from './users.service.js'; // Not included in this example

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

Ví dụ federated: Posts

Với lý do tương tự như trên, ta không cần extend User và Query nữa.

Schema first

Ta chỉ cần bỏ directive extend và external khỏi schema

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

Vì ta không còn extend entity User nữa, ta có thể bỏ luôn directive extends và external khỏi User.

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

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

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

Ngoài ra, tương tự như ở Users service, ta cần chỉ định trong GraphQLModule để dùng Federation 2.

import {
  ApolloFederationDriver,
  ApolloFederationDriverConfig,
} from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { User } from './user.entity.js';
import { PostsResolver } from './posts.resolvers.js';
import { UsersResolver } from './users.resolvers.js';
import { PostsService } from './posts.service.js'; // Not included in example

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

Ghi chú người dịch Nếu bạn quen với việc tách monolith thành microservice REST, đây là dạng tương đương cho GraphQL: thay vì mỗi service tự expose REST endpoint và client phải tự gọi nhiều nơi rồi ráp dữ liệu (kiểu BFF thủ công), federation cho phép “khai báo” quyền sở hữu từng phần của một type dùng chung (như User) ngay trong schema của từng service, còn việc ráp dữ liệu là do gateway lo — tương tự cách một API Gateway kiểu Laravel/Express tổng hợp nhiều microservice, nhưng ở mức field thay vì mức endpoint. Cạm bẫy thường gặp: nếu bạn quên đánh dấu @key đúng field, hoặc field đó không tồn tại/không khớp giữa các subgraph, composition sẽ lỗi ngay ở bước gateway khởi động chứ không phải lúc runtime — nên luôn kiểm tra log gateway đầu tiên khi thêm subgraph mới.

Nội dung gốc: NestJS Documentation © Kamil Myśliwiec, giấy phép MIT. Bản dịch và ghi chú: Lê Văn Dũng.