☰ 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ữ
Microservices

Tổng quan (Overview)

Bản dịch từ docs.nestjs.com/microservices/basics

Bên cạnh kiến trúc ứng dụng truyền thống (đôi khi gọi là monolithic), Nest hỗ trợ sẵn kiểu kiến trúc microservice. Hầu hết các khái niệm đã nói ở những phần khác trong tài liệu này — như dependency injection, decorator, exception filter, pipe, guard và interceptor — đều áp dụng tương tự cho microservices. Ở bất cứ đâu có thể, Nest trừu tượng hóa chi tiết triển khai để cùng một component có thể chạy trên các nền tảng dựa trên HTTP, WebSockets và Microservices. Phần này nói về những khía cạnh của Nest dành riêng cho microservices.

Trong Nest, một microservice về bản chất là một ứng dụng sử dụng lớp transport khác với HTTP.

Minh hoạ: Microservices

Nest hỗ trợ sẵn nhiều cách triển khai lớp transport, gọi là transporter, có nhiệm vụ truyền message giữa các instance microservice khác nhau. Hầu hết các transporter hỗ trợ sẵn cả hai kiểu message: request-response và event-based. Nest trừu tượng hóa chi tiết triển khai của từng transporter phía sau một interface chung cho cả messaging kiểu request-response và event-based. Nhờ vậy bạn dễ dàng chuyển từ transport layer này sang transport layer khác — ví dụ để tận dụng tính năng độ tin cậy hay hiệu năng riêng của một transport layer cụ thể — mà không ảnh hưởng đến code ứng dụng.

Cài đặt (Installation)

Để bắt đầu xây dựng microservices, trước tiên cài package cần thiết:

$ npm i --save @nestjs/microservices

Bắt đầu (Getting started)

Để khởi tạo một microservice, dùng method createMicroservice() của class NestFactory:

import { NestFactory } from '@nestjs/core';
import { Transport, MicroserviceOptions } from '@nestjs/microservices';
import { AppModule } from './app.module.js';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.TCP,
    },
  );
  await app.listen();
}
await bootstrap();
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { AppModule } from './app.module.js';

async function bootstrap() {
  const app = await NestFactory.createMicroservice(AppModule, {
    transport: Transport.TCP,
  });
  await app.listen();
}
await bootstrap();

Gợi ý Microservices dùng lớp transport TCP theo mặc định.

Tham số thứ hai của method createMicroservice() là một object options. Object này có thể gồm hai thành phần:

transport Chỉ định transporter (ví dụ, Transport.NATS)
options Object option riêng cho từng transporter, quyết định hành vi của transporter

Object options phụ thuộc vào transporter bạn chọn. Transporter TCP có các thuộc tính được mô tả dưới đây. Với các transporter khác (ví dụ Redis, MQTT, v.v.), xem chương tương ứng để biết các option khả dụng.

host Hostname kết nối
port Port kết nối
retryAttempts Số lần thử lại gửi message (mặc định: 0)
retryDelay Khoảng thời gian giữa các lần thử lại (ms) (mặc định: 0)
serializer Serializer tùy chỉnh cho message gửi đi
deserializer Deserializer tùy chỉnh cho message nhận vào
socketClass Một Socket tùy chỉnh kế thừa từ TcpSocket (mặc định: JsonSocket)
tlsOptions Các option để cấu hình giao thức tls

Gợi ý Các thuộc tính trên chỉ dành riêng cho transporter TCP. Để biết các option khả dụng của transporter khác, xem chương tương ứng.

Message pattern và Event pattern (Message and Event Patterns)

Microservices nhận diện cả message và event thông qua pattern. Pattern là một giá trị đơn giản, ví dụ một object literal hoặc một string. Pattern được tự động serialize và gửi qua network kèm phần data của message. Nhờ vậy, bên gửi và bên nhận message có thể phối hợp xác định request nào được xử lý bởi handler nào.

Request-response

Kiểu message request-response hữu ích khi bạn cần trao đổi message giữa các service bên ngoài với nhau. Mô hình này đảm bảo rằng service đã thực sự nhận được message (mà bạn không cần tự triển khai một cơ chế acknowledgment). Tuy nhiên, cách tiếp cận request-response không phải lúc nào cũng là lựa chọn phù hợp nhất. Ví dụ, các transporter kiểu streaming như Kafka hoặc NATS streaming, dùng cơ chế lưu trữ dựa trên log, được tối ưu để giải quyết một nhóm vấn đề khác, gần với mô hình event messaging hơn (xem event-based messaging để biết chi tiết).

