Диагностика ошибки NestJS: невозможно разрешить зависимости, причины по порядку возникновения
Научитесь распознавать ошибки разрешения зависимостей в NestJS и устранять пять наиболее распространённых причин их возникновения: отсутствие провайдеров, невыведенные модули, циклы зависимостей, некорректные токены и модули тестов без обёртки.
Рано или поздно каждый проект NestJS останавливается при запуске с сообщением о том, что контейнер не может разрешить аргумент конструктора. Формулировка кажется неясной, но на самом деле точная, и почти всегда причина кроется в одной из пяти ошибок настройки. В этом руководстве объясняется, как интерпретировать это сообщение, и рассматриваются все возможные причины в том порядке, в котором их следует проверять.
Интерпретация сообщения об ошибке
Типичная ошибка выглядит так:
Nest can't resolve dependencies of the UsersService (?). Please make sure that the argument at index [0] is available in the UsersModule context.
Класс, упомянутый в сообщении (UsersService), — это тот, который Nest пытался инстанцировать. В скобках перечисляются все параметры конструктора, причем ? обозначает тот, который вызвал ошибку; index [0] указывает на соответствующую позицию. В последней части указывается модуль, в котором происходила поиск. Каждое из решений ниже представляет собой способ сделать этот аргумент видимым.
Причина 1: класс так и не был зарегистрирован как поставщик
Самый простой случай: файл сервиса существует, но ни один модуль его не указывает. Nest управляет только классами, которые присутствуют в массиве providers модуля.
@Module({
controllers: [UsersController],
providers: [UsersService], // <-- missing? that's your error
})
export class UsersModule {}
Команда CLI nest generate service обновляет модуль за вас. При ручном создании сервисов этот шаг часто забывается.
Причина 2: провайдер находится в другом модуле, который не импортируется и не экспортируется
Это самый распространённый вариант межмодульного взаимодействия. AuthService зарегистрирован в AuthModule, и UsersService его подключает, но в UsersModule нет импорта, указывающего на AuthModule:
@Module({
imports: [AuthModule], // <-- without this, AuthService is invisible here
providers: [UsersService],
})
export class UsersModule {}
Импорт — это лишь половина условий соглашения. Модуль, содержащий провайдер, также должен указать его в разделе exports:
@Module({
providers: [AuthService],
exports: [AuthService], // <-- other modules can only use what you export
})
export class AuthModule {}
Полезная модель мышления: провайдер является приватным для своего модуля, если только он не экспортируется, а экспортированный провайдер по-прежнему остается невидимым для модулей, которые не импортируют его создателя. Должны соблюдаться оба условия.
Причина 3: два сервиса зависят друг от друга
Если UsersService нуждается в OrdersService, а OrdersService — в UsersService, ни один из них не может быть создан в первую очередь. Nest предоставляет функцию forwardRef() для отсрочки решения этой проблемы. Она применяется на уровне модуля при импорте:
// users.module.ts
@Module({
imports: [forwardRef(() => OrdersModule)],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
и снова в месте инъекции в конструкторе сервиса:
// users.service.ts
constructor(
@Inject(forwardRef(() => OrdersService))
private ordersService: OrdersService,
) {}
На другой стороне необходимо аналогичное решение (OrdersModule импортирует forwardRef(() => UsersModule)). Рассматривайте это как временный патч, а не окончательное решение. Цикл обычно означает, что поведение, предназначенное для совместного использования, находится не в том месте; его перемещение в третий модуль, который могут импортировать оба модуля, полностью устраняет цикл и необходимость в forwardRef.
Связанная проблема: циклические импорты файлов, часто через общие файлы index.ts, могут привести к тому, что ссылка на класс во время декорирования станет undefined. В таком случае Nest сообщает о нерешаемой зависимости, хотя модули кажутся корректными.
Причина 4: токен инъекции не совпадает
Пользовательские поставщики регистрируются под определенным токеном, и инъекция должна использовать именно этот токен. Рассмотрим поставщик значений, ключом к которому является строка:
{
provide: 'CONFIG_OPTIONS',
useValue: configOptions,
}
Если при объявлении параметра конструктора указывается только тип, его не удастся найти, поскольку тип сам по себе не является токеном. Используйте @Inject() с той же строкой:
constructor(@Inject('CONFIG_OPTIONS') private config: ConfigOptions) {}
Типы в TypeScript также исчезают во время выполнения, поэтому интерфейс никогда не может служить токеном сам по себе.
Хранилища TypeORM тоже работают так же: указание параметра как класса хранилища не совпадает с зарегистрированным токеном Nest:
// Wrong
constructor(private repo: UserRepository) {}
В рабочей версии используется @InjectRepository() с энтити:
// Right
constructor(
@InjectRepository(User)
private repo: Repository<User>,
) {}
Чтобы этот токен вообще существовал, модуль также должен импортировать TypeOrmModule.forFeature([User]).
Причина 5: в модуле тестирования отсутствуют провайдеры или моки
Иногда приложение запускается без проблем, в то время как тесты на единицы выдают одинаковую ошибку. Метод Test.createTestingModule создаёт совершенно новый контейнер, содержащий только то, что вы указали, поэтому все зависимости тестируемого класса должны быть предоставлены, обычно в виде моков, связанных с соответствующим токеном:
const module = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockRepository, // <-- every dependency needs one of these
},
],
}).compile();
getRepositoryToken(User) генерирует тот же токен, который ищет @InjectRepository(User). Если ошибка появляется только в тестах, значит конфигурация в продакшене корректна, а настройка тестов неполная.
Чек-лист для отладки
Выполните следующие шаги в порядке:
- Включён ли класс в списке
providersсоответствующего модуля? - Импортирован ли модуль, содержащий этот провайдер, в месте его использования, и экспортирует ли он сам провайдер?
forwardRef для исправления, затем проведите рефакторинг.Основные выводы
- Знак
?и индекс в сообщении точно указывают, какой аргумент конструктора отсутствует и в пределах какого модуля. - В Nest видимость элементов явна: регистрация, экспорт, импорт.
forwardRefскрывает циклы; извлечение общего модуля позволяет их устранить.- Для инъекции во время выполнения используются токены, а не типы TypeScript.
- Модули тестов являются отдельными контейнерами и требуют собственной полной настройки связей.
Чтобы узнать больше о том, как сохранять здоровую структуру модулей по мере роста кодовой базы, ознакомьтесь с шестью правилами DDD для структурирования доменов в приложениях NestJS.