open atlas
↑ К треку
NestJS с нуля до senior NEST · 01 · 01

Модули, контроллеры, провайдеры

NestJS — это три блока: controller-ы маршрутизируют запросы и остаются тонкими, provider-ы держат бизнес-логику и инжектятся, module-и группируют их и управляют видимостью через imports/exports. Забыл export — и DI не может разрешить зависимость.

NEST Middle ◷ 16 min
Уровень
ОсновыJuniorMiddleSenior

Приложение стартует, потом умирает на первом же запросе с 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. Что заставит инъекцию разрешиться?

Выбери лучший вариант

Новый эндпоинт должен провалидировать пейлоад, применить бизнес-правило и сохранить запись. Где этой логике место?

Вспомните перед уходом
  1. 01
    Проведи запрос через три блока и скажи, за что отвечает каждый.
  2. 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 8 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.