Để bật kiểu message request-response, Nest tạo ra hai channel logic: một để truyền data, một để chờ response trả về. Với một số transport bên dưới, như NATS, việc hỗ trợ hai channel này đã có sẵn. Với các transport khác, Nest bù đắp bằng cách tự tạo thêm channel riêng. Cách này hiệu quả, nhưng có thể phát sinh thêm overhead. Vì vậy, nếu bạn không cần kiểu message request-response, bạn nên xem xét dùng phương thức event-based.

Để tạo một message handler theo mô hình request-response, dùng decorator @MessagePattern(), được import từ package @nestjs/microservices. Decorator này chỉ nên dùng trong các class controller, vì controller đóng vai trò là entry point của ứng dụng. Dùng nó trong provider sẽ không có tác dụng, vì Nest runtime sẽ bỏ qua.

import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';

@Controller()
export class MathController {
  @MessagePattern({ cmd: 'sum' })
  accumulate(data: number[]): number {
    return (data || []).reduce((a, b) => a + b);
  }
}
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';

@Controller()
export class MathController {
  @MessagePattern({ cmd: 'sum' })
  accumulate(data) {
    return (data || []).reduce((a, b) => a + b);
  }
}

Ghi chú người dịch Nếu bạn từng viết Express, cứ nghĩ @MessagePattern() giống như một route, chỉ khác là “đường dẫn” của nó không phải URL mà là một object hoặc string pattern ({ cmd: 'sum' }). Nest match message đến bằng cách so khớp pattern này, tương tự cách Express router match method + path.

Trong đoạn code trên, message handler accumulate() lắng nghe các message khớp với pattern { cmd: 'sum' }. Message handler nhận một tham số duy nhất là data, được truyền từ client. Trong trường hợp này, data là một array số cần được cộng lại.

Response bất đồng bộ (Asynchronous responses)

Message handler có thể trả response đồng bộ hoặc bất đồng bộ, nghĩa là method async được hỗ trợ.

@MessagePattern({ cmd: 'sum' })
async accumulate(data: number[]): Promise<number> {
  return (data || []).reduce((a, b) => a + b);
}
@MessagePattern({ cmd: 'sum' })
async accumulate(data) {
  return (data || []).reduce((a, b) => a + b);
}

Message handler cũng có thể trả về một Observable, trong trường hợp đó các giá trị kết quả sẽ được emit liên tục cho tới khi stream hoàn tất.

@MessagePattern({ cmd: 'sum' })
accumulate(data: number[]): Observable<number> {
  return from([1, 2, 3]);
}
@MessagePattern({ cmd: 'sum' })
accumulate(data: number[]): Observable<number> {
  return from([1, 2, 3]);
}

Trong ví dụ trên, message handler sẽ trả response ba lần, mỗi lần cho một phần tử trong array.

Event-based

Trong khi phương thức request-response rất phù hợp để trao đổi message giữa các service, nó lại kém phù hợp cho messaging kiểu event-based — khi bạn chỉ đơn giản muốn publish event mà không cần chờ response. Trong trường hợp này, phần overhead của việc duy trì hai channel cho request-response là không cần thiết.

Ví dụ, nếu bạn muốn thông báo cho một service khác biết rằng một điều kiện cụ thể đã xảy ra ở phần này của hệ thống, kiểu message event-based là lựa chọn lý tưởng.

Để tạo một event handler, bạn có thể dùng decorator @EventPattern(), được import từ package @nestjs/microservices.

@EventPattern('user_created')
async handleUserCreated(data: Record<string, unknown>) {
  // business logic
}
@EventPattern('user_created')
async handleUserCreated(data) {
  // business logic
}

Gợi ý Bạn có thể đăng ký nhiều event handler cho cùng một event pattern, và tất cả sẽ tự động được kích hoạt song song.

Event handler handleUserCreated() lắng nghe event 'user_created'. Event handler nhận một tham số duy nhất là data, được truyền từ client (trong trường hợp này là payload của event, đã được gửi qua network).

Thông tin bổ sung về request (Additional request details)

Trong các tình huống phức tạp hơn, bạn có thể cần truy cập thêm thông tin về request đến. Ví dụ, khi dùng NATS với wildcard subscription, bạn có thể muốn lấy subject gốc mà producer đã gửi message tới. Tương tự, với Kafka, bạn có thể cần truy cập message headers. Để làm điều này, bạn có thể dùng các decorator có sẵn như dưới đây:

