Skip to main content

Providers trong NestJS

Provider là một khái niệm cơ bản trong NestJS. Hầu hết các classes trong NestJS đều có thể được coi là provider - services, repositories, factories, helpers, v.v. Ý tưởng chính của provider là nó có thể inject dependencies vào các classes khác.

Khái niệm Provider​

Provider là một class được NestJS quản lý thông qua Dependency Injection Container. Khi bạn khai báo một provider trong module, NestJS sẽ:

  1. Tạo instance của class đó
  2. Quản lý vòng đời của instance
  3. Inject vào các classes khác khi cần
import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
getUsers() {
return ['Alice', 'Bob'];
}
}

Decorator @Injectable() đánh dấu class này là một provider.

Cách Khai Báo Providers​

1. Value Provider (useValue)​

Cung cấp một giá trị tĩnh:

const mockUsersService = {
getUsers: () => ['Mock User 1', 'Mock User 2'],
};

@Module({
providers: [
{
provide: UsersService,
useValue: mockUsersService,
},
],
})
export class AppModule {}

Khi nào sử dụng:

  • Testing với mock data
  • Cung cấp configuration constants
  • Các giá trị tĩnh không cần initialization

2. Class Provider (useClass)​

Cung cấp một class khác thay vì class gốc:

class BaseUsersService {
getUsers() {
return [];
}
}

class AdvancedUsersService extends BaseUsersService {
getUsers() {
return ['Alice', 'Bob', 'Charlie'];
}
}

@Module({
providers: [
{
provide: UsersService,
useClass: AdvancedUsersService,
},
],
})
export class AppModule {}

Khi nào sử dụng:

  • Strategy pattern - swap implementations
  • Environment-based providers (dev/prod)
  • Testing với mock classes

3. Factory Provider (useFactory)​

Tạo instance thông qua một factory function:

@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: () => {
const connection = new DatabaseConnection({
host: 'localhost',
port: 5432,
});
connection.connect();
return connection;
},
},
],
})
export class DatabaseModule {}

Với dependencies:

@Module({
providers: [
ConfigService,
{
provide: 'DATABASE_CONNECTION',
useFactory: (configService: ConfigService) => {
const connection = new DatabaseConnection(configService.getDbConfig());
connection.connect();
return connection;
},
inject: [ConfigService], // Inject ConfigService vào factory
},
],
})
export class DatabaseModule {}

Khi nào sử dụng:

  • Initialization phức tạp
  • Tạo instance dựa trên configuration
  • Dependencies động

4. Alias Provider (useExisting)​

Tạo alias cho một provider khác:

@Module({
providers: [
{
provide: 'USERS_SERVICE',
useExisting: UsersService,
},
],
})
export class UsersModule {}

Khi nào sử dụng:

  • Cung cấp nhiều names cho cùng một service
  • Backward compatibility

Scopes (Phạm vi) của Providers​

1. Singleton (DEFAULT)​

Một instance duy nhất cho toàn bộ ứng dụng:

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

@Injectable({ scope: Scope.SINGLETON })
export class UsersService {
private users = [];

addUser(user: string) {
this.users.push(user);
}

getUsers() {
return this.users;
}
}

Đặc điểm:

  • Instance được tạo một lần
  • Chia sẻ giữa toàn bộ ứng dụng
  • Tốt hơn về performance
  • Dùng cho stateless services

2. Request​

Một instance mới cho mỗi request:

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

@Injectable({ scope: Scope.REQUEST })
export class RequestService {
private requestId = Math.random();

getRequestId() {
return this.requestId;
}
}

Đặc điểm:

  • Instance mới cho mỗi HTTP request
  • Có thể lưu trữ dữ liệu request-specific
  • Tốn tài nguyên hơn singleton
  • Dùng cho services cần isolation giữa requests

3. Transient​

Một instance mới được tạo mỗi khi được inject:

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

@Injectable({ scope: Scope.TRANSIENT })
export class TransientService {
private id = Math.random();

getId() {
return this.id;
}
}

Đặc điểm:

  • Instance mới mỗi lần inject
  • Nhiều instances trong một request
  • Tốn tài nguyên nhất
  • Dùng hiếm khi

So sánh scopes:

Request #1:
├── UsersService (SINGLETON) - Instance A
├── RequestService (REQUEST) - Instance B
└── TransientService (TRANSIENT) - Instance C, D, E (new mỗi inject)

Request #2:
├── UsersService (SINGLETON) - Instance A (cùng)
├── RequestService (REQUEST) - Instance F (mới)
└── TransientService (TRANSIENT) - Instance G, H, I (mới)

Dependency Injection (DI)​

Constructor Injection​

Cách phổ biến nhất - inject qua constructor:

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
constructor(
private databaseService: DatabaseService,
private mailService: MailService,
) {}

createUser(userData: any) {
// Sử dụng injected dependencies
const user = this.databaseService.save(userData);
this.mailService.sendWelcomeEmail(user.email);
return user;
}
}

Property Injection​

Inject trực tiếp vào property (ít dùng):

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

@Injectable()
export class UsersService {
@Inject('DATABASE_SERVICE')
private databaseService: DatabaseService;

getUsers() {
return this.databaseService.find();
}
}

Method Injection​

Inject vào method parameters (cực hiếm):

export class UsersService {
getUsers(databaseService: DatabaseService) {
return databaseService.find();
}
}

Ví dụ Thực Tế​

Service với Multiple Dependencies​

// users.service.ts
import { Injectable } from '@nestjs/common';
import { DatabaseService } from '../database/database.service';
import { MailService } from '../mail/mail.service';
import { LoggerService } from '../logger/logger.service';

