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.