☰ 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ữ
Kỹ thuật (Techniques)

Caching

Bản dịch từ docs.nestjs.com/techniques/caching

Caching là một kỹ thuật mạnh mẽ và đơn giản để nâng cao hiệu năng ứng dụng của bạn. Bằng cách đóng vai trò như một lớp lưu trữ tạm thời, nó cho phép truy cập nhanh hơn tới dữ liệu được dùng thường xuyên, giảm nhu cầu phải fetch hoặc tính toán lại cùng một thông tin nhiều lần. Điều này giúp thời gian phản hồi nhanh hơn và cải thiện hiệu quả tổng thể.

Cài đặt

Để bắt đầu dùng caching trong Nest, bạn cần cài package @nestjs/cache-manager cùng với package cache-manager.

$ npm install @nestjs/cache-manager cache-manager

Mặc định, mọi thứ được lưu trong memory (bộ nhớ); vì cache-manager dùng Keyv bên dưới, bạn có thể dễ dàng chuyển sang một giải pháp lưu trữ nâng cao hơn, chẳng hạn Redis, bằng cách cài package phù hợp. Chúng ta sẽ nói kỹ hơn về điều này ở phần sau.

Cache trong memory (In-memory cache)

Để bật caching trong ứng dụng của bạn, import CacheModule và cấu hình nó bằng phương thức register():

import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { AppController } from './app.controller.js';

@Module({
  imports: [CacheModule.register()],
  controllers: [AppController],
})
export class AppModule {}

Cấu hình này khởi tạo caching trong memory với các thiết lập mặc định, cho phép bạn bắt đầu cache dữ liệu ngay lập tức.

Tương tác với cache store

Để tương tác với instance cache manager, inject nó vào class của bạn bằng token CACHE_MANAGER, như sau:

constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

Gợi ý Class Cache và token CACHE_MANAGER đều được import từ package @nestjs/cache-manager.

Phương thức get trên instance Cache (từ package cache-manager) được dùng để lấy các item từ cache. Nếu item không tồn tại trong cache, undefined sẽ được trả về (trong cache-manager v6 trở về trước, null được trả về thay vào đó). Hãy coi cả hai đều là falsy khi migrate.

const value = await this.cacheManager.get('key');

Để thêm một item vào cache, dùng phương thức set:

await this.cacheManager.set('key', 'value');

Lưu ý Bộ lưu trữ cache trong memory chỉ có thể lưu các giá trị thuộc kiểu được hỗ trợ bởi structured clone algorithm.

Ghi chú người dịch Structured clone algorithm là cơ chế “deep clone” chuẩn của JS engine (dùng bởi postMessage, structuredClone()), nó không clone được function, class instance có method, hay Symbol. Nếu bạn quen lưu cache bằng Redis từ PHP/Go rồi serialize JSON thủ công, điểm khác biệt ở đây là store in-memory của Nest bỏ qua bước serialize/deserialize — nên nó nhanh hơn nhưng cũng kén kiểu dữ liệu hơn JSON.

Bạn có thể chỉ định thủ công một TTL (thời gian hết hạn, tính bằng mili-giây) cho key cụ thể này, như sau:

await this.cacheManager.set('key', 'value', 1000);

Trong đó 1000 là TTL tính bằng mili-giây - trong trường hợp này, cache item sẽ hết hạn sau một giây.

Để tắt việc hết hạn của cache, đặt thuộc tính cấu hình ttl thành 0:

await this.cacheManager.set('key', 'value', 0);

Để xóa một item khỏi cache, dùng phương thức del:

await this.cacheManager.del('key');

Để xóa toàn bộ cache, dùng phương thức clear:

await this.cacheManager.clear();

Tự động cache response (Auto-caching responses)

Cảnh báo Trong các ứng dụng GraphQL, interceptor được thực thi riêng biệt cho từng field resolver. Do đó, CacheModule (vốn dùng interceptor để cache response) sẽ không hoạt động đúng.

Để bật tự động cache response, chỉ cần gắn (bind) CacheInterceptor vào nơi bạn muốn cache dữ liệu.

@Controller()
@UseInterceptors(CacheInterceptor)
export class AppController {
  @Get()
  findAll(): string[] {
    return [];
  }
}

warningCảnh báo Chỉ các endpoint GET mới được cache. Ngoài ra, các route của HTTP server mà inject native response object (@Res()) không thể dùng Cache Interceptor. Xem response mapping để biết thêm chi tiết.

Để giảm lượng boilerplate cần thiết, bạn có thể gắn CacheInterceptor cho toàn bộ endpoint một cách global:

import { Module } from '@nestjs/common';
import { CacheModule, CacheInterceptor } from '@nestjs/cache-manager';
import { AppController } from './app.controller.js';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  imports: [CacheModule.register()],
  controllers: [AppController],
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: CacheInterceptor,
    },
  ],
})
export class AppModule {}

