Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке модулі в NestJS і як вони працюють?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**NestJS модуль** - це TypeScript-клас з `@Module()`, який групує пов'язані контролери та провайдери в скоп для DI. Провайдери приватні за замовчуванням - доступними для інших модулів стають лише ті, що перелічені в `exports`. ```typescript @Module({ controllers: [UsersController], providers: [UsersService], exports: [UsersService] }) export class UsersModule {} ``` **Ключове:** імпорт модуля без `exports` нічого не дає іншим модулям - при старті отримаєш `No provider for X`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**NestJS модуль** - це TypeScript-клас з декоратором `@Module()`, який групує пов'язані контролери, провайдери та підмодулі в одну область видимості для dependency injection (DI). ## Теорія ### TL;DR - Модуль як контейнер: тримає своє (controllers, providers), каже що йому потрібно (`imports`) і що дає назовні (`exports`) - Провайдери приватні за замовчуванням - нічого не витікає без явного `exports` - Один функціональний домен = один модуль (`UsersModule`, `OrdersModule`). `AppModule` тільки імпортує їх - `providers` реєструє клас у DI-скопі модуля; `exports` робить частину провайдерів доступними для імпортерів - Кругові залежності вирішуються через `forwardRef()` ### Швидкий приклад ```typescript // users/users.module.ts @Module({ controllers: [UsersController], // обробляє /users-маршрути providers: [UsersService], // видно тільки всередині цього модуля exports: [UsersService], // тепер інші модулі можуть ін'єктувати це }) export class UsersModule {} // app.module.ts @Module({ imports: [UsersModule], // підтягує те, що UsersModule експортує }) export class AppModule {} ``` При старті NestJS сканує `AppModule`, завантажує `UsersModule` і автоматично підключає `UsersService` до `UsersController`. Ендпоінти `/users` стають активними. Без `exports` - жоден імпортер нічого не отримує. ### Скоп залежностей Провайдер живе тільки всередині модуля, який його оголошує. Якщо `EmailsModule` потрібен `UsersService`, він мусить імпортувати `UsersModule`, а `UsersModule` мусить експортувати `UsersService`. Цей контракт явний і перевіряється при запуску. В звичайному Express ти просто `require()` що завгодно звідки завгодно. Для маленького проекту нормально, для великого - незрозуміло хто що використовує. NestJS робить залежності видимими. ### Типи модулів **Функціональний модуль** (feature module) групує один домен. Він має власні контролери та сервіси, і експортує те, що потрібне іншим модулям. **Спільний модуль** (shared module) тримає утиліти, що використовуються скрізь: `DatabaseService`, `EmailService`, `LoggerService`. Будь-який модуль, що імпортує `SharedModule`, отримує доступ до всього що той експортує. **Глобальний модуль** (з декоратором `@Global()`) реєструє провайдери на рівні всього застосунку - жодному модулю не потрібно явно його імпортувати. Зручно для конфігурації чи логування. Але якщо зловживати, залежності стають невидимими: не зрозуміло з `imports` що модуль насправді використовує. **Динамічний модуль** (dynamic module) приймає конфігурацію під час виконання. `TypeOrmModule.forRoot(options)` - класичний приклад. Статичний метод повертає об'єкт `DynamicModule`, що дозволяє передати параметри підключення або ключі під час імпорту. ### Як будується граф модулів При запуску NestJS читає всі декоратори `@Module()` і будує направлений ациклічний граф (DAG) залежностей. `imports` розгортаються рекурсивно, поки все дерево не відоме. Потім провайдери ініціалізуються через рефлексію TypeScript-метаданих (`reflect-metadata`). IoC-контейнер зіставляє токени `@Inject()` з провайдерами у вирішеному скопі. Якщо токен не знайдено - отримуєш `UnknownInjectionToken` ще до першого запиту. Це навмисно: краще впасти при старті, ніж у три ночі в продакшені. ### Динамічний модуль на практиці ```typescript // config/config.module.ts import { DynamicModule, Module } from '@nestjs/common'; import { ConfigService } from './config.service'; @Module({}) export class ConfigModule { static forRoot(configPath: string): DynamicModule { return { module: ConfigModule, global: true, // доступно всюди без явних імпортів providers: [ { provide: 'CONFIG_PATH', useValue: configPath }, ConfigService, ], exports: [ConfigService], }; } } // app.module.ts @Module({ imports: [ConfigModule.forRoot('prod.env')], }) export class AppModule {} ``` `global: true` робить `ConfigService` доступним всюди. Без нього сервіс доступний тільки модулям, які явно імпортують `ConfigModule`. Обидва підходи працюють - вибір залежить від того наскільки широко потрібен сервіс. ### Типові помилки **Забути `exports`** - найчастіша помилка, особливо після перенесення сервісу в новий модуль: ```typescript // users.module.ts - НЕПРАВИЛЬНО: немає exports @Module({ providers: [UsersService] }) export class UsersModule {} // emails.module.ts імпортує UsersModule, але: constructor(private usersService: UsersService) {} // Error: Nest can't resolve dependencies of EmailsService. // No provider for UsersService! ``` Виправлення: додай `exports: [UsersService]` до `UsersModule`. Самого імпорту модуля недостатньо. **Кругові залежності** виникають коли `UsersModule` імпортує `EmailsModule`, а той імпортує `UsersModule`. NestJS кидає помилку. Рішення через `forwardRef()`: ```typescript // users.module.ts @Module({ imports: [forwardRef(() => EmailsModule)], }) export class UsersModule {} ``` **Складати все в AppModule** - типовий патерн у джунів. 30-50 провайдерів у кореневому модулі означають неможливість ізольованого тестування і болючі рефакторинги. AppModule має тільки імпортувати функціональні модулі, а не оголошувати сервіси напряму. **`global: true` на кожному модулі** - швидке рішення для "мій сервіс не ін'єктується". Працює, але приховує залежності. Правильно: явно експортуй і імпортуй там де потрібно. ### Де зустрічається - `nest new` генерує `AppModule` з імпортом функціональних модулів одразу - `@nestjs/passport` поставляється як `PassportModule` зі стандартним синтаксисом імпорту - Prisma-інтеграція живе в спільному `PrismaModule`, що експортується до всіх функціональних модулів - `TypeOrmModule.forRoot()` у кореневому модулі та `TypeOrmModule.forFeature([Entity])` у кожному функціональному - канонічний приклад динамічного модуля - Мікросервісні транспорти (TCP, Redis, Kafka) використовують ту саму структуру `@Module()` ### Питання на співбесіді **Q:** Яка різниця між `providers` і `exports`? **A:** `providers` реєструє класи в DI-скопі цього модуля. `exports` робить частину з них доступною для інших модулів. Провайдер без `exports` залишається приватним. **Q:** Як NestJS вирішує токени ін'єкції? **A:** За класом (найчастіше) або за рядковим чи символьним токеном з `@Inject('TOKEN')`. NestJS шукає токен у графі модулів і кидає `UnknownInjectionToken` якщо не знаходить. **Q:** Коли варто використовувати `global: true`? **A:** Для сінглтонів, потрібних всюди: конфіг, логування. Для доменних сервісів - ні. Явні імпорти роблять залежності видимими і спрощують відлагодження. **Q:** Як модулі допомагають у тестуванні? **A:** У юніт-тестах створюється `TestingModule`, де реальні імпорти замінюються моками. Межа модуля дозволяє підключати тільки ті залежності, які потрібні тестованому класу. **Q:** Побудуй динамічний модуль з асинхронним завантаженням конфіга через `forRootAsync`. **A:** Використовуй `useFactory` з async-підтримкою у поверненому `DynamicModule`. Фабрика отримує залежності через `inject` і повертає об'єкт конфігурації. NestJS чекає на проміс перед тим як завершити ініціалізацію модуля і почати приймати запити. ## Приклади ### Функціональний модуль: CRUD користувачів ```typescript // users/users.module.ts import { Module } from '@nestjs/common'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; @Module({ controllers: [UsersController], providers: [UsersService], exports: [UsersService], // AuthModule зможе ін'єктувати для перевірки юзерів }) export class UsersModule {} // users/users.service.ts import { Injectable } from '@nestjs/common'; @Injectable() export class UsersService { private users = [{ id: 1, name: 'Alice' }]; findById(id: number) { return this.users.find(u => u.id === id); } } // users/users.controller.ts import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common'; @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Get(':id') getUser(@Param('id', ParseIntPipe) id: number) { return this.usersService.findById(id); // DI підключає сервіс автоматично } } ``` `UsersService` оголошений у `UsersModule` і ін'єктується в `UsersController` без жодного `new UsersService()`. Контролер просто вказує залежність у конструкторі. ### Спільний модуль для доступу до бази даних ```typescript // database/database.module.ts import { Module } from '@nestjs/common'; import { DatabaseService } from './database.service'; @Module({ providers: [DatabaseService], exports: [DatabaseService], // будь-який імпортер отримує доступ }) export class DatabaseModule {} // users/users.module.ts import { Module } from '@nestjs/common'; import { DatabaseModule } from '../database/database.module'; import { UsersService } from './users.service'; import { UsersController } from './users.controller'; @Module({ imports: [DatabaseModule], // підтягує DatabaseService controllers: [UsersController], providers: [UsersService], // тепер UsersService може ін'єктувати DatabaseService }) export class UsersModule {} // users/users.service.ts @Injectable() export class UsersService { constructor(private readonly db: DatabaseService) {} findAll() { return this.db.query('SELECT * FROM users'); } } ``` `DatabaseService` оголошений один раз і використовується в `UsersModule`, `OrdersModule` чи будь-якому іншому модулі що імпортує `DatabaseModule`. Одне визначення - багато споживачів. ### Динамічний модуль з асинхронною фабрикою ```typescript // config/config.module.ts import { DynamicModule, Module } from '@nestjs/common'; import { ConfigService } from './config.service'; @Module({}) export class ConfigModule { static forRootAsync(options: { useFactory: (...args: any[]) => Promise<Record<string, string>>; inject?: any[]; }): DynamicModule { return { module: ConfigModule, global: true, providers: [ { provide: 'CONFIG_OPTIONS', useFactory: options.useFactory, inject: options.inject || [], }, ConfigService, ], exports: [ConfigService], }; } } // app.module.ts @Module({ imports: [ ConfigModule.forRootAsync({ useFactory: async () => ({ DB_HOST: process.env.DB_HOST ?? 'localhost', JWT_SECRET: process.env.JWT_SECRET ?? 'dev-secret', }), }), ], }) export class AppModule {} ``` Фабрика виконується асинхронно до того як застосунок починає приймати запити. `ConfigService` доступний всюди завдяки `global: true`. Саме так працює `@nestjs/config` під капотом.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.