☰ 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

MikroORM

Bản dịch từ docs.nestjs.com/recipes/mikroorm

Recipe này giúp người dùng bắt đầu với MikroORM trong Nest. MikroORM là ORM TypeScript cho Node.js dựa trên các pattern Data Mapper, Unit of Work và Identity Map. Đây là một lựa chọn thay thế tốt cho TypeORM, và việc migrate từ TypeORM sang khá dễ dàng. Tài liệu đầy đủ về MikroORM có thể tìm thấy tại đây.

Info @mikro-orm/nestjs là một package bên thứ ba và không được quản lý bởi độ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ách dễ nhất để tích hợp MikroORM vào Nest là qua module @mikro-orm/nestjs. Chỉ cần cài nó cùng với Nest, MikroORM và driver tương ứng:

$ npm i @mikro-orm/core @mikro-orm/nestjs @mikro-orm/sqlite

MikroORM cũng hỗ trợ postgres, sqlite, và mongo. Xem tài liệu chính thức để biết đầy đủ các driver.

Gợi ý Kể từ MikroORM v6, EntityManager và EntityRepository nên được import từ package driver của bạn (ví dụ @mikro-orm/sqlite), trong khi decorator và các type khác thì đến từ @mikro-orm/core. Bản thân MikroOrmModule được export từ @mikro-orm/nestjs. Các đoạn code bên dưới đã bao gồm các câu lệnh import liên quan.

Sau khi cài đặt xong, chúng ta có thể import MikroOrmModule vào AppModule gốc.

import { MikroOrmModule } from '@mikro-orm/nestjs';
import { SqliteDriver } from '@mikro-orm/sqlite';

