☰ 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

Custom transporters (transporter tùy chỉnh)

Bản dịch từ docs.nestjs.com/microservices/custom-transport

Nest cung cấp sẵn nhiều transporter khác nhau, cùng với một API cho phép bạn tự xây dựng chiến lược transport (transport strategy) tùy chỉnh. Transporter giúp bạn kết nối các component qua network bằng một lớp giao tiếp (communications layer) có thể cắm ghép (pluggable) và một message protocol ở mức application rất đơn giản (đọc thêm bài viết đầy đủ).

Gợi ý Xây dựng microservice với Nest không nhất thiết phải dùng package @nestjs/microservices. Ví dụ, nếu bạn muốn giao tiếp với các service bên ngoài (giả sử là các microservice khác viết bằng ngôn ngữ khác), có thể bạn không cần tới toàn bộ tính năng mà thư viện @nestjs/microservice cung cấp. Thực tế, nếu bạn không cần các decorator (@EventPattern hoặc @MessagePattern) để khai báo subscriber, chỉ cần chạy một Standalone Application và tự quản lý việc kết nối/subscribe channel bằng tay là đủ cho hầu hết trường hợp, đồng thời linh hoạt hơn.

Với custom transporter, bạn có thể tích hợp bất kỳ messaging system/protocol nào (bao gồm Google Cloud Pub/Sub, Amazon Kinesis, v.v…) hoặc mở rộng transporter có sẵn, thêm các tính năng riêng (ví dụ QoS cho MQTT).

Gợi ý Để hiểu rõ hơn cách microservice của Nest hoạt động và cách bạn có thể mở rộng khả năng của các transporter hiện có, chúng tôi khuyến khích đọc series bài viết NestJS Microservices in Action và Advanced NestJS Microservices.

Creating a strategy (tạo strategy)

Trước tiên, hãy định nghĩa một class đại diện cho custom transporter của chúng ta.

import { CustomTransportStrategy, Server } from '@nestjs/microservices';

class GoogleCloudPubSubServer
  extends Server
  implements CustomTransportStrategy
{
  /**
   * Triggered when you run "app.listen()".
   */
  listen(callback: () => void) {
    callback();
  }

  /**
   * Triggered on application shutdown.
   */
  close() {}

  /**
   * You can ignore this method if you don't want transporter users
   * to be able to register event listeners. Most custom implementations
   * will not need this.
   */
  on(event: string, callback: Function) {
    throw new Error('Method not implemented.');
  }

  /**
   * You can ignore this method if you don't want transporter users
   * to be able to retrieve the underlying native server. Most custom implementations
   * will not need this.
   */
  unwrap<T = never>(): T {
    throw new Error('Method not implemented.');
  }
}

Cảnh báo Lưu ý là chúng ta sẽ không implement đầy đủ một Google Cloud Pub/Sub server thực sự trong chương này, vì việc đó đòi hỏi phải đi sâu vào chi tiết kỹ thuật riêng của transporter đó.

Trong ví dụ trên, chúng ta khai báo class GoogleCloudPubSubServer và cung cấp hai method listen() và close() mà interface CustomTransportStrategy bắt buộc phải có. Class này còn extend từ class Server được import từ package @nestjs/microservices, cung cấp sẵn một số method hữu ích, ví dụ các method được Nest runtime dùng để đăng ký message handler. Ngoài ra, nếu bạn muốn mở rộng khả năng của một transport strategy có sẵn, bạn có thể extend từ class server tương ứng, ví dụ ServerRedis. Theo quy ước, chúng ta thêm hậu tố "Server" vào tên class vì nó sẽ chịu trách nhiệm subscribe message/event (và phản hồi lại nếu cần).

Sau khi có class này, chúng ta có thể dùng strategy tùy chỉnh của mình thay cho một transporter có sẵn, như sau:

const app = await NestFactory.createMicroservice<MicroserviceOptions>(
  AppModule,
  {
    strategy: new GoogleCloudPubSubServer(),
  },
);

Về cơ bản, thay vì truyền object options thông thường gồm transport và options, chúng ta truyền một property duy nhất là strategy, với giá trị là một instance của custom transporter class.

Trở lại với class GoogleCloudPubSubServer, trong một ứng dụng thực tế, chúng ta sẽ thiết lập kết nối đến message broker/external service và đăng ký subscriber/lắng nghe các channel cụ thể trong method listen() (sau đó gỡ subscription & đóng kết nối trong method teardown close()), nhưng vì việc này yêu cầu hiểu rõ cách các microservice của Nest giao tiếp với nhau, chúng tôi khuyến khích đọc series bài viết này. Ở chương này, chúng ta sẽ tập trung vào các khả năng mà class Server cung cấp và cách bạn có thể tận dụng chúng để xây dựng custom strategy.

Ví dụ, giả sử ở đâu đó trong ứng dụng có định nghĩa message handler sau:

@MessagePattern('echo')
echo(@Payload() data: object) {
  return data;
}

