Các Vấn đề NestJS Thường gặp và Cách Khắc phục
Xây dựng ứng dụng production với NestJS rất mạnh mẽ, nhưng hệ thống dependency injection, kiến trúc dựa trên decorator, và tích hợp TypeScript của framework cũng có thể tạo ra các lỗi tinh vi, khó debug. Tài liệu này tổng hợp các vấn đề NestJS trong thực tế từ GitHub issues, Stack Overflow và kinh nghiệm vận hành production.
Chúng ta sẽ đi qua:
- Lỗi dependency injection
- Bẫy cấu hình module
- Các tình huống dễ nhầm trong request lifecycle
- Lỗi tích hợp database
- Lỗi authentication/guard
- Khó khăn khi testing
- Vấn đề hiệu năng production
Mỗi vấn đề đều có: cách phát hiện, nguyên nhân, và cách xử lý.
1. Dependency Injection Issues
1.1 "Nest can't resolve dependencies of the [Service] (?)"
- Tần suất: Cao nhất (500+ issue được report)
Đây là lỗi phổ biến nhất trong NestJS. Ký hiệu (?) thường ám chỉ thiếu dependency.
Ví dụ lỗi
Error: Nest can't resolve dependencies of the UserService (?).
Please make sure that the argument at index [0] is available in the UserModule context.Nguyên nhân thường gặp
- Provider không nằm trong
providerscủa module
// ❌ SAI
@Module({
controllers: [UserController],
// Thiếu UserRepository trong providers
})
export class UserModule {}- Thiếu
exportkhi service được dùng xuyên module
// ❌ SAI - AuthModule dùng UserService nhưng UserService chưa được export
@Module({
providers: [UserService],
// Thiếu exports
})
export class UserModule {}Cách fix dependency resolution
Cách phát hiện nhanh
- Đếm constructor parameters để xác định biến nào là
(?) - Kiểm tra service đó có trong
providerskhông - Nếu dùng qua module khác, kiểm tra có trong
exportskhông - Kiểm tra service có
@Injectable()
Cách đúng:
// ✅ ĐÚNG
@Module({
imports: [DatabaseModule],
controllers: [UserController],
providers: [UserService, UserRepository],
exports: [UserService],
})
export class UserModule {}1.2 Circular Dependency Detected
- Tần suất: Cao | Độ phức tạp: Cao
Circular dependency xảy ra khi Module A import Module B và Module B lại import Module A.
Ví dụ lỗi
Error: A circular dependency has been detected (UserService -> AuthService -> UserService).
Please make sure that each side of a bidirectional relationship is decorated with "forwardRef()".Vì sao xảy ra
// user.service.ts
@Injectable()
export class UserService {
constructor(private authService: AuthService) {}
}
// auth.service.ts
@Injectable()
export class AuthService {
constructor(private userService: UserService) {}
}Cách xử lý (theo mức ưu tiên)
Option 1: Tách shared logic (TỐT NHẤT)
@Injectable()
export class UserAuthSharedService {
validateUser(user: User) {
// shared logic
}
}
@Injectable()
export class UserService {
constructor(private shared: UserAuthSharedService) {}
}
@Injectable()
export class AuthService {
constructor(private shared: UserAuthSharedService) {}
}Option 2: Dùng forwardRef (GIẢI PHÁP TẠM)
@Injectable()
export class UserService {
constructor(
@Inject(forwardRef(() => AuthService))
private authService: AuthService,
) {}
}
@Injectable()
export class AuthService {
constructor(
@Inject(forwardRef(() => UserService))
private userService: UserService,
) {}
}WARNING
forwardRef() là dấu hiệu có vấn đề trong thiết kế. Dùng tạm thời trong lúc refactor sang Option 1.
1.3 Provider Scope Mismatches
- Tần suất: Trung bình | Độ phức tạp: Trung bình
NestJS provider có các scope DEFAULT (singleton), REQUEST, TRANSIENT. Dùng sai scope dễ gây lỗi khó đoán.
Ví dụ vấn đề
// ❌ SAI
@Injectable({ scope: Scope.REQUEST })
export class UserService {
constructor(private cacheService: CacheService) {}
}Quy tắc scope
Scope Rules
- DEFAULT: singleton toàn app
- REQUEST: instance mới theo mỗi request
- TRANSIENT: instance mới mỗi lần inject
Quy tắc: REQUEST có thể inject DEFAULT/REQUEST; nhưng DEFAULT không được inject REQUEST.
// ✅ ĐÚNG
@Injectable({ scope: Scope.REQUEST })
export class UserService {
constructor(
@Inject(REQUEST) private request: Request,
private cacheService: CacheService,
) {}
}2. Module Configuration Problems
2.1 Import vs Export Confusion
- Tần suất: Cao | Độ phức tạp: Thấp
Hiểu đúng imports, providers, exports, controllers là bắt buộc.
@Module({
imports: [OtherModule],
controllers: [MyController],
providers: [MyService],
exports: [MyService],
})
export class MyModule {}Lỗi thường gặp: export module thay vì service
// ❌ SAI
@Module({
providers: [ActorService],
exports: [ActorModule],
})
export class ActorModule {}
// ✅ ĐÚNG
@Module({
providers: [ActorService],
exports: [ActorService],
})
export class ActorModule {}2.2 Global vs Feature Modules
- Tần suất: Trung bình | Độ phức tạp: Thấp
Chỉ dùng @Global() cho service thực sự dùng chung (logger/config).
@Global()
@Module({
providers: [LoggerService, ConfigService],
exports: [LoggerService, ConfigService],
})
export class CoreModule {}WARNING
Không lạm dụng @Global(). Nó làm dependency tracking khó hơn. Ưu tiên explicit imports.
2.3 Dynamic Module Registration
- Tần suất: Trung bình | Độ phức tạp: Cao
Dynamic module cho phép cấu hình runtime (JwtModule.register(), forRoot()).
@Module({})
export class DatabaseModule {
static forRoot(options: DatabaseOptions): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: 'DATABASE_OPTIONS',
useValue: options,
},
DatabaseService,
],
exports: [DatabaseService],
};
}
}
@Module({
imports: [
DatabaseModule.forRoot({
host: 'localhost',
port: 5432,
}),
],
})
export class AppModule {}3. Request Lifecycle & Middleware
3.1 Execution Order
- Tần suất: Cao | Độ phức tạp: Trung bình
Hiểu thứ tự pipeline giúp tránh bug ở guard/interceptor/pipe.
Incoming Request
↓
1. Middleware
↓
2. Guards
↓
3. Interceptors (before)
↓
4. Pipes
↓
5. Route Handler
↓
6. Interceptors (after)
↓
7. Exception Filters (if error)
↓
ResponseLỗi thường gặp: nghĩ rằng guard chạy trước middleware
export class AuthMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
req.user = validateToken(req.headers.authorization);
next();
}
}
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
return !!request.user;
}
}3.2 Guard vs Interceptor Timing
| Use Case | Dùng gì |
|---|---|
| Authentication/Authorization | Guard |
| Logging, caching | Interceptor |
| Input validation | Pipe |
| Error transformation | Exception Filter |
3.3 Exception Filter Priority
- Tần suất: Trung bình | Độ phức tạp: Thấp
Filter càng cụ thể càng ưu tiên cao.
// 1. Method-level (cao nhất)
@Post()
@UseFilters(MethodExceptionFilter)
createUser() {}
// 2. Controller-level
@Controller('users')
@UseFilters(ControllerExceptionFilter)
export class UserController {}
// 3. Global-level (thấp nhất)
app.useGlobalFilters(new GlobalExceptionFilter());4. Database Integration Issues
4.1 TypeORM Connection Errors
- Tần suất: Cao | Độ phức tạp: Cao
Lỗi Unable to connect to the database thường gây hiểu nhầm; nhiều khi không phải do network/credential mà do entity syntax.
// ❌ SAI - dễ hiện thành "connection error"
@Entity()
export class User {
@Column('description')
name: string;
}
// ✅ ĐÚNG
@Entity()
export class User {
@Column()
name: string;
@Column({ type: 'text' })
description: string;
}Các bước debug
- Kiểm tra syntax decorator trong entity trước
- Kiểm tra entity đã khai báo trong
TypeOrmModule.forFeature([Entity]) - Kiểm tra credentials trong
.env - Bật
logging: trueở TypeORM config
4.2 Prisma Client Injection
- Tần suất: Trung bình | Độ phức tạp: Thấp
Prisma trong NestJS cần provider setup đúng.
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class DatabaseModule {}
@Injectable()
export class UserService {
constructor(private prisma: PrismaService) {}
async getUsers() {
return this.prisma.user.findMany();
}
}4.3 Multiple Database Connections
- Tần suất: Trung bình | Độ phức tạp: Trung bình
Dùng named connection khi có nhiều DB.
@Module({
imports: [
TypeOrmModule.forRoot({
name: 'default',
type: 'postgres',
host: 'localhost',
database: 'main_db',
entities: [User],
}),
TypeOrmModule.forRoot({
name: 'analytics',
type: 'postgres',
host: 'localhost',
database: 'analytics_db',
entities: [Event],
}),
],
})
export class AppModule {}
@Injectable()
export class UserService {
constructor(
@InjectRepository(User, 'default')
private userRepo: Repository<User>,
@InjectRepository(Event, 'analytics')
private eventRepo: Repository<Event>,
) {}
}5. Authentication & Authorization
5.1 "Unknown authentication strategy 'jwt'"
- Tần suất: Cao | Độ phức tạp: Thấp
Lỗi này nghĩa là Passport không tìm thấy JWT strategy.
Nguyên nhân thường gặp
- Import sai strategy
// ❌ SAI
import { Strategy } from 'passport-local';
// ✅ ĐÚNG
import { Strategy, ExtractJwt } from 'passport-jwt';- Chưa register strategy trong module
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
@Module({
imports: [
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1h' },
}),
],
providers: [AuthService, JwtStrategy],
})
export class AuthModule {}5.2 "secretOrPrivateKey must have a value"
- Tần suất: Cao | Độ phức tạp: Thấp
Lỗi do JWT secret bị thiếu/undefined.
// ❌ SAI
@Module({
imports: [
JwtModule.register({
secret: process.env.JWT_SECRET,
}),
],
})
export class AuthModule {}
// ✅ ĐÚNG
@Module({
imports: [
ConfigModule.forRoot(),
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '1h' },
}),
}),
],
})
export class AuthModule {}5.3 Guard Execution Context
- Tần suất: Trung bình | Độ phức tạp: Trung bình
ExecutionContext xử lý khác nhau giữa HTTP, WebSocket, GraphQL.
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = request.user;
const requiredRoles = this.reflector.get<string[]>(
'roles',
context.getHandler(),
);
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
@Post()
@Roles('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
createUser() {}6. Testing Challenges
6.1 Mocking Dependencies
- Tần suất: Cao | Độ phức tạp: Trung bình
Unit test cần mock đầy đủ dependencies.
describe('UserService', () => {
let service: UserService;
let mockUserRepo: jest.Mocked<Repository<User>>;
beforeEach(async () => {
mockUserRepo = {
findOne: jest.fn(),
save: jest.fn(),
} as any;
const module = await Test.createTestingModule({
providers: [
UserService,
{
provide: getRepositoryToken(User),
useValue: mockUserRepo,
},
],
}).compile();
service = module.get<UserService>(UserService);
});
it('should find user by id', async () => {
const user = { id: 1, name: 'Alice' };
mockUserRepo.findOne.mockResolvedValue(user);
const result = await service.findById(1);
expect(result).toEqual(user);
expect(mockUserRepo.findOne).toHaveBeenCalledWith({ where: { id: 1 } });
});
});6.2 E2E Test Setup
- Tần suất: Cao | Độ phức tạp: Cao
E2E test cần full app context.
describe('UserController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(PrismaService)
.useValue(mockPrismaService)
.compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('/users (GET)', () => {
return request(app.getHttpServer())
.get('/users')
.expect(200)
.expect((res) => {
expect(res.body).toHaveLength(2);
});
});
});6.3 Testing Custom Decorators
- Tần suất: Trung bình | Độ phức tạp: Trung bình
Custom decorator cần cách test riêng.
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
it('should extract user from request', () => {
const mockContext = {
switchToHttp: () => ({
getRequest: () => ({ user: { id: 1, name: 'Alice' } }),
}),
} as ExecutionContext;
const result = CurrentUser(null, mockContext);
expect(result).toEqual({ id: 1, name: 'Alice' });
});7. Production Performance Issues
7.1 Memory Leaks
- Tần suất: Thấp | Độ phức tạp: Cao
Memory leak thường đến từ connection/listener không được đóng.
// ❌ SAI
@Injectable()
export class NotificationService implements OnModuleInit {
onModuleInit() {
eventEmitter.on('user.created', this.sendEmail);
}
}
// ✅ ĐÚNG
@Injectable()
export class NotificationService implements OnModuleInit, OnModuleDestroy {
onModuleInit() {
eventEmitter.on('user.created', this.sendEmail);
}
onModuleDestroy() {
eventEmitter.off('user.created', this.sendEmail);
}
}7.2 Blocking the Event Loop
- Tần suất: Trung bình | Độ phức tạp: Cao
Tác vụ CPU nặng sẽ block event loop Node.js.
// ❌ SAI
@Get('report')
async generateReport() {
const data = await this.getData();
return this.processLargeDataset(data);
}
// ✅ ĐÚNG
@Get('report')
async generateReport() {
const jobId = await this.queue.add('generate-report', { userId: 123 });
return { jobId, status: 'processing' };
}7.3 Graceful Shutdown
- Tần suất: Thấp | Độ phức tạp: Trung bình
Đảm bảo shutdown sạch để tránh mất dữ liệu.
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();
await app.listen(3000);
}
@Injectable()
export class DatabaseService implements OnModuleDestroy {
async onModuleDestroy() {
await this.connection.close();
console.log('Database connection closed');
}
}8. Best Practices Checklist
NestJS Best Practices
Module Architecture
- [ ] Tất cả service có
@Injectable() - [ ] Provider nằm đúng trong
providers, vàexportskhi cần dùng xuyên module - [ ] Tránh circular dependency (hoặc chỉ dùng
forwardReftạm thời) - [ ] Module boundaries tách theo domain/feature
Database Integration
- [ ] Entity decorator đúng cú pháp (
@Column()thay vì@Column('name')) - [ ] Connection error không làm app crash cứng (có retry logic)
- [ ] Nhiều DB thì dùng named connection
- [ ] Repository test qua
getRepositoryToken(Entity)
Authentication
- [ ] JWT Strategy import từ
passport-jwt - [ ] JWT secret đọc từ environment qua
ConfigService - [ ] Header đúng format:
Bearer [token] - [ ] Guard bảo vệ route đúng
Testing
- [ ] Unit test mock đủ dependencies
- [ ] E2E dùng
Test.createTestingModule() - [ ] TypeORM repository mock bằng
getRepositoryToken() - [ ] Mọi async operation đều được
await
Production
- [ ] Cleanup event listener trong
onModuleDestroy() - [ ] CPU-intensive work đẩy sang queue/worker
- [ ] Bật graceful shutdown
- [ ] Có health checks
9. Debugging Checklist
Khi gặp lỗi NestJS, đi theo luồng sau:
1. Dependency Injection Error?
- Kiểm tra providers array
- Kiểm tra exports nếu dùng xuyên module
- Kiểm tra typo ở constructor parameters
- Kiểm tra @Injectable()
2. Module Configuration Issue?
- Soát imports/providers/exports
- Kiểm tra circular dependency
- Kiểm tra dynamic module registration
3. Request Lifecycle Problem?
- Nắm thứ tự: Middleware → Guards → Interceptors → Pipes
- Kiểm tra guard trả về đúng (boolean hoặc throw)
- Kiểm tra xử lý async trong interceptor
4. Database Connection Error?
- Kiểm tra entity decorator syntax trước tiên
- Kiểm tra credentials DB
- Bật TypeORM logging
- Test kết nối độc lập
5. Authentication Failure?
- Kiểm tra import strategy (passport-jwt vs passport-local)
- Kiểm tra JWT_SECRET đã load chưa
- Kiểm tra format token (Bearer [token])
- Kiểm tra strategy registration trong module
6. Test Failing?
- Mock đủ dependencies
- Dùng getRepositoryToken() cho TypeORM
- Kiểm tra async/await trong test
- Kiểm tra imports của test module10. Conclusion
Các lỗi NestJS thường đến từ sức mạnh nhưng cũng là độ phức tạp của DI system, decorator architecture và TypeScript integration. Nhóm lỗi phổ biến nhất gồm:
- Thiếu providers/exports
- Circular dependencies
- Lỗi database gây hiểu nhầm (thực ra lỗi entity syntax)
- Cấu hình auth sai (import sai hoặc thiếu secret)
- Setup testing chưa chuẩn
Khi nắm đúng pattern và dùng checklist debug có hệ thống, bạn có thể:
- Chẩn đoán nhanh hơn
- Tránh lỗi lặp lại khi phát triển
- Xây dựng app NestJS sẵn sàng production
- Debug tự tin hơn khi incident xảy ra
Nên đưa tài liệu này vào code review checklist và onboarding docs của team.