@Module({
  imports: [
    MikroOrmModule.forRoot({
      entities: ['./dist/entities'],
      entitiesTs: ['./src/entities'],
      dbName: 'my-db-name.sqlite3',
      driver: SqliteDriver,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Method forRoot() nhận vào object cấu hình giống hệt với init() từ package MikroORM. Xem trang này để biết tài liệu cấu hình đầy đủ.

Ngoài ra, chúng ta có thể cấu hình CLI bằng cách tạo một file cấu hình mikro-orm.config.ts rồi gọi forRoot() mà không cần đối số nào.

import { MikroOrmModule } from '@mikro-orm/nestjs';

@Module({
  imports: [
    MikroOrmModule.forRoot(),
  ],
  ...
})
export class AppModule {}

Nhưng cách này sẽ không hoạt động khi bạn dùng các build tool có tree shaking, vì vậy tốt hơn nên cung cấp config một cách tường minh:

import { MikroOrmModule } from '@mikro-orm/nestjs';
import config from './mikro-orm.config.js'; // config ORM của bạn

@Module({
  imports: [
    MikroOrmModule.forRoot(config),
  ],
  ...
})
export class AppModule {}

Sau đó, EntityManager sẽ sẵn sàng để inject trên toàn bộ dự án (mà không cần import lại bất kỳ module nào ở nơi khác).

// Import mọi thứ từ package driver của bạn hoặc `@mikro-orm/knex`
import { EntityManager, MikroORM } from '@mikro-orm/sqlite';

@Injectable()
export class MyService {
  constructor(
    private readonly orm: MikroORM,
    private readonly em: EntityManager,
  ) {}
}

Info Lưu ý rằng EntityManager được import từ package @mikro-orm/driver, trong đó driver có thể là mysql, sqlite, postgres hoặc driver bạn đang dùng. Trong trường hợp bạn đã cài @mikro-orm/knex như một dependency, bạn cũng có thể import EntityManager từ đó.

Repository

MikroORM hỗ trợ repository design pattern. Với mỗi entity, chúng ta có thể tạo một repository. Đọc tài liệu đầy đủ về repository tại đây. Để định nghĩa những repository nào nên được đăng ký trong scope hiện tại, bạn có thể dùng method forFeature(). Ví dụ, theo cách sau:

Info Bạn không nên đăng ký các base entity của mình qua forFeature(), vì không có repository nào cho chúng cả. Mặt khác, base entity cần phải nằm trong danh sách ở forRoot() (hoặc trong ORM config nói chung).

// photo.module.ts
import { MikroOrmModule } from '@mikro-orm/nestjs';

@Module({
  imports: [MikroOrmModule.forFeature([Photo])],
  providers: [PhotoService],
  controllers: [PhotoController],
})
export class PhotoModule {}

và import nó vào AppModule gốc:

// app.module.ts
import { MikroOrmModule } from '@mikro-orm/nestjs';

@Module({
  imports: [MikroOrmModule.forRoot(...), PhotoModule],
})
export class AppModule {}

Theo cách này, chúng ta có thể inject PhotoRepository vào PhotoService bằng decorator @InjectRepository():

import { EntityRepository } from '@mikro-orm/sqlite'; // import từ package driver của bạn

@Injectable()
export class PhotoService {
  constructor(
    @InjectRepository(Photo)
    private readonly photoRepository: EntityRepository<Photo>,
  ) {}
}

Dùng custom repository

Khi dùng custom repository, chúng ta không còn cần decorator @InjectRepository() nữa, vì Nest DI đã resolve dựa trên class reference.

// `**./author.entity.ts**`
import { Entity, EntityRepositoryType } from '@mikro-orm/core';

@Entity({ repository: () => AuthorRepository })
export class Author {
  // để cho phép inference trong `em.getRepository()`
  [EntityRepositoryType]?: AuthorRepository;
}

// `**./author.repository.ts**`
import { EntityRepository } from '@mikro-orm/sqlite'; // import từ package driver của bạn

export class AuthorRepository extends EntityRepository<Author> {
  // các method tùy chỉnh của bạn...
}

Vì tên của custom repository giống hệt với những gì getRepositoryToken() sẽ trả về, chúng ta không cần decorator @InjectRepository() nữa:

@Injectable()
export class MyService {
  constructor(private readonly repo: AuthorRepository) {}
}

Tự động load entity

Việc thêm entity thủ công vào mảng entities trong tùy chọn kết nối có thể khá tốn công. Thêm vào đó, việc tham chiếu entity từ module gốc phá vỡ ranh giới domain của ứng dụng và khiến chi tiết triển khai bị rò rỉ sang các phần khác của ứng dụng. Để giải quyết vấn đề này, ta có thể dùng static glob path.

Tuy nhiên, cần lưu ý rằng glob path không được webpack hỗ trợ, vì vậy nếu bạn build ứng dụng trong một monorepo, bạn sẽ không thể dùng chúng. Để giải quyết vấn đề này, có một giải pháp thay thế được cung cấp. Để tự động load entity, hãy đặt property autoLoadEntities trong object cấu hình (truyền vào method forRoot()) thành true, như bên dưới:

import { MikroOrmModule } from '@mikro-orm/nestjs';

@Module({
  imports: [
    MikroOrmModule.forRoot({
      ...
      autoLoadEntities: true,
    }),
  ],
})
export class AppModule {}

Với tùy chọn đó được chỉ định, mọi entity đăng ký qua method forFeature() sẽ tự động được thêm vào mảng entities của object cấu hình.

Info Lưu ý rằng những entity không được đăng ký qua method forFeature(), mà chỉ được tham chiếu từ entity khác (qua một relationship), sẽ không được bao gồm bởi tùy chọn autoLoadEntities.

Info Việc dùng autoLoadEntities cũng không ảnh hưởng gì đến MikroORM CLI - vì CLI vẫn cần config với danh sách đầy đủ các entity. Mặt khác, chúng ta có thể dùng glob ở đó, vì CLI không đi qua webpack.

Serialization

Lưu ý MikroORM bọc mọi entity relation trong một object Reference<T> hoặc Collection<T>, nhằm cung cấp type-safety tốt hơn. Điều này khiến serializer tích hợp sẵn của Nest không “nhìn thấy” bất kỳ relation nào bị bọc. Nói cách khác, nếu bạn trả về entity MikroORM từ handler HTTP hoặc WebSocket, tất cả relation của chúng sẽ KHÔNG được serialize.

May mắn là, MikroORM cung cấp một serialization API có thể dùng thay cho ClassSerializerInterceptor.

import { Entity, Property, ManyToOne } from '@mikro-orm/core';

@Entity()
export class Book {
  @Property({ hidden: true }) // Tương đương với `@Exclude` của class-transformer
  hiddenField = Date.now();

  @Property({ persist: false }) // Tương tự `@Expose()` của class-transformer. Chỉ tồn tại trong bộ nhớ, và sẽ được serialize.
  count?: number;

  @ManyToOne({
    serializer: (value) => value.name,
    serializedName: 'authorName',
  }) // Tương đương với `@Transform()` của class-transformer
  author: Author;
}

Hàm xử lý theo scope request trong queue

Như đã đề cập trong tài liệu, chúng ta cần một state “sạch” cho mỗi request. Điều này được xử lý tự động nhờ helper RequestContext được đăng ký qua middleware.

Nhưng middleware chỉ được thực thi cho các request handler HTTP thông thường, vậy nếu chúng ta cần một method có scope request ở ngoài phạm vi đó thì sao? Một ví dụ là handler của queue hoặc scheduled task.

Chúng ta có thể dùng decorator @CreateRequestContext(). Nó yêu cầu bạn trước tiên phải inject instance MikroORM vào context hiện tại, instance này sau đó sẽ được dùng để tạo context giúp bạn. Bên dưới, decorator sẽ đăng ký một request context mới cho method của bạn và thực thi nó bên trong context đó.

import { MikroORM, CreateRequestContext } from '@mikro-orm/core';

@Injectable()
export class MyService {
  constructor(private readonly orm: MikroORM) {}

  @CreateRequestContext()
  async doSomething() {
    // đoạn này sẽ được thực thi trong một context riêng biệt
  }
}

Lưu ý Đúng như tên gọi, decorator này luôn tạo context mới, khác với decorator thay thế @EnsureRequestContext - decorator này chỉ tạo context nếu chưa nằm trong một context nào khác.

Testing

Package @mikro-orm/nestjs cung cấp hàm getRepositoryToken() trả về một token đã được chuẩn bị sẵn dựa trên entity cho trước, cho phép bạn mock repository.

import { MikroOrmModule, getRepositoryToken } from '@mikro-orm/nestjs';

@Module({
  providers: [
    PhotoService,
    {
      // hoặc khi bạn có custom repository: `provide: PhotoRepository`
      provide: getRepositoryToken(Photo),
      useValue: mockedRepository,
    },
  ],
})
export class PhotoModule {}

Ví dụ

Một ví dụ thực tế về NestJS kết hợp với MikroORM có thể tìm thấy tại đây

Ghi chú người dịch Nếu bạn đã quen TypeORM, khác biệt lớn nhất ở đây là pattern Unit of Work: EntityManager của MikroORM theo dõi mọi entity đã load/thay đổi trong một “phiên” (giống session trong Hibernate ở Java, hoặc unit of work trong Entity Framework .NET) và chỉ đẩy các thay đổi xuống DB khi bạn gọi em.flush(), thay vì mỗi lần save() là một query riêng lẻ như TypeORM.

Ghi chú người dịch Phần RequestContext/@CreateRequestContext() giải quyết một vấn đề khá ngầm: vì EntityManager mang state theo từng request (giống connection theo request trong nhiều framework khác), nếu bạn dùng chung một instance EntityManager toàn cục giữa các request đồng thời, dữ liệu request này có thể “rò” sang request khác. Middleware tự tạo context sạch cho mỗi HTTP request, nhưng job chạy nền (cron, queue) không đi qua middleware nên phải tự tạo context bằng tay.

Ghi chú người dịch Lỗi hay gặp khi vibe-code: quên rằng entity trả thẳng ra JSON sẽ không tự ẩn field nhạy cảm (password, token…) như khi dùng class-transformer + ClassSerializerInterceptor ở chỗ khác trong app - MikroORM cần dùng cơ chế serialization riêng (hidden: true) như ví dụ ở trên.

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.