Ghi chú người dịch Cách APP_INTERCEPTOR hoạt động ở đây tương đương việc bạn đăng ký một middleware global trong Express (app.use(cacheMiddleware)) hoặc một global middleware trong Laravel (app/Http/Kernel.php) — điểm khác là interceptor của Nest chạy quanh cả pipeline (trước lẫn sau khi handler xử lý), còn middleware chỉ chạy trước. Vì đăng ký qua DI token, CacheInterceptor này còn có thể inject phụ thuộc khác nếu bạn subclass nó.

Thời gian sống (Time-to-live - TTL)

Giá trị mặc định cho ttl là 0, nghĩa là cache sẽ không bao giờ hết hạn. Để chỉ định một TTL tùy chỉnh, bạn có thể cung cấp tùy chọn ttl trong phương thức register(), như minh họa dưới đây:

CacheModule.register({
  ttl: 5000, // milliseconds
});

Dùng module ở phạm vi global (Use module globally)

Khi bạn muốn dùng CacheModule trong các module khác, bạn cần import nó (như thông thường với bất kỳ module Nest nào). Ngoài ra, bạn có thể khai báo nó như một global module bằng cách đặt thuộc tính isGlobal của object options thành true, như bên dưới. Trong trường hợp đó, bạn sẽ không cần import CacheModule ở các module khác một khi nó đã được load ở module gốc (ví dụ, AppModule).

CacheModule.register({
  isGlobal: true,
});

Ghi đè cache global (Global cache overrides)

Trong khi cache global được bật, các cache entry được lưu dưới một CacheKey được tự động sinh ra dựa trên route path. Bạn có thể ghi đè một số thiết lập cache (@CacheKey() và @CacheTTL()) theo từng phương thức, cho phép các chiến lược caching tùy biến cho từng phương thức controller. Điều này có thể phù hợp nhất khi dùng các cache store khác nhau.

Bạn có thể áp dụng decorator @CacheTTL() theo từng controller để đặt TTL caching cho cả controller. Trong trường hợp cả thiết lập TTL cấp controller lẫn cấp phương thức đều được định nghĩa, thiết lập TTL cache ở cấp phương thức sẽ được ưu tiên hơn thiết lập ở cấp controller.

@Controller()
@CacheTTL(5000)
export class AppController {
  @CacheKey('custom_key')
  @CacheTTL(2000)
  findAll(): string[] {
    return [];
  }
}

Gợi ý Decorator @CacheKey() và @CacheTTL() được import từ package @nestjs/cache-manager.

Decorator @CacheKey() có thể được dùng kèm hoặc không kèm decorator @CacheTTL() tương ứng, và ngược lại. Bạn có thể chọn chỉ ghi đè @CacheKey() hoặc chỉ @CacheTTL(). Các thiết lập không được ghi đè bằng decorator sẽ dùng giá trị mặc định đã đăng ký ở cấp global (xem Tùy biến caching).

WebSockets và Microservices

Bạn cũng có thể áp dụng CacheInterceptor cho cả WebSocket subscriber lẫn pattern của Microservice (bất kể phương thức transport nào đang được dùng).

@CacheKey('events')
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client: Client, data: string[]): Observable<string[]> {
  return [];
}
@CacheKey('events')
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client, data) {
  return [];
}

Tuy nhiên, decorator @CacheKey() bổ sung là bắt buộc để chỉ định một key dùng để lưu và lấy lại dữ liệu đã cache sau đó. Ngoài ra, xin lưu ý rằng bạn không nên cache mọi thứ. Các hành động thực hiện một số thao tác nghiệp vụ thay vì chỉ đơn thuần truy vấn dữ liệu thì không bao giờ nên được cache.

Thêm vào đó, bạn có thể chỉ định thời gian hết hạn cache (TTL) bằng decorator @CacheTTL(), decorator này sẽ ghi đè giá trị TTL mặc định ở cấp global.

@CacheTTL(1000)
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client: Client, data: string[]): Observable<string[]> {
  return [];
}
@CacheTTL(1000)
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client, data) {
  return [];
}

Gợi ý Decorator @CacheTTL() có thể được dùng kèm hoặc không kèm decorator @CacheKey() tương ứng.

Điều chỉnh cách tracking (Adjust tracking)

Mặc định, Nest dùng request URL (trong ứng dụng HTTP) hoặc cache key (trong ứng dụng websocket và microservice, đặt qua decorator @CacheKey()) để gắn kết các cache record với endpoint của bạn. Tuy nhiên, đôi khi bạn có thể muốn thiết lập việc tracking dựa trên các yếu tố khác, ví dụ dùng HTTP header (chẳng hạn Authorization để nhận diện đúng các endpoint profile).

