Что такое Circular Dependencies в NestJS?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Circular Dependencies в NestJS
Circular Dependencies (циклические зависимости) — это одна из самых распространенных проблем в NestJS приложениях. Это происходит когда модуль A зависит от модуля B, а модуль B зависит от модуля A, создавая цикл. В production это приводит к ошибкам инициализации и ненадежности приложения.
Как возникают Circular Dependencies
Сценарий 1: Service-to-Service зависимость
// users.service.ts
import { Injectable } from '@nestjs/common';
import { PostsService } from './posts.service';
@Injectable()
export class UsersService {
constructor(private postsService: PostsService) {}
async getUserWithPosts(userId: number) {
const user = await this.findUser(userId);
// Используем PostsService
const posts = await this.postsService.findByUserId(userId);
return { ...user, posts };
}
}
// posts.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from './users.service'; // CIRCULAR!
@Injectable()
export class PostsService {
constructor(private usersService: UsersService) {}
async findByUserId(userId: number) {
// Проверяем что пользователь существует
const user = await this.usersService.findUser(userId);
return this.db.posts.find({ userId });
}
}
Это создает цикл: UsersService → PostsService → UsersService
Сценарий 2: Module-level dependencies
// users.module.ts
import { Module } from '@nestjs/common';
import { PostsModule } from './posts.module';
@Module({
imports: [PostsModule], // UsersModule зависит от PostsModule
providers: [UsersService],
})
export class UsersModule {}
// posts.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users.module'; // CIRCULAR!
@Module({
imports: [UsersModule], // PostsModule зависит от UsersModule
providers: [PostsService],
})
export class PostsModule {}
Симптомы Circular Dependencies
- Runtime error при инициализации:
Error: Circular dependency detected while instantiating provider "UsersService"
- undefined сервисы в constructor
const usersService = new UsersService();
console.log(usersService); // undefined!
- Race conditions при lazy loading
Решение 1: Refactoring архитектуры (BEST)
Лучший подход — избежать circular dependency через правильную архитектуру:
// shared.service.ts — создаем новый сервис для общей логики
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
@Injectable()
export class UserValidationService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async validateUser(userId: number) {
const user = await this.userRepository.findOne(userId);
if (!user) throw new NotFoundException();
return user;
}
}
// users.service.ts — теперь не зависит от PostsService
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
private validationService: UserValidationService,
) {}
async getUser(userId: number) {
return this.validationService.validateUser(userId);
}
}
// posts.service.ts — не зависит от UsersService
@Injectable()
export class PostsService {
constructor(
@InjectRepository(Post)
private postRepository: Repository<Post>,
private validationService: UserValidationService,
) {}
async findByUserId(userId: number) {
await this.validationService.validateUser(userId);
return this.postRepository.find({ userId });
}
}
Принцип: Вместо A → B → A, создаем C который знают обе.
Решение 2: forwardRef() — временное решение
NestJS предоставляет forwardRef() для разрешения circular dependencies:
import { forwardRef } from '@nestjs/common';
// users.service.ts
@Injectable()
export class UsersService {
constructor(
@Inject(forwardRef(() => PostsService))
private postsService: PostsService,
) {}
}
// posts.service.ts
@Injectable()
export class PostsService {
constructor(
@Inject(forwardRef(() => UsersService))
private usersService: UsersService,
) {}
}
// В модуле указываем оба сервиса
@Module({
providers: [UsersService, PostsService],
exports: [UsersService, PostsService],
})
export class AppModule {}
Это работает, но это code smell — указывает на проблемы в архитектуре.
Решение 3: Lazy Module Loading
Для module-level circular dependencies используем lazy loading:
// users.module.ts
import { forwardRef } from '@nestjs/common';
import { PostsModule } from './posts.module';
@Module({
imports: [forwardRef(() => PostsModule)],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// posts.module.ts
import { forwardRef } from '@nestjs/common';
import { UsersModule } from './users.module';
@Module({
imports: [forwardRef(() => UsersModule)],
providers: [PostsService],
exports: [PostsService],
})
export class PostsModule {}
Решение 4: Event-driven architecture
Вместо прямых зависимостей, используем события:
// events.service.ts
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class PostsService {
constructor(private eventEmitter: EventEmitter2) {}
async createPost(post: CreatePostDto) {
const newPost = await this.db.posts.create(post);
// Вместо вызова UsersService, эмитим событие
this.eventEmitter.emit('post.created', newPost);
return newPost;
}
}
// users.service.ts
@Injectable()
export class UsersService {
constructor(private eventEmitter: EventEmitter2) {}
@OnEvent('post.created')
async handlePostCreated(post: Post) {
// Обновляем статистику пользователя
await this.updateUserStats(post.userId);
}
}
Это полностью развязывает зависимости.
Real-world пример: Social Media система
Проблема:
// Плохая архитектура
UsersService ← нужны posts для профиля
PostsService ← нужны users для валидации
Хорошее решение:
// shared.service.ts
@Injectable()
export class SharedUserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async validateAndGetUser(userId: number) {
return this.userRepository.findOne(userId);
}
}
// users.service.ts
@Injectable()
export class UsersService {
constructor(private sharedUserService: SharedUserService) {}
async getUserProfile(userId: number) {
const user = await this.sharedUserService.validateAndGetUser(userId);
// Получаем посты через API или другой сервис, но не прямой dependency
return user;
}
}
// posts.service.ts
@Injectable()
export class PostsService {
constructor(private sharedUserService: SharedUserService) {}
async createPost(userId: number, post: CreatePostDto) {
await this.sharedUserService.validateAndGetUser(userId);
return this.postRepository.create(post);
}
}
Detection: как найти Circular Dependencies
# NestJS CLI может помочь
npm run build -- --debug
# Или используем визуализацию зависимостей
npm install -D @nestjs/schematics
Лучшие практики
- Проектируй в слои: Controllers → Services → Repositories
- Избегай bidirectional зависимостей:
// ✅ Хорошо: одностороняя
UserService → ValidationService
// ❌ Плохо: двусторонняя
UserService ↔ PostService
- Используй Dependency Inversion:
// ✅ Зависит от abstraction
constructor(private validator: IValidator) {}
// ❌ Зависит от конкретной реализации
constructor(private userService: UsersService) {}
- Shared/Common сервисы для часто используемой логики
Вывод
Circular dependencies — это симптом, а не болезнь. Правильное лечение — refactoring архитектуры. Использование forwardRef() — это patch, не решение. В production системах я всегда ищу и удаляю circular dependencies потому что они создают хрупкость и сложность в поддержке кода.