Strona główna / Artykuły / Diagnozowanie błędu „NestJS Cannot Resolve Dependencies” – przyczyny po kolei

Diagnozowanie błędu „NestJS Cannot Resolve Dependencies” – przyczyny po kolei

Naucz się odczytywać błędy rozwiązywania zależności w NestJS oraz naprawiać ich pięć najczęstszych przyczyn: brakujące dostawcy, nieeksporowane moduły, cykle, błędne tokeny oraz proste moduły testowe.

1046 słów

Ranо czy późno każdy projekt NestJS zatrzymuje się podczas uruchamiania z komunikatem o tym, że kontener nie może rozwiązać argumentu konstruktora. Sformułowanie to wydaje się niejasne, ale jest precyzyjne i niemal zawsze wynika z jednego z pięciu błędów konfiguracji. Ten przewodnik pokazuje, jak odczytać ten komunikat, oraz omawia każdą przyczynę w kolejności, w której należy je sprawdzić.

Odczytywanie komunikatu o błędzie

Typowy przypadek awarii wygląda tak:

Nest can't resolve dependencies of the UsersService (?). Please make sure that the argument at index [0] is available in the UsersModule context.

Klasa wymieniona w komunikacie (UsersService) to ta, którą Nest próbował zainstancjować. W nawiasach wymienione są wszystkie parametry konstruktora, a ? oznacza ten, który spowodował błąd; index [0] wskazuje na tę pozycję. Ostatnia część komunikatu podaje nazwę modułu, w którego zakresie przeprowadzano poszukiwania. Każde z poniższych rozwiązań polega na ujawnieniu tego argumentu.

Przyczyna 1: klasa nigdy nie została zarejestrowana jako dostawca

Najprostszy przypadek: plik usługi istnieje, ale żaden moduł go nie wymienia. Nest zarządza tylko klasami, które pojawiają się w tablicy providers modułu.

@Module({
  controllers: [UsersController],
  providers: [UsersService], // <-- missing? that's your error
})
export class UsersModule {}

Polecenie CLI nest generate service aktualizuje moduł za Ciebie. W przypadku ręcznie napisanych usług ten krok jest często pomijany.

Powód 2: dostawca znajduje się w innym module, który nie jest importowany ani eksportowany

To najczęstsza wersja problemu międzymodułowego. AuthService jest zarejestrowany w AuthModule, a UsersService go injectuje, ale UsersModule nie zawiera importu wskazującego na 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 {}

Przydatny model mentalny: dostawca jest dostępny wyłącznie wewnątrz swojego modułu, chyba że został wyeksportowany, a dostawca wyeksportowany pozostaje niewidoczny dla modułów, które nie importują jego właściciela. Obie te warunki muszą być spełnione.

Powód 3: dwa usługi są od siebie zależne

Jeśli UsersService potrzebuje OrdersService, a OrdersService potrzebuje UsersService, żadna z nich nie może zostać najpierw stworzona. Nest oferuje funkcję forwardRef() do odroczenia rozstrzygnięcia tej kwestii. Jest ona stosowana na poziomie modułu przy importowaniu:

