Chào mừng các bạn quay lại với series "Mỗi ngày một chút với NestJs", bài hôm nay chúng ta cùng đi tìm hiểu Scope trong NestJs.

Trong NestJS, Scope là cách bạn kiểm soát vòng đời của các provider. Khi dự án của bạn càng lớn, việc kiểm soát vòng đời (lifecycle) của các service càng trở nên quan trọng. Trong NestJS, bạn có thể kiểm soát điều này bằng cách khai báo Scope cho từng provider. Việc hiểu đúng scope sẽ giúp:

  • Tránh leak dữ liệu giữa người dùng.

  • Tối ưu hiệu năng.

  • Dễ dàng debug và mở rộng hệ thống.

Việc hiểu rõ scope giúp bạn tránh những lỗi khó lường trong hệ thống lớn: như chia sẻ sai instance, rò rỉ trạng thái, hoặc sử dụng sai context (ví dụ như trong xử lý HTTP request, WebSocket, v.v.).

NestJS cung cấp 3 scope chính:

Scope

Tạo Instance khi nào?

Áp dụng khi nào?

SINGLETON

Khi app khởi động

Dùng chung toàn hệ thống

REQUEST

Mỗi HTTP request mới

Cần dữ liệu gắn với từng request

TRANSIENT

Mỗi lần được inject

Cần nhiều bản sao độc lập của provider

1. SINGLETON – Mặc định, chỉ tạo 1 instance duy nhất

Đặc điểm:

  • Chỉ tạo một instance duy nhất cho toàn bộ ứng dụng.

  • Dùng chung ở mọi nơi, inject ở đâu cũng là cùng một object.

Cách hoạt động:

Nest chỉ tạo 1 instance của provider khi ứng dụng khởi động, và dùng lại trong toàn bộ lifecycle.

@Injectable()
export class ConfigService {
  getDatabaseUrl() {
    return process.env.DATABASE_URL;
  }
}

Inject vào nhiều nơi:

@Injectable()
export class UserService {
  constructor(private readonly configService: ConfigService) {}
}

Khi nào dùng:

  • Service chứa config

  • Logging

  • Database connection (singleton DB client)

  • Email sender (1 pool duy nhất)

2. REQUEST – Tạo mới mỗi request

Đặc điểm:

  • Tạo mỗi khi có request mới.

  • Phù hợp khi bạn cần thông tin gắn với request (ví dụ: currentUser, ip, headers, language,…)

Ví dụ:

import { Scope, Injectable, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';

@Injectable({ scope: Scope.REQUEST })
export class CurrentUserService {
  constructor(@Inject(REQUEST) private readonly request: Request) {}

  getUser() {
    return this.request.user;
  }
}

Khi service A dùng REQUEST scope → các service phụ thuộc vào nó cũng sẽ bị buộc chuyển sang REQUEST scope theo “scope cascade”.

Khi nào dùng?

  • Service chứa thông tin người dùng hiện tại

  • Truy cập request.body, request.headers, request.ip

  • Xác định locale (ngôn ngữ, múi giờ theo request)

3. TRANSIENT – Mỗi lần inject tạo mới

Đặc điểm:

  • Mỗi lần injectmột instance hoàn toàn mới.

  • Tách biệt 100% giữa các nơi sử dụng, không chia sẻ state.

Cách dùng:

@Injectable({ scope: Scope.TRANSIENT })
export class UUIDGenerator {
  private readonly id = Math.random().toString(36).slice(2);

  getId() {
    return this.id;
  }
}
@Injectable()
export class AService {
  constructor(private readonly uuid: UUIDGenerator) {
    console.log('A:', uuid.getId());
  }
}

@Injectable()
export class BService {
  constructor(private readonly uuid: UUIDGenerator) {
    console.log('B:', uuid.getId());
  }
}

Khi nào dùng:

  • Dịch vụ dùng trong batch hoặc xử lý song song

  • Tạo mới logic phụ thuộc tham số đầu vào

  • Sử dụng trong các factory pattern

Tổng kết

Tiêu chí

SINGLETON

REQUEST

TRANSIENT

Vòng đời

Toàn app

Mỗi request

Mỗi lần inject

Hiệu năng

Tốt nhất

Trung bình

Thấp nhất

Dùng khi nào

Cấu hình, DB

Request data

Factory, xử lý độc lập

Chia sẻ state

Không

Không

 

Kết luận

Dùng SINGLETON nếu không có lý do đặc biệt.

Dùng REQUEST nếu bạn cần data thay đổi theo từng người dùng.

Dùng TRANSIENT nếu bạn cần sự độc lập hoàn toàn cho mỗi lần xử lý.

 

Đồng Nguyễn

Software Engineer