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

Nest Commander

Bản dịch từ docs.nestjs.com/recipes/nest-commander

Mở rộng từ tài liệu standalone application, còn có package nest-commander để viết các ứng dụng command line theo cấu trúc tương tự một ứng dụng Nest thông thường.

Info nest-commander là một package bên thứ ba và không được quản lý bởi toàn bộ đội ngũ core của NestJS. Vui lòng báo cáo bất kỳ vấn đề nào gặp phải với thư viện tại repository tương ứng

Cài đặt

Cũng giống như mọi package khác, bạn cần cài nó trước khi có thể sử dụng.

$ npm i nest-commander

Một file Command

nest-commander giúp việc viết các ứng dụng command-line mới trở nên dễ dàng bằng decorator thông qua decorator @Command() cho class và decorator @Option() cho các method của class đó. Mỗi file command nên implement abstract class CommandRunner và nên được đánh dấu bằng decorator @Command().

Mỗi command được Nest coi như một @Injectable(), nên Dependency Injection thông thường vẫn hoạt động như bạn mong đợi. Điều duy nhất cần lưu ý là abstract class CommandRunner, class này nên được implement bởi mỗi command. Abstract class CommandRunner đảm bảo mọi command đều có một method run trả về Promise<void> và nhận vào các tham số string[], Record<string, any>. Method run là nơi bạn có thể bắt đầu toàn bộ logic của mình, nó sẽ nhận vào bất kỳ tham số nào không khớp với các option flag và truyền chúng vào dưới dạng mảng, phòng khi bạn thực sự muốn làm việc với nhiều tham số. Còn về các option, tức Record<string, any>, tên của các property này khớp với property name được truyền cho decorator @Option(), còn giá trị của chúng khớp với giá trị trả về của option handler. Nếu bạn muốn có type safety tốt hơn, bạn hoàn toàn có thể tạo một interface cho các option của mình.

Chạy Command

Tương tự như cách trong một ứng dụng NestJS chúng ta dùng NestFactory để tạo server và chạy nó bằng listen, package nest-commander cung cấp một API đơn giản để chạy server của bạn. Import CommandFactory và dùng static method run, truyền vào module gốc của ứng dụng. Đoạn code sẽ trông như sau

import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module.js';

async function bootstrap() {
  await CommandFactory.run(AppModule);
}

await bootstrap();

Mặc định, logger của Nest sẽ bị tắt khi dùng CommandFactory. Tuy nhiên bạn vẫn có thể cung cấp nó, dưới dạng tham số thứ hai của hàm run. Bạn có thể truyền vào một logger NestJS tùy chỉnh, hoặc một mảng các log level bạn muốn giữ lại - có thể hữu ích nếu ít nhất truyền vào ['error'] ở đây, nếu bạn chỉ muốn in ra log lỗi của Nest.

import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module.js';
import { LogService } from './log.service.js';

async function bootstrap() {
  await CommandFactory.run(AppModule, new LogService());

  // hoặc, nếu bạn chỉ muốn in ra warning và error của Nest
  await CommandFactory.run(AppModule, ['warn', 'error']);
}

await bootstrap();

Và thế là xong. Bên dưới, CommandFactory sẽ lo việc gọi NestFactory giúp bạn và gọi app.close() khi cần thiết, nên bạn không cần phải lo về memory leak ở đó. Nếu bạn cần thêm xử lý lỗi, bạn luôn có thể dùng try/catch bọc quanh lệnh run, hoặc chain thêm method .catch() vào lời gọi bootstrap().

Testing

Vậy thì viết một command line script “siêu ngầu” mà không thể test dễ dàng thì có ích gì đúng không? May mắn là nest-commander có một số tiện ích bạn có thể tận dụng, phù hợp hoàn hảo với hệ sinh thái NestJS, những ai đã quen với Nest sẽ thấy rất thân thuộc. Thay vì dùng CommandFactory để build command ở chế độ test, bạn có thể dùng CommandTestFactory và truyền vào metadata của mình, cách hoạt động khá giống với Test.createTestingModule từ @nestjs/testing. Thực tế, nó sử dụng chính package đó bên dưới. Bạn cũng vẫn có thể chain thêm các method overrideProvider trước khi gọi compile(), nhờ đó có thể thay thế các thành phần DI ngay trong bài test.

Ghép tất cả lại

Class dưới đây tương đương với việc có một CLI command có thể nhận subcommand basic hoặc được gọi trực tiếp, với -n, -s, và -b (cùng với các flag dài tương ứng) đều được hỗ trợ và có custom parser cho từng option. Flag --help cũng được hỗ trợ, như thông lệ của commander.

import { Command, CommandRunner, Option } from 'nest-commander';
import { LogService } from './log.service.js';

interface BasicCommandOptions {
  string?: string;
  boolean?: boolean;
  number?: number;
}

@Command({ name: 'basic', description: 'A parameter parse' })
export class BasicCommand extends CommandRunner {
  constructor(private readonly logService: LogService) {
    super()
  }

  async run(
    passedParam: string[],
    options?: BasicCommandOptions,
  ): Promise<void> {
    if (options?.boolean !== undefined && options?.boolean !== null) {
      this.runWithBoolean(passedParam, options.boolean);
    } else if (options?.number) {
      this.runWithNumber(passedParam, options.number);
    } else if (options?.string) {
      this.runWithString(passedParam, options.string);
    } else {
      this.runWithNone(passedParam);
    }
  }

  @Option({
    flags: '-n, --number [number]',
    description: 'A basic number parser',
  })
  parseNumber(val: string): number {
    return Number(val);
  }

  @Option({
    flags: '-s, --string [string]',
    description: 'A string return',
  })
  parseString(val: string): string {
    return val;
  }

  @Option({
    flags: '-b, --boolean [boolean]',
    description: 'A boolean parser',
  })
  parseBoolean(val: string): boolean {
    return JSON.parse(val);
  }

  runWithString(param: string[], option: string): void {
    this.logService.log({ param, string: option });
  }

  runWithNumber(param: string[], option: number): void {
    this.logService.log({ param, number: option });
  }

  runWithBoolean(param: string[], option: boolean): void {
    this.logService.log({ param, boolean: option });
  }

  runWithNone(param: string[]): void {
    this.logService.log({ param });
  }
}

Đảm bảo command class được thêm vào một module

@Module({
  providers: [LogService, BasicCommand],
})
export class AppModule {}

Và giờ để chạy CLI trong main.ts của bạn, bạn có thể làm như sau

async function bootstrap() {
  await CommandFactory.run(AppModule);
}

await bootstrap();

Và như vậy, bạn đã có một ứng dụng command line.

Thông tin thêm

Truy cập trang docs của nest-commander để biết thêm thông tin, ví dụ, và tài liệu API.

Ghi chú người dịch Cách tiếp cận này khá giống Artisan Console trong Laravel (class command với $signature, handle()) hoặc cobra.Command trong Go: bạn định nghĩa command như một class/struct riêng, còn framework lo phần parse argument và gọi đúng handler. Điểm hay ở đây là mỗi command vẫn là một @Injectable() bình thường, nên bạn tái sử dụng được toàn bộ provider, service đã viết cho phần web app - không cần viết lại logic riêng cho CLI.

Ghi chú người dịch Vì nest-commander là package của bên thứ ba (không thuộc @nestjs/* chính thức), nên khi nâng cấp NestJS lên major version mới, hãy kiểm tra kỹ tính tương thích của package này trước, tránh trường hợp nó chưa kịp hỗ trợ API 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.