@MessagePattern('time.us.*')
getDate(@Payload() data: number[], @Ctx() context: NatsContext) {
  console.log(`Subject: ${context.getSubject()}`); // e.g. "time.us.east"
  return new Date().toLocaleTimeString(...);
}
@Bind(Payload(), Ctx())
@MessagePattern('time.us.*')
getDate(data, context) {
  console.log(`Subject: ${context.getSubject()}`); // e.g. "time.us.east"
  return new Date().toLocaleTimeString(...);
}

Gợi ý @Payload(), @Ctx() và NatsContext được import từ @nestjs/microservices.

Gợi ý Bạn cũng có thể truyền một property key vào decorator @Payload() để lấy ra một property cụ thể từ payload object nhận được, ví dụ @Payload('id').

Client (producer class)

Một ứng dụng Nest đóng vai trò client có thể trao đổi message hoặc publish event tới một Nest microservice bằng class ClientProxy. Class này cung cấp nhiều method, như send() (cho messaging kiểu request-response) và emit() (cho messaging kiểu event-driven), giúp giao tiếp với một microservice từ xa. Bạn có thể lấy một instance của class này theo các cách sau:

Một cách là import ClientsModule, module này expose static method register(). Method này nhận một array các object đại diện cho transporter của microservice. Mỗi object phải có property name, và có thể có thêm property transport (mặc định là Transport.TCP), cũng như property options (tùy chọn).

Property name đóng vai trò là một injection token, bạn có thể dùng nó để inject một instance ClientProxy ở bất cứ đâu cần. Giá trị của property name này có thể là một string hoặc JavaScript symbol tùy ý, như mô tả tại đây.

Property options là một object chứa các thuộc tính tương tự như những gì ta thấy trong method createMicroservice() ở trên.

@Module({
  imports: [
    ClientsModule.register([
      { name: 'MATH_SERVICE', transport: Transport.TCP },
    ]),
  ],
})

Ngoài ra, bạn có thể dùng method registerAsync() nếu cần cung cấp configuration hoặc thực hiện các xử lý bất đồng bộ khác trong quá trình setup.

@Module({
  imports: [
    ClientsModule.registerAsync([
      {
        imports: [ConfigModule],
        name: 'MATH_SERVICE',
        useFactory: async (configService: ConfigService) => ({
          transport: Transport.TCP,
          options: {
            url: configService.get('URL'),
          },
        }),
        inject: [ConfigService],
      },
    ]),
  ],
})

Sau khi import module, bạn có thể inject một instance ClientProxy đã được cấu hình với các option của transporter 'MATH_SERVICE' bằng decorator @Inject().

constructor(
  @Inject('MATH_SERVICE') private client: ClientProxy,
) {}

Gợi ý Class ClientsModule và ClientProxy được import từ package @nestjs/microservices.

Đôi khi, bạn có thể cần lấy configuration của transporter từ một service khác (chẳng hạn ConfigService), thay vì hard-code trong ứng dụng client. Để làm điều này, bạn có thể đăng ký một custom provider dùng class ClientProxyFactory. Class này cung cấp static method create() nhận vào một object option của transporter và trả về một instance ClientProxy đã được tùy chỉnh.

@Module({
  providers: [
    {
      provide: 'MATH_SERVICE',
      useFactory: (configService: ConfigService) => {
        const mathSvcOptions = configService.getMathSvcOptions();
        return ClientProxyFactory.create(mathSvcOptions);
      },
      inject: [ConfigService],
    }
  ]
  ...
})

Gợi ý ClientProxyFactory được import từ package @nestjs/microservices.

Một lựa chọn khác là dùng property decorator @Client().

@Client({ transport: Transport.TCP })
client: ClientProxy;

Gợi ý Decorator @Client() được import từ package @nestjs/microservices.

Dùng decorator @Client() không phải là kỹ thuật được khuyến nghị, vì nó khó test hơn và khó chia sẻ chung một client instance hơn.

ClientProxy là lazy. Nó không khởi tạo kết nối ngay lập tức. Thay vào đó, kết nối sẽ được thiết lập trước lần gọi microservice đầu tiên, sau đó được tái sử dụng cho mỗi lần gọi tiếp theo. Tuy nhiên, nếu bạn muốn trì hoãn quá trình bootstrap ứng dụng cho đến khi kết nối được thiết lập, bạn có thể tự chủ động gọi method connect() của object ClientProxy trong lifecycle hook OnApplicationBootstrap.

