Головна / Статті / Діагностика помилки 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.