// users.module.ts
@Module({
  imports: [forwardRef(() => OrdersModule)],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

i ponownie w miejscu iniekcji, w konstruktorze usługi:

// users.service.ts
constructor(
  @Inject(forwardRef(() => OrdersService))
  private ordersService: OrdersService,
) {}

Obraz lustrzany jest potrzebny po drugiej stronie (OrdersModule importujący forwardRef(() => UsersModule)). Traktuj to jako tymczasowe rozwiązanie, a nie ostateczną metodę. Cykl zazwyczaj oznacza, że wspólne zachowanie znajduje się w niewłaściwym miejscu; przeniesienie go do trzeciego modułu, który może go importować oba, całkowicie eliminuje cykl oraz potrzebę użycia forwardRef.

Powiązana pułapka: cykliczne importy plików, często poprzez pliki index.ts, mogą sprawić, że odniesienie do klasy stanie się undefined w momencie dekoracji. Wtedy Nest zgłasza nie rozwiązalną zależność, mimo że moduły wyglądają poprawnie.

Powód 4: token iniekcji nie pasuje

Dostawcy niestandardowi są rejestrowani pod określonym tokenem, a iniekcja musi używać dokładnie tego tokena. Rozważmy dostawcę wartości oznaczonyego łańcuchem znaków:

{
  provide: 'CONFIG_OPTIONS',
  useValue: configOptions,
}

Deklarowanie parametru konstruktora tylko z typem nie pozwoli na jego znalezienie, ponieważ typ sam w sobie nie jest tokenem. Należy użyć @Inject() z tą samą łańcuchem znaków:

constructor(@Inject('CONFIG_OPTIONS') private config: ConfigOptions) {}

Typy w TypeScript również znikają w czasie wykonywania, dlatego interfejs sam w sobie nigdy nie może pełnić roli tokena.

Repozytoria TypeORM działają w ten sam sposób. Określenie parametru jako klasy repozytorium nie odpowiada tokenowi zarejestrowanemu przez Nest:

// Wrong
constructor(private repo: UserRepository) {}

Wersja działająca wykorzystuje @InjectRepository() wraz z entytą:

// Right
constructor(
  @InjectRepository(User)
  private repo: Repository<User>,
) {}

Aby ten token w ogóle istniał, moduł musi również zaimportować TypeOrmModule.forFeature([User]).

Powód 5: moduł testowy nie zawiera dostawców ani mocków

Czasami aplikacja uruchamia się poprawnie, podczas gdy testy jednostkowe wywołują ten sam błąd. Test.createTestingModule tworzy zupełnie nowy kontener zawierający tylko to, co zadeklarowaliśmy, więc każda zależność klasy testowanej musi zostać dostarczona, zwykle w postaci mocka powiązanego z właściwym tokenem:

const module = await Test.createTestingModule({
  providers: [
    UsersService,
    {
      provide: getRepositoryToken(User),
      useValue: mockRepository, // <-- every dependency needs one of these
    },
  ],
}).compile();

getRepositoryToken(User) wytwarza ten sam token, którego szuka @InjectRepository(User). Jeśli błąd pojawia się tylko w testach, to konfiguracja w środowisku produkcyjnym jest poprawna, a ustawienia testów są niekompletne.

Lista kontrolna do debugowania

Przejdź przez nie w kolejności:

  1. Czy klasa jest wymieniona w providers odpowiedniego modułu?
  2. Czy moduł, który ją definiuje, jest importowany w miejscu użycia dostawcy i czy eksportuje go?
  • Czy istnieje zależność cykliczna, czy to pomiędzy usługami, czy pomiędzy plikami? Napraw ją za pomocą forwardRef, a następnie przeprojektuj kod.
  • Czy token iniekcji dla niestandardowych dostawców i repositoriów dokładnie odpowiada ich rejestracji?
  • Czy błąd występuje tylko w testach? Dodaj brakujące dostawcy lub mocki.
  • Główne wnioski

    • ? oraz indeks w komunikacie precyzyjnie wskazują, który argument konstruktora jest brakujący i w jakim zakresie modułu.
    • Widoczność elementów w Nest jest jawna: rejestracja, eksport, import.
    • forwardRef ukrywa cykle; wyodrębnienie wspólnego modułu je naprawia.
    • To tokeny, a nie typy TypeScript, sterują iniekcją w czasie wykonywania.
    • Moduły testowe to oddzielne kontenery i wymagają własnego, kompletnego połączenia komponentów.

    Aby dowiedzieć się więcej na temat utrzymania zdrowych granic modułów w miarę rozwoju bazy kodu, zapoznaj się z sześcioma zasadami DDD dotyczącymi strukturyzacji domen w aplikacjach NestJS.