Модули Nest и область доступности provider
Bootstrap знает AppModule, но API состоит из нескольких связанных обязанностей. Заявки обслуживает TicketsModule, а соединениями PostgreSQL владеет DatabaseModule. Рассмотрим, как Nest-модуль объявляет свои controllers, providers, imports и exports. Результат главы — понятный граф доступности зависимостей, в котором один пул не создаётся случайно заново в каждом обработчике и инфраструктурный объект не превращается в публичный HTTP-контроллер.
Module в Nest описывает группу средств приложения и правила их связывания. Это не ESM-модуль файла: обычный import делает имя доступным TypeScript, а Nest metadata сообщает контейнеру, какие providers принадлежат области и какие можно получить из импортированного модуля. Оба механизма нужны. Сам факт import { Database } в repository не означает, что Nest уже знает, где создать Database для его конструктора.
Модуль заявок
Ниже полный src/tickets.module.ts. Здесь controller объявлен отдельно, а service, repository и два guards — как providers. DatabaseModule попадает в imports, поскольку экспортирует Database. Имена классов совпадают с файлами конечного архива; локальные серверные импорты содержат .js из-за ESM-профиля. Не заменяйте imports массива декоратора обычным импортом файла: это устранило бы лишь ошибку неизвестного имени, но не обеспечило зависимость контейнера.
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database.js';
import { TicketsController } from './tickets.controller.js';
import { TicketsService } from './tickets.service.js';
import { TicketsRepository } from './tickets.repository.js';
import { DemoAuthGuard, DispatcherGuard } from './auth.js';
@Module({imports:[DatabaseModule],controllers:[TicketsController],providers:[TicketsService,TicketsRepository,DemoAuthGuard,DispatcherGuard]})
export class TicketsModule {}
TicketsController получает TicketsService, тот получает TicketsRepository, а repository получает Database. Nest создаёт эту цепочку из объявленных providers. Controller не требуется вручную вызывать new TicketsService(new TicketsRepository(...)). Такая ручная цепь закрепила бы конкретный способ создания в месте HTTP и затруднила замену зависимости. Модуль удерживает знание о составе, а классы описывают необходимое поведение и тип своих зависимостей.
Ожидается, что один экземпляр Database, предоставленный DatabaseModule, обслуживает операции repository. По умолчанию providers имеют общую область приложения в пределах графа Nest, а не создаются заново на каждый GET. Если объявить Database непосредственно в нескольких feature modules, можно получить несколько владельцев pool. Это увеличит число соединений и изменит lifecycle. Для лаборатории выбрана одна явно экспортированная инфраструктурная зависимость, чтобы число объектов следовало структуре, а не случайным копиям providers.
Export как публичная поверхность
DatabaseModule имеет providers Database и exports Database. Первый массив разрешает контейнеру создать ресурс, второй делает его доступным импортирующему TicketsModule. Необъявленный export не превращается в доступную зависимость только потому, что оба файла расположены в одной папке src. Это полезная граница: внутри инфраструктурного модуля могли бы появиться вспомогательные классы, которые не нужны другим частям приложения. Экспортировать следует средства, которые действительно составляют внешний договор модуля.
TicketsModule не экспортирует свой repository, потому что AppModule лишь подключает HTTP-функцию и не вызывает SQL напрямую. AppModule импортирует модуль заявок как feature. Если позднее появится ReportModule, который нуждается в предметном чтении, можно экспортировать TicketsService либо создать отдельный read API. Прямой экспорт repository ради удобства отчёта закрепил бы детали SQL во втором модуле. Выбор public surface должен отвечать на задачу потребителя, а не на желание сделать все классы видимыми.
ConfigModule предоставлен с isGlobal true в AppModule. Это осознанное исключение для общих настроек запуска: Database и guards читают ConfigService без отдельного повторения ConfigModule в каждом feature. Не стоит по аналогии делать любой предметный модуль global. Такой выбор скрывает зависимости и усложняет понимание, какие части приложение действительно импортирует. В «Диспетчере» глобальными остаются настройки и зарегистрированные cross-cutting providers, а заявки имеют явную связь через imports.
Ошибки графа
Если убрать Database из exports, repository не сможет получить нужный provider через импортированный модуль. Если убрать TicketsRepository из providers, создание service потеряет следующую зависимость. Эти ошибки относятся к graph configuration, а не к неверному HTTP token или отсутствующей записи. Их ожидается обнаружить при создании приложения, до осмысленного GET. Поэтому при диагностике полезно сначала посмотреть, какой класс Nest не смог создать, а затем искать provider и путь его доступности.
Циклический граф также не следует принимать как обычное следствие «большого приложения». Если repository начнёт требовать TicketsService, а service уже требует repository, обязанности перепутаны. В лаборатории направление идёт от транспорта к предметной операции и затем к данным. ForwardRef может существовать для определённых случаев, но он не нужен текущему проекту. Сначала найдите, почему два класса хотят управлять друг другом, и отделите общий механизм либо измените границу ответственности.
Модульная структура не является процессным разделением. TicketsModule и DatabaseModule работают внутри одного Nest-приложения; между ними нет сети или отдельной deployment-жизни. PostgreSQL находится вне процесса, но отдельный модуль Database лишь представляет её клиентский ресурс. Не называйте такой граф микросервисами и не добавляйте очередь для обычного вызова service. На данном этапе прямой вызов метода соответствует требуемому результату и оставляет ошибку операции в понятной HTTP-цепочке.
Для самостоятельного упражнения нарисуйте четыре узла: AppModule, TicketsModule, DatabaseModule и ConfigModule. Затем отдельно нарисуйте зависимости экземпляров controller → service → repository → Database. В первой схеме imports описывает доступность набора providers, во второй constructor — потребность конкретного объекта. Сравнение показывает, почему одинаковое слово «зависимость» используется на двух уровнях. Один массив module imports не заменяет требования конструктора, а требование конструктора не экспортирует provider автоматически.
Следующий урок рассмотрит сами классы service и repository. Сейчас достаточно понимать, кто их предоставляет и где заканчивается публичный договор модуля. Файлы конечной версии уже содержат завершённую конфигурацию; запуск приложения не выполнялся, поэтому создание экземпляров описано как ожидаемое. Текстовая структура обеспечивает воспроизводимый план, но успешный bootstrap с выбранными npm peer dependencies остаётся действием читателя после подготовки среды.
Статус примера — not_run. Зависимости, приложение, SQL, HTTP и тесты не запускались. Описанные ответы и изменения интерфейса ожидаемые; чтение документации не является проверкой выполнением. Полная конечная версия находится в архиве исходников, а её ограничения и действия читателя — в README и CONTRACT.md.
Перейдите к оглавлению серии или общему маршруту интерактивной веб-разработки. Механизм этой главы сверяется с официальным первичным источником; конкретные версии и непроверенные части перечислены в договоре курса.