Message handler này sẽ được Nest runtime tự động đăng ký. Với class Server, bạn có thể xem những message pattern nào đã được đăng ký, và cũng có thể truy cập, thực thi trực tiếp các method đã được gán cho pattern đó. Để kiểm tra điều này, hãy thêm một console.log đơn giản bên trong method listen(), trước khi hàm callback được gọi:

listen(callback: () => void) {
  console.log(this.messageHandlers);
  callback();
}

Sau khi restart ứng dụng, bạn sẽ thấy log sau trong terminal:

Map { 'echo' => [AsyncFunction] { isEventHandler: false } }

Gợi ý Nếu dùng decorator @EventPattern, bạn sẽ thấy output tương tự, nhưng với property isEventHandler là true.

Như bạn thấy, property messageHandlers là một Map chứa tất cả message (và event) handler, trong đó pattern được dùng làm key. Bây giờ, bạn có thể dùng một key (ví dụ "echo") để lấy reference tới message handler:

async listen(callback: () => void) {
  const echoHandler = this.messageHandlers.get('echo');
  console.log(await echoHandler('Hello world!'));
  callback();
}

Sau khi thực thi echoHandler với một chuỗi bất kỳ làm argument (ở đây là "Hello world!"), chúng ta sẽ thấy kết quả trong console:

Hello world!

Điều này cho thấy method handler của chúng ta đã được thực thi đúng.

Khi dùng CustomTransportStrategy cùng với Interceptors, các handler sẽ được bọc trong RxJS stream. Nghĩa là bạn cần subscribe vào chúng để logic bên trong stream được thực thi (ví dụ: tiếp tục vào logic của controller sau khi interceptor đã chạy xong).

Ví dụ dưới đây minh họa điều này:

async listen(callback: () => void) {
  const echoHandler = this.messageHandlers.get('echo');
  const streamOrResult = await echoHandler('Hello World');
  if (isObservable(streamOrResult)) {
    streamOrResult.subscribe();
  }
  callback();
}

Reporting event handler errors (báo lỗi từ event handler)

Event handler không có response stream, nên một exception ném ra bên trong nó không có nơi nào để đi tiếp. Nest sẽ tự log lại lỗi đó, nhưng chỉ sau khi các exception filter của bạn đã chạy. Nếu một filter khớp với exception đó, filter đó sẽ là nơi duy nhất báo lỗi.

Một số transporter tự await event stream và chuyển lỗi đó cho client library của chúng để library đó tự báo lỗi. ServerKafka làm theo cách này: kafkajs sẽ tự log lại rejection, nên nếu Nest cũng log thì sẽ bị trùng lặp. Trong trường hợp đó, đặt propagatesEventHandlerErrors thành true:

class GoogleCloudPubSubServer
  extends Server
  implements CustomTransportStrategy
{
  /**
   * The client library reports an event handler failure, so Nest must not
   * log it a second time.
   */
  public override readonly propagatesEventHandlerErrors = true;
}

Giá trị mặc định là false, phù hợp với transporter dùng handleEvent được kế thừa sẵn. Trong trường hợp đó không có gì subscribe vào event stream, nên Nest là nơi duy nhất có thể báo lỗi.

Client proxy

Như đã nói ở phần đầu, bạn không nhất thiết phải dùng package @nestjs/microservices để tạo microservice, nhưng nếu bạn quyết định dùng nó và cần tích hợp một custom strategy, bạn cũng cần cung cấp thêm một “client” class.

Gợi ý Một lần nữa, để implement đầy đủ một client class tương thích với tất cả tính năng của @nestjs/microservices (ví dụ streaming) đòi hỏi hiểu rõ các kỹ thuật giao tiếp mà framework sử dụng. Để tìm hiểu thêm, xem bài viết này.

Để giao tiếp với external service/emit & publish message (hoặc event), bạn có thể dùng SDK package riêng của thư viện đó, hoặc tự implement một custom client class bằng cách extend ClientProxy, như sau:

import { ClientProxy, ReadPacket, WritePacket } from '@nestjs/microservices';

class GoogleCloudPubSubClient extends ClientProxy {
  async connect(): Promise<any> {}
  async close() {}
  async dispatchEvent(packet: ReadPacket<any>): Promise<any> {}
  publish(
    packet: ReadPacket<any>,
    callback: (packet: WritePacket<any>) => void,
  ): Function {}
  unwrap<T = never>(): T {
    throw new Error('Method not implemented.');
  }
}

Cảnh báo Lưu ý là chúng ta sẽ không implement đầy đủ một Google Cloud Pub/Sub client thực sự trong chương này, vì việc đó đòi hỏi phải đi sâu vào chi tiết kỹ thuật riêng của transporter đó.