async onApplicationBootstrap() {
  await this.client.connect();
}

Nếu không thể tạo được kết nối, method connect() sẽ reject với error object tương ứng.

Gửi message (Sending messages)

ClientProxy expose method send(). Method này dùng để gọi microservice và trả về một Observable chứa response. Nhờ vậy, ta có thể subscribe vào các giá trị được emit một cách dễ dàng.

accumulate(): Observable<number> {
  const pattern = { cmd: 'sum' };
  const payload = [1, 2, 3];
  return this.client.send<number>(pattern, payload);
}
accumulate() {
  const pattern = { cmd: 'sum' };
  const payload = [1, 2, 3];
  return this.client.send(pattern, payload);
}

Method send() nhận hai tham số, pattern và payload. pattern phải khớp với pattern đã định nghĩa trong decorator @MessagePattern(). payload là message mà ta muốn truyền tới microservice từ xa. Method này trả về một Observable lạnh (cold), nghĩa là bạn phải chủ động subscribe vào nó trước khi message được gửi đi.

Publish event (Publishing events)

Để gửi một event, dùng method emit() của object ClientProxy. Method này publish một event tới message broker.

async publish() {
  this.client.emit<number>('user_created', new UserCreatedEvent());
}
async publish() {
  this.client.emit('user_created', new UserCreatedEvent());
}

Method emit() nhận hai tham số: pattern và payload. pattern phải khớp với pattern đã định nghĩa trong decorator @EventPattern(), còn payload là dữ liệu của event mà bạn muốn truyền tới microservice từ xa. Method này trả về một Observable nóng (hot) (khác với Observable lạnh mà send() trả về), nghĩa là bất kể bạn có subscribe vào observable hay không, proxy vẫn sẽ ngay lập tức thử gửi event đi.

Ghi chú người dịch “Cold Observable” và “hot Observable” là thuật ngữ của RxJS: cold observable chỉ thực sự bắt đầu chạy logic (ở đây là gửi request) khi có ai đó subscribe(), giống một hàm chỉ chạy khi được gọi. Hot observable thì logic (gửi event) đã chạy ngay khi bạn gọi emit(), subscribe hay không không ảnh hưởng — giống việc bạn đã bấm gửi email rồi, có mở hộp thư theo dõi hay không email vẫn được gửi.

Phạm vi theo request (Request-scoping)

Với những ai quen làm việc với các ngôn ngữ lập trình khác, có thể sẽ bất ngờ khi biết rằng trong Nest, phần lớn mọi thứ được chia sẻ (shared) giữa các request đến. Điều này bao gồm connection pool tới database, các service singleton có global state, và nhiều thứ khác. Hãy nhớ rằng Node.js không theo mô hình request/response đa luồng (multi-threaded) và stateless, nơi mỗi request được xử lý bởi một thread riêng. Vì vậy, dùng singleton instance là an toàn cho ứng dụng của chúng ta.

Tuy nhiên, có những trường hợp đặc biệt (edge case) mà việc handler có lifetime theo từng request lại là điều mong muốn. Ví dụ như caching theo từng request trong ứng dụng GraphQL, request tracking, hoặc multi-tenancy. Bạn có thể tìm hiểu thêm về cách kiểm soát scope tại đây.

Handler và provider có scope request có thể inject RequestContext bằng decorator @Inject() kết hợp với token CONTEXT:

import { Injectable, Scope, Inject } from '@nestjs/common';
import { CONTEXT, RequestContext } from '@nestjs/microservices';

@Injectable({ scope: Scope.REQUEST })
export class CatsService {
  constructor(@Inject(CONTEXT) private ctx: RequestContext) {}
}

Ghi chú người dịch Nếu bạn từng dùng Go, RequestContext ở đây tương tự vai trò của context.Context được truyền qua các lớp gọi hàm trong một request — chỉ khác là ở Nest, bạn không cần truyền tay qua từng tham số hàm, mà inject qua DI. Cần lưu ý: mỗi request-scoped provider sẽ tạo instance mới cho từng request, tốn overhead hơn singleton, nên chỉ dùng khi thực sự cần.

Điều này cho phép truy cập object RequestContext, có hai property:

export interface RequestContext<T = any> {
  pattern: string | Record<string, any>;
  data: T;
}