@Injectable()
export class UsersService {
constructor(
private db: DatabaseService,
private mail: MailService,
private logger: LoggerService,
) {}

async createUser(createUserDto: CreateUserDto) {
this.logger.log(`Creating user: ${createUserDto.email}`);

try {
const user = await this.db.users.create(createUserDto);
await this.mail.sendWelcomeEmail(user.email);
this.logger.log(`User created successfully: ${user.id}`);
return user;
} catch (error) {
this.logger.error(`Failed to create user: ${error.message}`);
throw error;
}
}

async getUser(id: string) {
this.logger.log(`Fetching user: ${id}`);
return this.db.users.findById(id);
}
}

Factory Provider với Configuration​

// config.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}

// config.service.ts
@Injectable()
export class ConfigService {
private config = {
db: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
user: process.env.DB_USER || 'postgres',
},
mail: {
service: process.env.MAIL_SERVICE || 'gmail',
from: process.env.MAIL_FROM,
},
};

get(key: string) {
return this.config[key];
}
}

// database.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from '../config/config.service';

@Module({
providers: [
{
provide: 'DATABASE_CONNECTION',
useFactory: (configService: ConfigService) => {
const dbConfig = configService.get('db');
return new Database(dbConfig).connect();
},
inject: [ConfigService],
},
],
exports: ['DATABASE_CONNECTION'],
})
export class DatabaseModule {}

Custom Provider Token​

// Dùng string token
@Module({
providers: [
{
provide: 'USERS_REPOSITORY',
useClass: UsersRepository,
},
],
})
export class UsersModule {}

// Inject bằng @Inject decorator
@Injectable()
export class UsersService {
constructor(
@Inject('USERS_REPOSITORY')
private usersRepository: UsersRepository,
) {}
}

Conditional Providers​

@Module({
providers: [
{
provide: DatabaseService,
useClass: process.env.NODE_ENV === 'production'
? PostgresDatabaseService
: MockDatabaseService,
},
],
})
export class AppModule {}

Optional Dependencies​

Đánh dấu dependency là optional:

import { Inject, Optional } from '@nestjs/common';

@Injectable()
export class UsersService {
constructor(
@Optional()
@Inject('CACHE_SERVICE')
private cacheService?: CacheService,
) {}

getUser(id: string) {
// Kiểm tra cache nếu có
if (this.cacheService) {
const cached = this.cacheService.get(`user:${id}`);
if (cached) return cached;
}

// Lấy từ database
const user = this.db.users.findById(id);

// Lưu vào cache nếu có
if (this.cacheService) {
this.cacheService.set(`user:${id}`, user);
}

return user;
}
}

Best Practices​

1. Một Responsibility (Single Responsibility)​

Mỗi provider chỉ nên có một mục đích duy nhất:

// ❌ Sai - Quá nhiều trách nhiệm
@Injectable()
export class UsersService {
createUser() { /* ... */ }
sendEmail() { /* ... */ }
logActivity() { /* ... */ }
validateData() { /* ... */ }
}

// ✅ Đúng - Tách biệt trách nhiệm
@Injectable()
export class UsersService {
constructor(
private db: DatabaseService,
private mail: MailService,
private logger: LoggerService,
private validator: ValidatorService,
) {}

createUser(data: CreateUserDto) {
this.validator.validate(data);
const user = this.db.users.create(data);
this.logger.log(`User created: ${user.id}`);
this.mail.sendWelcomeEmail(user.email);
return user;
}
}

2. Inject Interfaces, không Classes​

Giúp testing và flexibility:

// ❌ Sai - Inject cụ thể class
constructor(private db: PostgresDatabaseService) {}

// ✅ Đúng - Inject interface
interface IDatabase {
users: any;
}

constructor(@Inject('DATABASE') private db: IDatabase) {}

3. Sử dụng Appropriate Scopes​

Chọn scope phù hợp:

// ✅ Stateless service - SINGLETON
@Injectable({ scope: Scope.SINGLETON })
export class UsersService {}

// ✅ Request-specific data - REQUEST
@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {}

// ❌ Tránh dùng TRANSIENT nếu không cần
@Injectable({ scope: Scope.TRANSIENT })
export class UtilService {}

4. Tránh Circular Dependencies​

Sử dụng lazy loading hoặc shared modules:

// ❌ Sai - Circular
// UsersService inject OrdersService
// OrdersService inject UsersService

// ✅ Đúng - Tạo SharedService
@Injectable()
export class SharedService {
// Common logic
}

@Injectable()
export class UsersService {
constructor(private shared: SharedService) {}
}

@Injectable()
export class OrdersService {
constructor(private shared: SharedService) {}
}

Testing Providers​

Mock providers để test dễ dàng:

describe('UsersService', () => {
let service: UsersService;
let mockDatabaseService: DatabaseService;

beforeEach(async () => {
mockDatabaseService = {
users: {
create: jest.fn().mockResolvedValue({ id: 1, name: 'Test' }),
findById: jest.fn().mockResolvedValue({ id: 1, name: 'Test' }),
},
} as any;

const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: DatabaseService,
useValue: mockDatabaseService,
},
],
}).compile();

service = module.get<UsersService>(UsersService);
});

it('should create user', async () => {
const result = await service.createUser({ name: 'Test' });
expect(result.id).toBe(1);
});
});

Kết Luận​

Providers là nền tảng của Dependency Injection trong NestJS. Hiểu rõ về providers giúp bạn:

  • Tổ chức code logic và dễ bảo trì
  • Viết unit tests dễ dàng hơn
  • Quản lý dependencies một cách rõ ràng
  • Xây dựng ứng dụng linh hoạt và mở rộng
  • Áp dụng các design patterns như Factory, Strategy, Singleton

Sử dụng providers đúng cách là chìa khóa để xây dựng các ứng dụng NestJS chất lượng cao.