Главная / Статьи / Диагностика ошибки NestJS: невозможно разрешить зависимости, причины по порядку возникновения

Диагностика ошибки NestJS: невозможно разрешить зависимости, причины по порядку возникновения

Научитесь распознавать ошибки разрешения зависимостей в NestJS и устранять пять наиболее распространённых причин их возникновения: отсутствие провайдеров, невыведенные модули, циклы зависимостей, некорректные токены и модули тестов без обёртки.

1046 слов

Рано или поздно каждый проект 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). Если ошибка появляется только в тестах, значит конфигурация в продакшене корректна, а настройка тестов неполная.

Чек-лист для отладки

Выполните следующие шаги в порядке:

  1. Включён ли класс в списке providers соответствующего модуля?
  2. Импортирован ли модуль, содержащий этот провайдер, в месте его использования, и экспортирует ли он сам провайдер?
  • Существует ли циклическая зависимость между сервисами или файлами? Используйте forwardRef для исправления, затем проведите рефакторинг.
  • Для пользовательских провайдеров и репозиториев совпадает ли токен инъекции точно с данными регистрации?
  • Происходит ли ошибка только в тестах? Добавьте отсутствующие провайдеры или моки.
  • Основные выводы

    • Знак ? и индекс в сообщении точно указывают, какой аргумент конструктора отсутствует и в пределах какого модуля.
    • В Nest видимость элементов явна: регистрация, экспорт, импорт.
    • forwardRef скрывает циклы; извлечение общего модуля позволяет их устранить.
    • Для инъекции во время выполнения используются токены, а не типы TypeScript.
    • Модули тестов являются отдельными контейнерами и требуют собственной полной настройки связей.

    Чтобы узнать больше о том, как сохранять здоровую структуру модулей по мере роста кодовой базы, ознакомьтесь с шестью правилами DDD для структурирования доменов в приложениях NestJS.