Property data là message payload được gửi bởi bên sản xuất message (producer). Property pattern là pattern được dùng để xác định handler phù hợp để xử lý message đến.

Cập nhật trạng thái instance (Instance status updates)

Để nhận cập nhật theo thời gian thực về kết nối và trạng thái của driver instance bên dưới, bạn có thể subscribe vào stream status. Stream này cung cấp các cập nhật trạng thái riêng cho từng driver được chọn. Ví dụ, nếu bạn dùng transporter TCP (mặc định), stream status sẽ emit event connected và disconnected.

this.client.status.subscribe((status: TcpStatus) => {
  console.log(status);
});

Gợi ý Type TcpStatus được import từ package @nestjs/microservices.

Tương tự, bạn có thể subscribe vào stream status của server để nhận thông báo về trạng thái của server.

const server = app.connectMicroservice<MicroserviceOptions>(...);
server.status.subscribe((status: TcpStatus) => {
  console.log(status);
});

Lắng nghe các event nội bộ (Listening to internal events)

Trong một số trường hợp, bạn có thể muốn lắng nghe các event nội bộ được emit bởi microservice. Ví dụ, bạn có thể lắng nghe event error để kích hoạt thêm các xử lý khác khi có lỗi xảy ra. Để làm điều này, dùng method on(), như dưới đây:

this.client.on('error', (err) => {
  console.error(err);
});

Tương tự, bạn có thể lắng nghe các event nội bộ của server:

server.on<TcpEvents>('error', (err) => {
  console.error(err);
});

Gợi ý Type TcpEvents được import từ package @nestjs/microservices.

Truy cập driver bên dưới (Underlying driver access)

Với các use case phức tạp hơn, bạn có thể cần truy cập driver instance bên dưới. Điều này hữu ích cho các tình huống như chủ động đóng kết nối hoặc dùng các method riêng của driver. Tuy nhiên, hãy nhớ rằng trong hầu hết trường hợp, bạn không nên cần truy cập trực tiếp driver.

Để làm điều này, bạn có thể dùng method unwrap(), trả về driver instance bên dưới. Generic type parameter nên chỉ định type của driver instance mà bạn mong đợi.

const netServer = this.client.unwrap<Server>();

Ở đây, Server là một type được import từ module net.

Tương tự, bạn có thể truy cập driver instance bên dưới của server:

const netServer = server.unwrap<Server>();

Xử lý timeout (Handling timeouts)

Trong hệ thống phân tán, microservices đôi khi có thể bị down hoặc không khả dụng. Để tránh chờ đợi vô thời hạn, bạn có thể dùng timeout. Timeout là một pattern rất hữu ích khi giao tiếp với các service khác. Để áp dụng timeout cho các lệnh gọi microservice, bạn có thể dùng operator timeout của RxJS. Nếu microservice không trả response trong khoảng thời gian quy định, một exception sẽ được throw, và bạn có thể catch để xử lý phù hợp.

Để triển khai điều này, bạn cần dùng package rxjs. Chỉ cần dùng operator timeout trong pipe:

this.client
  .send<TResult, TInput>(pattern, data)
  .pipe(timeout(5000));
this.client
  .send(pattern, data)
  .pipe(timeout(5000));

Gợi ý Operator timeout được import từ package rxjs/operators.

Sau 5 giây, nếu microservice không phản hồi, nó sẽ throw một lỗi.

Trace một request đi qua nhiều service (Tracing a request across services)

Timeout cho bạn biết một lệnh gọi không kịp trả về đúng hạn. Nhưng nó không cho biết thời gian đã trôi vào đâu — và trong một hệ thống gồm năm service giao tiếp qua TCP, NATS, và Kafka, đó mới chính là câu hỏi đáng đặt ra. Một request tới gateway mất 3 giây có thể tiêu tốn 2.9 giây trong đó ở một service downstream mà không ai ngờ tới, và log riêng của từng service, khi xem một mình, đều sẽ khẳng định là mọi thứ vẫn ổn.

Cách khắc phục thông thường là tự truyền tay một correlation id qua từng transport, rồi ghép lại timeline sau đó. NestJS Observe làm việc ghép nối này cho bạn: instrument từng service với SDK @nestjs/observe và forward trace id qua bất kỳ channel nào transport đang sẵn có — một header Kafka, một header NATS, một field trong payload TCP — rồi dashboard sẽ tái tạo lại một waterfall duy nhất trải dài qua mọi service đã tham gia:

