NestJS / 37 минут

Модули NestJS

Собирайте возможности приложения в связанные feature-модули.

В процессе Теория: впереди Практика: впереди Тест: впереди
02 / 14
После этой главы
  • Собирайте возможности приложения в связанные feature-модули.
  • Применить тему в задаче «Модули NestJS» и объяснить граничные случаи.

Около 7 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Модуль как граница возможности

Модуль описывает связную часть приложения: каталог книг, авторизацию или работу с заметками. Его назначение — сгруппировать относящиеся к одной возможности контроллеры и провайдеры и явно определить внешние зависимости. Это не отдельный процесс и не обязательный микросервис. Несколько feature-модулей могут прекрасно работать в одном приложении и одной БД.

@Module содержит metadata, по которой Nest строит граф. imports подключает другие модули, controllers регистрирует HTTP-контроллеры, providers — доступные зависимости, exports — провайдеры, которые модуль разрешает использовать импортирующим потребителям. Эти поля не взаимозаменяемы, даже если во всех них записываются массивы.

Регистрация providers и controllers

Провайдер-класс помечают @Injectable и регистрируют в providers подходящего модуля. Контроллеры помещают в controllers. Объявить класс в файле и импортировать его TypeScript-оператором недостаточно: статический импорт делает имя доступным исходному коду, а Nest-регистрация делает его частью контейнера приложения.

TypeScript
@Injectable()
class BooksService {
  list() { return ['JavaScript']; }
}
@Controller('books')
class BooksController {
  constructor(private readonly books: BooksService) {}
  @Get() list() { return this.books.list(); }
}
@Module({ providers: [BooksService], controllers: [BooksController] })
class BooksModule {}

В обычной конфигурации для constructor injection должны корректно работать настройки декораторов и metadata TypeScript. Если зависимость описана интерфейсом, он исчезает после компиляции, поэтому потребуется явный runtime-токен. Этому посвящена следующая глава; не лечите такую проблему случайным ручным new внутри controller.

imports и exports

Если другой модуль должен использовать BooksService, BooksModule экспортирует его, а потребитель импортирует BooksModule. Экспортировать весь внутренний набор не обязательно: внешний API модуля лучше держать узким. Детали хранилища могут оставаться приватными, пока потребителю достаточно публичного сервиса сценария.

TypeScript
@Module({ providers: [BooksService], exports: [BooksService] })
class BooksModule {}
@Module({ imports: [BooksModule], providers: [RecommendationsService] })
class RecommendationsModule {}

Не регистрируйте тот же сервис заново в каждом модуле только для устранения ошибки разрешения. Это может создать отдельные экземпляры и нарушить ожидаемое общее состояние. Правильная связь проходит через экспорт провайдера и импорт владеющего модуля. Поймите область видимости, прежде чем добавлять класс во все встретившиеся массивы.

Значения и фабрики как провайдеры

Провайдер не обязан быть только классом. useValue удобно передаёт конфигурацию или fake, useFactory вычисляет зависимость, useClass выбирает конкретную реализацию, useExisting создаёт связь с уже зарегистрированным провайдером. Во всех случаях токен должен существовать во время исполнения и совпадать у регистратора и потребителя.

TypeScript
const GREETING = 'GREETING';
@Injectable()
class GreetingService {
  constructor(@Inject(GREETING) private readonly text: string) {}
  message() { return this.text; }
}
@Module({
  providers: [GreetingService, { provide: GREETING, useValue: 'Привет' }],
})
class AppModule {}

Строка в примере — явный учебный токен. Для больших приложений Symbols помогают уменьшить случайные совпадения. При этом два отдельно созданных Symbol с одинаковым описанием не равны. Определяйте и экспортируйте один токен, а не повторяйте его создание в каждом файле.

Общее и глобальное

Инфраструктура может быть общей, но @Global не следует использовать как универсальное исправление зависимостей. Слишком много неявно доступных провайдеров делает граф труднее чтения и тестирования. Конфигурация, клиент БД и другие действительно общие адаптеры должны иметь понятную регистрацию и узкий API.

Циклические импорты модулей часто показывают смешение обязанностей. Иногда framework предлагает технический механизм forwardRef, но сначала полезно спросить, можно ли выделить общий контракт или изменить направление зависимости. Техническое разрешение цикла не делает архитектурный смысл автоматически хорошим.

Практика контекста и сообщения

Создайте модуль с GreetingService и токеном GREETING, получающим переданный text через useValue. Получите сервис из application context, вызовите message и закройте контекст в finally. Это проверяет реальную регистрацию и разрешение зависимости без HTTP-сервера. Пустая строка остаётся допустимым сообщением, если так задан вход.

После решения экспортируйте GreetingService из отдельного модуля и используйте его во втором. Убедитесь, что не создали дубликат провайдера в потребителе. Для итогового проекта разложите библиотеку по бизнес-возможностям и опишите публичные exports каждой одной фразой. Это помогает удержать модули как смысловые границы, а не случайные папки.

Загружаем редактор…
Следующая темаDependency Injection

Музыка: Reverie — Scott Buckley · CC BY 4.0