Skip to content

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

bash
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

  1. Provider không nằm trong providers của module
ts
// ❌ SAI
@Module({
  controllers: [UserController],
  // Thiếu UserRepository trong providers
})
export class UserModule {}
  1. Thiếu export khi service được dùng xuyên module
ts
// ❌ 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

  1. Đếm constructor parameters để xác định biến nào là (?)
  2. Kiểm tra service đó có trong providers không
  3. Nếu dùng qua module khác, kiểm tra có trong exports không
  4. Kiểm tra service có @Injectable()

Cách đúng:

ts
// ✅ ĐÚ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

bash
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

ts
// 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)
ts
@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)
ts
@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 đề

ts
// ❌ 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.

ts
// ✅ ĐÚ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.

ts
@Module({
  imports: [OtherModule],
  controllers: [MyController],
  providers: [MyService],
  exports: [MyService],
})
export class MyModule {}

Lỗi thường gặp: export module thay vì service

ts
// ❌ 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).

ts
@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()).

ts
@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.

text
Incoming Request

1. Middleware

2. Guards

3. Interceptors (before)

4. Pipes

5. Route Handler

6. Interceptors (after)

7. Exception Filters (if error)

Response

Lỗi thường gặp: nghĩ rằng guard chạy trước middleware

ts
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 CaseDùng gì
Authentication/AuthorizationGuard
Logging, cachingInterceptor
Input validationPipe
Error transformationException 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.

ts
// 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.

ts
// ❌ 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

  1. Kiểm tra syntax decorator trong entity trước
  2. Kiểm tra entity đã khai báo trong TypeOrmModule.forFeature([Entity])
  3. Kiểm tra credentials trong .env
  4. 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.

ts
@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.

ts
@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

  1. Import sai strategy
ts
// ❌ SAI
import { Strategy } from 'passport-local';

// ✅ ĐÚNG
import { Strategy, ExtractJwt } from 'passport-jwt';
  1. Chưa register strategy trong module
ts
@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.

ts
// ❌ 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.

ts
@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.

ts
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.

ts
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.

ts
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.

ts
// ❌ 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.

ts
// ❌ 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.

ts
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à exports khi cần dùng xuyên module
  • [ ] Tránh circular dependency (hoặc chỉ dùng forwardRef tạ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:

text
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 module

10. 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:

  1. Thiếu providers/exports
  2. Circular dependencies
  3. Lỗi database gây hiểu nhầm (thực ra lỗi entity syntax)
  4. Cấu hình auth sai (import sai hoặc thiếu secret)
  5. 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.


Additional Resources

Được phát hành theo giấy phép MIT.