Như bạn thấy, class ClientProxy yêu cầu chúng ta cung cấp một số method để thiết lập & đóng kết nối, cùng với việc publish message (publish) và event (dispatchEvent). Lưu ý, nếu bạn không cần hỗ trợ kiểu giao tiếp request-response, bạn có thể để method publish() trống. Tương tự, nếu không cần hỗ trợ giao tiếp dựa trên event, có thể bỏ qua method dispatchEvent().

Để quan sát các method này được thực thi khi nào và như thế nào, hãy thêm vài console.log, như sau:

class GoogleCloudPubSubClient extends ClientProxy {
  async connect(): Promise<any> {
    console.log('connect');
  }

  async close() {
    console.log('close');
  }

  async dispatchEvent(packet: ReadPacket<any>): Promise<any> {
    return console.log('event to dispatch: ', packet);
  }

  publish(
    packet: ReadPacket<any>,
    callback: (packet: WritePacket<any>) => void,
  ): Function {
    console.log('message:', packet);

    // In a real-world application, the "callback" function should be executed
    // with payload sent back from the responder. Here, we'll simply simulate (5 seconds delay)
    // that response came through by passing the same "data" as we've originally passed in.
    //
    // The "isDisposed" bool on the WritePacket tells the response that no further data is
    // expected. If not sent or is false, this will simply emit data to the Observable.
    setTimeout(() => callback({ 
      response: packet.data,
      isDisposed: true,
    }), 5000);

    return () => console.log('teardown');
  }

  unwrap<T = never>(): T {
    throw new Error('Method not implemented.');
  }
}

Ghi chú người dịch Nếu bạn đã quen với việc viết middleware cho Express hoặc adapter tùy chỉnh cho một message queue trong Go, thì ClientProxy ở đây đóng vai trò tương tự: nó chuẩn hóa (abstraction) cách gửi message qua network để phần code gọi send()/emit() không cần biết bên dưới là RabbitMQ, Kafka hay Google Pub/Sub. Cái khó là publish() phải tự quản lý observable/callback thủ công thay vì async/await đơn giản, vì Nest cần một cơ chế hủy đăng ký (teardown) khi consumer unsubscribe giữa đường — giống hệt việc bạn phải trả về một hàm “cleanup” trong useEffect của React, chỉ khác là ở đây nó phục vụ network stream thay vì DOM.

Với đoạn code trên, hãy tạo một instance của class GoogleCloudPubSubClient và chạy method send() (mà có thể bạn đã thấy ở các chương trước), subscribe vào observable stream trả về.

const googlePubSubClient = new GoogleCloudPubSubClient();
googlePubSubClient
  .send('pattern', 'Hello world!')
  .subscribe((response) => console.log(response));

Bây giờ, bạn sẽ thấy output sau trong terminal:

connect
message: { pattern: 'pattern', data: 'Hello world!' }
Hello world! // <-- after 5 seconds

Để kiểm tra method “teardown” (được method publish() của chúng ta trả về) có được thực thi đúng không, hãy áp dụng operator timeout vào stream, đặt là 2 giây để chắc chắn nó throw trước khi setTimeout gọi hàm callback.

const googlePubSubClient = new GoogleCloudPubSubClient();
googlePubSubClient
  .send('pattern', 'Hello world!')
  .pipe(timeout(2000))
  .subscribe(
    (response) => console.log(response),
    (error) => console.error(error.message),
  );

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

Với operator timeout được áp dụng, output trong terminal sẽ như sau:

connect
message: { pattern: 'pattern', data: 'Hello world!' }
teardown // <-- teardown
Timeout has occurred

Để dispatch một event (thay vì gửi message), dùng method emit():

googlePubSubClient.emit('event', 'Hello world!');

Và đây là những gì bạn sẽ thấy trong console:

connect
event to dispatch:  { pattern: 'event', data: 'Hello world!' }

Message serialization (tuần tự hóa message)

Nếu bạn cần thêm logic tùy chỉnh xung quanh việc serialization của response ở phía client, bạn có thể dùng một custom class extend từ class ClientProxy hoặc một trong các class con của nó. Để chỉnh sửa các request thành công, bạn có thể override method serializeResponse, còn để chỉnh sửa các lỗi đi qua client này, bạn có thể override method serializeError. Để sử dụng custom class này, bạn truyền chính class đó vào method ClientsModule.register() thông qua property customClass. Dưới đây là ví dụ một custom ClientProxy serialize mỗi lỗi thành một RpcException.

import { ClientTCP, RpcException } from '@nestjs/microservices';

class ErrorHandlingProxy extends ClientTCP {
  serializeError(err: Error) {
    return new RpcException(err);
  }
}

và sau đó dùng nó trong ClientsModule như sau:

@Module({
  imports: [
    ClientsModule.register([{
      name: 'CustomProxy',
      customClass: ErrorHandlingProxy,
    }]),
  ]
})
export class AppModule

Gợi ý Ở đây, chính class được truyền vào customClass, không phải một instance của class. Nest sẽ tự tạo instance bên dưới cho bạn, và sẽ truyền mọi option đã cấu hình ở property options vào ClientProxy 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.