Để làm được điều đó, tạo một subclass của CacheInterceptor và ghi đè phương thức trackBy().

@Injectable()
class HttpCacheInterceptor extends CacheInterceptor {
  trackBy(context: ExecutionContext): string | undefined {
    return 'key';
  }
}

Ghi chú người dịch trackBy() chính là chỗ bạn tự quyết định “khóa cache” (cache key) được sinh ra như thế nào cho mỗi request — mặc định dựa trên URL, nhưng nếu hai user khác nhau gọi cùng một URL /profile mà bạn không override trackBy() để tách theo token/Authorization header, bạn sẽ vô tình trả cache của user A cho user B. Đây là một cạm bẫy bảo mật thực tế hay gặp khi bật global cache mà quên tùy biến tracking cho các endpoint có dữ liệu theo user.

Dùng cache store thay thế (Using alternative Cache stores)

Chuyển sang một cache store khác khá đơn giản. Đầu tiên, cài package phù hợp. Ví dụ, để dùng Redis, cài package @keyv/redis:

$ npm install @keyv/redis

Sau khi có package này, bạn có thể đăng ký CacheModule với nhiều store như bên dưới:

import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { AppController } from './app.controller.js';
import { createKeyv } from '@keyv/redis';
import { Keyv } from 'keyv';
import { KeyvCacheableMemory } from 'cacheable';

@Module({
  imports: [
    CacheModule.registerAsync({
      useFactory: async () => {
        return {
          stores: [
            new Keyv({
              store: new KeyvCacheableMemory({ ttl: 60000, lruSize: 5000 }),
            }),
            createKeyv('redis://localhost:6379'),
          ],
        };
      },
    }),
  ],
  controllers: [AppController],
})
export class AppModule {}

Trong ví dụ này, chúng ta đã đăng ký hai store: CacheableMemory và một Redis store. Store CacheableMemory là một store in-memory đơn giản, được tạo qua storage adapter KeyvCacheableMemory, còn Redis store được tạo qua helper createKeyv từ @keyv/redis. Mảng stores được dùng để chỉ định các store bạn muốn dùng. Store đầu tiên trong mảng là store mặc định, các store còn lại là store dự phòng (fallback).

Xem tài liệu Keyv để biết thêm thông tin về các store có sẵn.

Cấu hình bất đồng bộ (Async configuration)

Bạn có thể muốn truyền các option của module theo cách bất đồng bộ thay vì truyền tĩnh tại compile time. Trong trường hợp này, dùng phương thức registerAsync(), phương thức này cung cấp nhiều cách để xử lý cấu hình bất đồng bộ.

Một cách tiếp cận là dùng factory function:

CacheModule.registerAsync({
  useFactory: () => ({
    ttl: 5000,
  }),
});

Factory của chúng ta hoạt động giống như mọi factory module bất đồng bộ khác (nó có thể là async và có thể inject các phụ thuộc thông qua inject).

CacheModule.registerAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    ttl: configService.get('CACHE_TTL'),
  }),
  inject: [ConfigService],
});

Ngoài ra, bạn có thể dùng cách useClass:

CacheModule.registerAsync({
  useClass: CacheConfigService,
});

Cấu trúc trên sẽ khởi tạo CacheConfigService bên trong CacheModule và dùng nó để lấy object options. CacheConfigService phải implement interface CacheOptionsFactory để cung cấp các option cấu hình:

@Injectable()
class CacheConfigService implements CacheOptionsFactory {
  createCacheOptions(): CacheModuleOptions {
    return {
      ttl: 5000,
    };
  }
}

Nếu bạn muốn dùng một provider cấu hình đã có sẵn, được import từ module khác, dùng cú pháp useExisting:

CacheModule.registerAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
});

Cách này hoạt động giống useClass nhưng có một khác biệt quan trọng - CacheModule sẽ tìm trong các module đã import để tái sử dụng bất kỳ ConfigService nào đã được tạo, thay vì tự khởi tạo instance riêng của nó.

Gợi ý CacheModule#register, CacheModule#registerAsync và CacheOptionsFactory có một generic tùy chọn (type argument) để thu hẹp các option cấu hình đặc thù cho từng store, giúp nó an toàn về kiểu (type safe).

Bạn cũng có thể truyền cái gọi là extraProviders cho phương thức registerAsync(). Các provider này sẽ được gộp chung với các provider của module.

CacheModule.registerAsync({
  imports: [ConfigModule],
  useClass: ConfigService,
  extraProviders: [MyAdditionalProvider],
});

Điều này hữu ích khi bạn muốn cung cấp thêm các phụ thuộc cho factory function hoặc constructor của class.

Ví dụ

Một ví dụ đầy đủ có sẵn tại đây.

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.