Trace correlation across services

Từ đó, timeout không còn là một bí ẩn nữa. Bạn có thể thấy send() ở gateway đang chờ, consumer nhận message (và nó đã “nằm chờ” bao lâu trước khi được xử lý), câu query bên trong nó chạy lâu, và lỗi mà nó cuối cùng throw ra, kèm dòng code nguồn — tất cả trên cùng một mốc thời gian. Message handler và event handler được instrument tự động, nên các handler @MessagePattern() và @EventPattern() hiện ra như các operation mà không cần khai báo span thủ công.

Việc forward trace id là phần duy nhất cần code trong ứng dụng, vì chỉ bạn mới biết transport của mình còn “chỗ trống” nào để tận dụng. Xem Distributed tracing để biết pattern cho từng transport, và chương Observability để thiết lập.

Ghi chú người dịch Đoạn này giới thiệu NestJS Observe — dịch vụ observability riêng của đội Nest (có gói Free và gói trả phí), không phải tính năng core của framework. Về bản chất kỹ thuật, đây là distributed tracing tương tự OpenTelemetry: mỗi request có một trace id được truyền qua tất cả service liên quan (qua header hoặc field trong payload), rồi một hệ thống trung tâm ghép các “span” từ các service lại thành một timeline duy nhất — giống cách Jaeger/Zipkin hoạt động ở các stack khác.

Hỗ trợ TLS (TLS support)

Khi giao tiếp bên ngoài một private network, việc mã hóa traffic để đảm bảo an toàn là rất quan trọng. Trong NestJS, điều này có thể thực hiện bằng TLS qua TCP, dùng module TLS có sẵn của Node. Nest hỗ trợ sẵn TLS trong transport TCP, cho phép ta mã hóa giao tiếp giữa các microservice hoặc client.

Để bật TLS cho một TCP server, bạn cần một private key và một certificate ở dạng PEM. Các file này được thêm vào option của server bằng cách thiết lập tlsOptions và chỉ định file key, cert, như dưới đây:

import * as fs from 'node:fs';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';

async function bootstrap() {
  const key = fs.readFileSync('<pathToKeyFile>', 'utf8').toString();
  const cert = fs.readFileSync('<pathToCertFile>', 'utf8').toString();

  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    AppModule,
    {
      transport: Transport.TCP,
      options: {
        tlsOptions: {
          key,
          cert,
        },
      },
    },
  );

  await app.listen();
}
await bootstrap();

Để một client có thể giao tiếp an toàn qua TLS, ta cũng định nghĩa object tlsOptions, nhưng lần này với CA certificate. Đây là certificate của tổ chức đã ký certificate của server. Điều này đảm bảo client tin tưởng certificate của server và có thể thiết lập một kết nối an toàn.

import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'MATH_SERVICE',
        transport: Transport.TCP,
        options: {
          tlsOptions: {
            ca: [fs.readFileSync('<pathToCaFile>', 'utf-8').toString()],
          },
        },
      },
    ]),
  ],
})
export class AppModule {}

Bạn cũng có thể truyền một array các CA nếu hệ thống của bạn liên quan tới nhiều tổ chức tin cậy khác nhau.

Sau khi mọi thứ đã được thiết lập, bạn có thể inject ClientProxy như bình thường bằng decorator @Inject() để dùng client trong service của mình. Điều này đảm bảo giao tiếp được mã hóa xuyên suốt các microservice NestJS của bạn, với module TLS của Node xử lý phần mã hóa.

Để biết thêm thông tin, xem tài liệu TLS của Node.

Cấu hình động (Dynamic configuration)

Khi một microservice cần được cấu hình bằng ConfigService (từ package @nestjs/config), nhưng injection context chỉ có sẵn sau khi microservice instance đã được tạo, AsyncMicroserviceOptions cung cấp một giải pháp. Cách tiếp cận này cho phép cấu hình động, đảm bảo tích hợp trơn tru với ConfigService.

import { ConfigService } from '@nestjs/config';
import { AsyncMicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module.js';

async function bootstrap() {
  const app = await NestFactory.createMicroservice<AsyncMicroserviceOptions>(
    AppModule,
    {
      useFactory: (configService: ConfigService) => ({
        transport: Transport.TCP,
        options: {
          host: configService.get<string>('HOST'),
          port: configService.get<number>('PORT'),
        },
      }),
      inject: [ConfigService],
    },
  );

  await app.listen();
}
await bootstrap();

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.