Модули, контроллеры, провайдеры
NestJS — это три блока: controller-ы маршрутизируют запросы и остаются тонкими, provider-ы держат бизнес-логику и инжектятся, module-и группируют их и управляют видимостью через imports/exports. Забыл export — и DI не может разрешить зависимость.
Приложение стартует, потом умирает на первом же запросе с Nest can't resolve dependencies of the UsersController (?). Please make sure that the argument UsersService at index [0] is available in the UsersModule context. Сервис существует. Он @Injectable(). Он даже перечислен в providers — но у auth-модуля, который его так и не экспортировал. Зависимость была прямо тут, в соседнем module, но невидима. Эта ошибка — самый частый обряд посвящения в Nest, и это не баг в твоём коде. Это фреймворк сообщает, что ты нарушил границу, о существовании которой не знал.
Контроллеры: тонкие обработчики запросов
Controller — это HTTP-фасад приложения. Декоратор @Controller('users') объявляет префикс route; декораторы методов вроде @Get(), @Post(), @Patch(':id'), @Delete(':id') сопоставляют обработчики с глаголами и под-путями. Параметрические декораторы достают вход из запроса: @Param('id') — сегменты пути, @Query() — query-строку, @Body() — распарсенный JSON-пейлоад. Что бы обработчик ни вернул, становится телом ответа, сериализованным за тебя.
Дисциплина, отделяющая junior-controller от senior-варианта, — тонкость. Controller должен переводить HTTP в вызов метода и переводить результат обратно в HTTP — и не более. Валидация, бизнес-правила, персистентность, оркестрация — всё это принадлежит provider-у. Когда controller начинает гонять запросы к базе или ветвиться на бизнес-состоянии, он перестаёт быть адаптером и становится god-объектом, который нельзя переиспользовать вне HTTP и нельзя протестировать без поднятия всего пайплайна запроса.
import { Controller, Get, Post, Param, Body } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UsersController {
// Сервис инжектится, а не конструируется. Controller никогда не делает new.
constructor(private readonly usersService: UsersService) {}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id); // делегируй, верни, всё
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}Провайдеры: логика, которую инжектят
Зачем не класть бизнес-логику прямо в controller? Потому что тогда ты теряешь возможность тестировать её без HTTP, переиспользовать из consumer очереди и подменять в тестах на фейк. Injectable provider-ы решают все три проблемы сразу.
Provider — это любой класс, который Nest может инстанцировать и выдать тому, кто его просит. Декоратор @Injectable() помечает класс как кандидата для контейнера dependency injection (DI). Самый частый provider — это сервис с бизнес-логикой, но репозитории, фабрики, HTTP-клиенты и config-объекты тоже provider-ы. Определяющая черта в том, что ты никогда не вызываешь new UsersService() сам — ты объявляешь зависимость в конструкторе, а Nest разрешает и поставляет её.
Именно это делает controller выше тестируемым: в unit-тесте ты инстанцируешь UsersController с фейковым UsersService, без HTTP и без базы. DI-контейнер читает типы параметров конструктора, находит подходящий provider в области видимости и инжектит единственный общий инстанс (provider-ы по умолчанию синглтоны в своей области).
import { Injectable, NotFoundException } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly users = new Map<string, { id: string; name: string }>();
findOne(id: string) {
const user = this.users.get(id);
if (!user) throw new NotFoundException(`User ${id} not found`);
return user;
}
create(dto: { name: string }) {
const id = crypto.randomUUID();
const user = { id, name: dto.name };
this.users.set(id, user);
return user;
}
}Модули: группировка и граница инкапсуляции
Module — это организующая единица. Декоратор @Module() принимает четыре массива: controllers (объявленные здесь), providers (инстанцируемые и доступные внутри этого module), imports (другие module-и, чьи exports этот module хочет использовать) и exports (то подмножество provider-ов этого module, которое другим module-ам разрешено потреблять). У каждого Nest-приложения есть корневой AppModule; всё остальное — feature-модули, которые ты в него ввязываешь.
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // <-- без этого ни один другой module не сможет инжектить UsersService
})
export class UsersModule {}Вот правило, которое нарушил хук, прямым текстом: provider приватен для своего module, если он не указан в exports. Объявление UsersService в providers делает его инжектируемым в UsersController внутри UsersModule — и больше нигде. Если OrdersModule хочет UsersService, должны быть истинны две вещи: UsersModule обязан export его, а OrdersModule обязан import UsersModule. Промахнись хоть в одной половине — и получишь ошибку Nest can't resolve dependencies. Фикс — никогда не пере-перечислять provider в потребляющем module (это создаёт второй, отдельный инстанс и тихо ломает общее состояние) — а экспортировать из владельца и импортировать владельца.
▸Почему это работает
Почему Nest принудительно делает приватность по умолчанию, а не делает каждый provider глобальным? Потому что инкапсуляция — это весь смысл module-ей. Будь каждый provider виден везде, граф зависимостей превратился бы в плоский ком грязи, и любой module мог бы залезть во внутренности любого другого. Явные exports превращают публичную поверхность module в осознанный, проверяемый контракт: ты с одного взгляда видишь, что именно фича предлагает остальному приложению, а рефакторинг приватного provider-а никогда не сломает далёкого потребителя, потому что их нет.
Это даёт тебе два вида module-ей, которые ты будешь строить постоянно. Feature-модуль владеет одним срезом домена (users, orders, billing) и экспортирует только то, что другим действительно нужно. Shared-модуль упаковывает сквозные provider-ы — соединение с базой, логгер, config-сервис — и экспортирует их, чтобы многие feature-модули импортировали одну и ту же обвязку. Когда shared-provider нужен почти везде, можно пометить его module как @Global(), но это осознанный аварийный выход, а не дефолт: глобализировать всё — значит выбросить ту инкапсуляцию, ради которой ты пришёл.
| Блок | Ответственность | Декоратор | Содержит / объявляет |
|---|---|---|---|
| Controller | Маршрутизировать route, доставать вход, возвращать ответы — оставаться тонким, делегировать логику | @Controller(‘users’) | @Get/@Post обработчики; @Param/@Query/@Body параметры |
| Provider | Держать бизнес-логику; инжектится через DI, по умолчанию синглтон | @Injectable() | Сервисы, репозитории, фабрики — методы, которые вызывают другие классы |
| Module | Группировать связанные блоки; управлять видимостью через границы module-ей | @Module({…}) | controllers, providers, imports, exports |
OrdersService нужен UsersService, который живёт в UsersModule. UsersService есть в providers у UsersModule. Что заставит инъекцию разрешиться?
Новый эндпоинт должен провалидировать пейлоад, применить бизнес-правило и сохранить запись. Где этой логике место?
- 01Проведи запрос через три блока и скажи, за что отвечает каждый.
- 02Ты ловишь «Nest can't resolve dependencies of UsersController — UsersService at index [0]». UsersService существует и он @Injectable(). Что не так и как починить правильно?
NestJS строит бэкенд из трёх блоков. Controller-ы — это HTTP-край: @Controller('users') задаёт префикс route, декораторы методов вроде @Get(':id') и @Post() сопоставляют обработчики, а @Param/@Query/@Body достают вход — и они должны оставаться тонкими, переводя HTTP в вызов метода и обратно, делегируя реальную работу в другое место. Provider-ы — это где живёт та работа: любой класс @Injectable() — сервис, репозиторий, фабрика — который DI-контейнер инстанцирует и поставляет через параметры конструктора, никогда не конструируется руками, что и делает систему тестируемой и держит provider-ы синглтонами в области видимости. Module-и — организующая единица: @Module() группирует controllers и providers, втягивает другие module-и через imports и открывает выбранное подмножество через exports. Правило инкапсуляции — то самое, что кусает: provider приватен для своего module, пока не экспортирован, поэтому межмодульная зависимость разрешается, только когда владелец экспортирует её, а потребитель импортирует владельца. Пере-перечисление в providers потребителя глушит ошибку, но плодит второй инстанс и ломает общее состояние — всегда экспортируй из владельца и импортируй владельца. Feature-модули владеют срезом домена и экспортируют скупо; shared-модули упаковывают сквозные provider-ы; @Global() — осознанное исключение, а не дефолт. Теперь, когда встретишь Nest can't resolve dependencies, первым делом проверяй exports — фикс это одна строка в module-владельце, а не дублирование provider-а в потребителе.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.