Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке провайдери і як працює dependency injection у NestJS?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Провайдер у NestJS** - це будь-який клас, який IoC контейнер Nest створює і впроваджує за тебе, замість того щоб ти викликав `new` вручну. Контейнер читає типи параметрів конструктора TypeScript через `reflect-metadata`, шукає їх у масиві `providers` модуля і передає закешований екземпляр. Сервіси - найчастіша форма, але `useValue`, `useClass`, `useFactory` і `useExisting` дозволяють зареєструвати провайдером що завгодно. ```typescript @Injectable() export class UsersService { findAll() { return [{ id: 1 }]; } } @Controller('users') export class UsersController { // Nest читає тип і автоматично впроваджує UsersService constructor(private readonly users: UsersService) {} } ``` **Ключове:** провайдери - це рецепти, закешовані за токеном всередині IoC контейнера. За замовчуванням ти отримуєш один екземпляр на застосунок, поки явно не обереш `Scope.REQUEST` або `Scope.TRANSIENT`.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Провайдер у NestJS** - це будь-який клас, який IoC контейнер Nest створює і впроваджує за тебе, замість того щоб ти писав `new` вручну. Сервіси, репозиторії, фабрики, навіть звичайні конфіг-значення - все це може бути провайдером. Контейнер читає типи параметрів конструктора через `reflect-metadata` під час старту, знаходить відповідний запис у масиві `providers` модуля, і передає закешований екземпляр ще до того, як перший запит дійде до контролера. ## Теорія ### TL;DR - Провайдери передають створення об'єктів контейнеру; контролери оголошують що їм треба, а не як це побудувати - Аналогія: IoC контейнер - це кухня, яка приймає замовлення (параметри конструктора) і доставляє готові страви (екземпляри). Контролери ніколи не торкаються інгредієнтів - Головна різниця: `new UsersService()` прив'язує класи один до одного; провайдер розв'язує їх, тому заміна одного сервісу автоматично оновлює всіх споживачів - Три умови: `@Injectable()` на класі, клас у масиві `providers` модуля, і `emitDecoratorMetadata: true` у `tsconfig.json` - Scope за замовчуванням - синглтон на рівні застосунку; `Scope.REQUEST` або `Scope.TRANSIENT` дають per-request або per-injection екземпляри ### Швидкий приклад ```typescript // users.service.ts import { Injectable } from '@nestjs/common'; @Injectable() // позначає клас як провайдер під управлінням контейнера export class UsersService { findAll() { return [{ id: 1, name: 'Alice' }]; } } // users.controller.ts import { Controller, Get } from '@nestjs/common'; import { UsersService } from './users.service'; @Controller('users') export class UsersController { constructor(private readonly users: UsersService) {} // контейнер впроваджує сюди @Get() findAll() { return this.users.findAll(); // new UsersService() ніде немає } } // users.module.ts @Module({ controllers: [UsersController], providers: [UsersService], // реєструємо в контейнері }) export class UsersModule {} ``` Контролер ніколи не викликає `new`. Модуль реєструє `UsersService` як рецепт, контейнер будує його один раз під час bootstrap і передає закешований екземпляр у будь-який конструктор, який оголошує цей тип. ### Чому `new` не підходить Уяви `UsersController`, який викликає `new UsersService()` напряму. Працює один раз. Потім тобі треба замокати сервіс у тестах, поділити один екземпляр між двома контролерами або підмінити його на `MockUsersService` на staging, і кожне місце де написано `new UsersService()` доводиться правити одночасно. DI (впровадження залежностей) вирішує саме цю проблему. Ти оголошуєш що тобі треба, контейнер вирішує як це доставити. У Nest контейнер керує класами, які називаються провайдерами. Це та відповідь, яку очікує інтерв'юер на junior-співбесіді з NestJS - не «клас з `@Injectable`», а пояснення чому `new` тут не підходить і що саме робить контейнер замість тебе. ### Як контейнер визначає тип за конструктором Коли ти пишеш `constructor(private users: UsersService)`, компілятор TypeScript зберігає типи параметрів у скомпільований вихід, звідки їх може прочитати `reflect-metadata`. Nest зчитує цей масив під час завантаження модуля, шукає кожен тип у реєстрі `providers`, і будує граф залежностей знизу вгору - спочатку глибші залежності. Три речі мають бути правдою одночасно, щоб впровадження спрацювало: - На класі є `@Injectable()` - Клас присутній у масиві `providers` якогось модуля, який бачить застосунок - У `tsconfig.json` увімкнено `emitDecoratorMetadata` і `experimentalDecorators` Зламай будь-що з трьох - отримаєш «Nest can't resolve dependencies of X» під час старту. ### Що відбувається під час bootstrap Ось точна послідовність, яку проходить кожен Nest-застосунок коли виконується `NestFactory.create()`: 1. Nest сканує кожен `@Module()` і збирає його `providers`, `controllers` і `imports` 2. Для кожного провайдера Nest читає типи параметрів конструктора через `reflect-metadata` 3. Nest будує направлений ациклічний граф провайдерів і резолвить глибші залежності першими 4. Nest створює один екземпляр кожного провайдера у правильному порядку і кешує його за токеном 5. Коли приходить HTTP-запит, кешований контролер уже тримає кешований сервіс, і хендлер запускається без жодного додаткового wiring Крок 3 - це місце де падають циклічні залежності. Якщо `UsersService` потребує `AuthService`, а `AuthService` потребує `UsersService`, у графі є цикл і контейнер не може вирішити який з них будувати першим. Фікс - `forwardRef(() => OtherService)` з обох сторін. Це говорить контейнеру резолвити посилання ліниво. ### Коли використовувати провайдери - **Спільна бізнес-логіка** - `UsersService`, який використовують кілька контролерів, реєструй як провайдер - **Тестованість** - впроваджуй мок через `useClass` або `useValue` без зміни вихідного коду - **Зовнішні бібліотеки** - обгорни клієнт бази даних у провайдер з `useFactory` для асинхронної ініціалізації - **Конфіг-значення** - `useValue` або `useFactory` провайдери для налаштувань з env-змінних - **Одноразовий розрахунок** - локальна функція підійде; реєструвати провайдер для чогось, що використовується в одному місці, не потрібно ### Custom providers: useValue, useClass, useFactory, useExisting Дев'ять з десяти провайдерів - це просто класи. Десятий потребує custom provider - невеликого об'єкта, який говорить контейнеру як побудувати значення замість того, щоб контейнер просто зробив `new`. Розуміння коли яку форму обирати - це класичний senior follow-up на співбесіді. ```typescript // useValue: передаємо контейнеру вже готове значення { provide: 'APP_CONFIG', useValue: { maxRetries: 3, region: 'eu' } } // useClass: підміняємо один клас іншим з тим самим інтерфейсом { provide: MailerService, useClass: MockMailerService } // useFactory: запускаємо код під час bootstrap (можна async) щоб отримати екземпляр { provide: 'DB_CONNECTION', useFactory: async (config: ConfigService) => createPool(config.get('DB_URL')), inject: [ConfigService], // контейнер резолвить це перед запуском фабрики } // useExisting: псевдонім для провайдера, який вже зареєстровано { provide: 'LOGGER', useExisting: PinoLoggerService } ``` Токен (`provide:`) - це те за чим контейнер ідентифікує провайдера. Для класових провайдерів токен - це сам клас, тому `private users: UsersService` працює без жодних додаткових анотацій. Для рядкових або Symbol-токенів отримуєш значення через `@Inject('DB_CONNECTION') private db: Pool`. Думай про кожну форму як про варіант рецепта: `useValue` - рецепт із вже готовим результатом, `useClass` вказує на інший набір інструкцій, `useFactory` запускає рецепт самостійно, а `useExisting` - псевдонім для рецепта, який вже є в контейнері. ### Scope: синглтон, request, transient Провайдери є синглтонами за замовчуванням. Один екземпляр на застосунок, спільний для всіх контролерів, які його впроваджують. `Scope.REQUEST` створює новий екземпляр для кожного HTTP-запиту. `Scope.TRANSIENT` створює новий екземпляр щоразу, коли щось впроваджує провайдер. Саме через синглтон за замовчуванням два контролери отримують один і той самий об'єкт `UsersService` у пам'яті - користувач, створений через один контролер, одразу видимий іншому без жодних зусиль. Зміни scope - і ця гарантія зникає. Один scope-баг, який регулярно підловлює людей: впровади `Scope.REQUEST` провайдер у default-scoped сервіс, і синглтон захоплює екземпляр першого запиту і тримає його назавжди. На одному Nest 10 проекті ми впровадили request-scoped `TenantContext` у default-scoped `BillingService`, і кожен наступний запит читав дані першого тенанта - зовнішній синглтон ніколи не відпускав перший `TenantContext`. Фікс - зробити весь ланцюжок залежностей request-scoped, а не тільки листовий провайдер. Документація Nest про це попереджає, але більшість знаходить ту сторінку вже після того, як обпеклися. ### Поширені помилки **Забули `@Injectable()`** ```typescript // Неправильно - немає декоратора export class UsersService { findAll() { return []; } } // Nest пропускає цей клас під час резолвингу; контролер отримує undefined ``` Додай `@Injectable()` і контейнер розпізнає клас. **Провайдер зареєстровано в неправильному модулі** ```typescript // Неправильно - UsersService в AuthModule, але UsersController в UsersModule @Module({ providers: [UsersService] }) export class AuthModule {} @Module({ controllers: [UsersController] }) // немає imports: [AuthModule] export class UsersModule {} // Error: Nest can't resolve dependencies of UsersController ``` `UsersModule` або сам оголошує `UsersService` у своєму `providers`, або `AuthModule` має його `export`, а `UsersModule` - `import: [AuthModule]`. Забутий `exports` - це причина номер один чому ця помилка з'являється на свіжому проекті. **Циклічна залежність без forwardRef** ```typescript // UsersService впроваджує AuthService, AuthService впроваджує UsersService // Без forwardRef: bootstrap падає з помилкою circular dependency // З forwardRef: @Injectable() export class UsersService { constructor( @Inject(forwardRef(() => AuthService)) private auth: AuthService ) {} } ``` **Невідповідність scope: request-scoped всередині синглтона** ```typescript // Неправильно: TenantContext має Scope.REQUEST, BillingService - синглтон @Injectable() export class BillingService { constructor(private tenant: TenantContext) {} // захоплює TenantContext першого запиту назавжди } // Правильно: зробити BillingService теж request-scoped @Injectable({ scope: Scope.REQUEST }) export class BillingService { constructor(private tenant: TenantContext) {} // свіжий екземпляр на кожен запит } ``` ### Де провайдери зустрічаються на практиці - **NestJS CLI-застосунки** - кожен згенерований сервіс (`AuthService`, `UsersService`) є провайдером за замовчуванням - **Prisma** - `PrismaService extends PrismaClient` реєструється як провайдер і впроваджується в репозиторії - **TypeORM** - репозиторії сутностей через `@InjectRepository(User)`, що є custom provider під капотом - **BullMQ** - обробники черги як провайдери, впроваджені в контролери або інші сервіси - **ConfigService** - глобальний конфіг-провайдер завантажується один раз під час bootstrap і впроваджується куди завгодно - **Тестування** - `Test.createTestingModule({ providers: [...] }).overrideProvider(UsersService).useValue(mockService)` підміняє реальний сервіс на мок без зміни вихідного коду ### Питання на співбесіді **Q:** Як Nest розуміє який клас впровадити для параметра конструктора? **A:** TypeScript записує типи параметрів у скомпільований вихід коли увімкнено `emitDecoratorMetadata`. Nest читає ці метадані через `reflect-metadata`, зіставляє кожен тип з токеном у реєстрі провайдерів модуля і резолвить екземпляр. Без цього флага кожен параметр виглядає як `Object` і впровадження не спрацьовує. **Q:** Яка різниця між singleton, request і transient scope? **A:** Singleton (за замовчуванням) створює один екземпляр на застосунок, спільний для всіх. `Scope.REQUEST` створює новий екземпляр на кожен HTTP-запит. `Scope.TRANSIENT` - новий екземпляр на кожну точку впровадження. Щоб вийти зі singleton-за-замовчуванням, використовуй `@Injectable({ scope: Scope.REQUEST })`. **Q:** Коли використовувати `useFactory` замість `useClass`? **A:** Коли провайдер потребує асинхронної ініціалізації або аргументів, які конструктор не може отримати з контейнера. Connection pool до бази, Redis-клієнт і фабричні функції на основі конфігу - типові випадки для `useFactory`. `useClass` підходить тільки для синхронної заміни класу. **Q:** Як замокати провайдер у тестах без `jest.mock()`? **A:** Створи `TestingModule` і виклич `.overrideProvider(UsersService).useValue(mockService)` до `.compile()`. Це підміняє провайдер у графі контейнера без впливу на глобальний стан модулів - ізоляція тестів набагато чистіша ніж з module-level мокінгом. **Q:** Чи сервіс і провайдер - це одне і те саме? **A:** Сервіс є провайдером, але провайдер не завжди є сервісом. Репозиторії, фабрики, value bindings і connection pool - все це провайдери в IoC контейнері, хоча жодне з них не підпадає під ярлик «сервіс». Контейнеру байдуже як ти називаєш клас. **Q:** Чи працює DI у NestJS без TypeScript? **A:** Класове впровадження спирається на `emitDecoratorMetadata`, тому чистий JavaScript його не підтримує. Обхідний шлях - custom providers з явними масивами `inject`, які говорять контейнеру що саме передавати, без покладання на відбиті типи. ## Приклади ### Базовий: контролер впроваджує сервіс ```typescript // users.service.ts import { Injectable } from '@nestjs/common'; @Injectable() export class UsersService { private list = ['Alice', 'Bob']; findAll() { return this.list; } findOne(name: string) { return this.list.find(u => u === name) ?? null; } } // users.controller.ts import { Controller, Get, Param } from '@nestjs/common'; import { UsersService } from './users.service'; @Controller('users') export class UsersController { constructor(private readonly users: UsersService) {} @Get() findAll() { return this.users.findAll(); } @Get(':name') findOne(@Param('name') name: string) { return this.users.findOne(name); } } // GET /users -> ["Alice", "Bob"] // GET /users/Alice -> "Alice" // GET /users/Charlie -> null ``` Контролер не знає як побудовано `UsersService`. Він оголошує тип у конструкторі, а контейнер робить усе інше. Жодної фабрики, жодного `new`, жодного ручного wiring. ### Середній: custom provider для впровадження репозиторія Цей патерн зустрічається в e-commerce або SaaS бекендах, де потрібно підміняти шар бази даних на мок у тестах без зміни `AuthService`. ```typescript // users.repository.ts export interface UsersRepository { findByEmail(email: string): Promise<{ id: number; email: string } | null>; } // prisma-users.repository.ts @Injectable() export class PrismaUsersRepository implements UsersRepository { constructor(private prisma: PrismaService) {} async findByEmail(email: string) { return this.prisma.user.findUnique({ where: { email } }); } } // auth.module.ts @Module({ providers: [ AuthService, { provide: 'USER_REPO', // рядковий токен для інтерфейсу useClass: PrismaUsersRepository, // у тестах замінюємо на MockUsersRepository }, ], controllers: [AuthController], }) export class AuthModule {} // auth.service.ts @Injectable() export class AuthService { constructor( @Inject('USER_REPO') private usersRepo: UsersRepository, ) {} async validateUser(email: string) { const user = await this.usersRepo.findByEmail(email); return user ? { id: user.id, email: user.email } : null; } } // validateUser('alice@example.com') -> { id: 1, email: 'alice@example.com' } // validateUser('nobody@example.com') -> null ``` Рядковий токен `'USER_REPO'` відв'язує `AuthService` від конкретного класу. У тестовому модулі замінюй `useClass: PrismaUsersRepository` на `useValue: { findByEmail: jest.fn() }` - сервіс ніколи не дізнається різниці. ### Senior: асинхронний factory provider для пулу з'єднань до бази ```typescript // database.module.ts import { Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Pool } from 'pg'; @Module({ providers: [ { provide: 'DB_POOL', useFactory: async (config: ConfigService): Promise<Pool> => { const pool = new Pool({ connectionString: config.get('DATABASE_URL') }); await pool.connect(); // перевіряємо з'єднання при старті, а не на першому запиті return pool; }, inject: [ConfigService], // контейнер резолвить ConfigService першим, потім запускає фабрику }, ], exports: ['DB_POOL'], }) export class DatabaseModule {} // orders.service.ts import { Inject, Injectable } from '@nestjs/common'; import { Pool } from 'pg'; @Injectable() export class OrdersService { constructor(@Inject('DB_POOL') private db: Pool) {} async findByUser(userId: number) { const { rows } = await this.db.query( 'SELECT * FROM orders WHERE user_id = $1', [userId] ); return rows; } } ``` Фабрика запускається один раз під час `NestFactory.create()`. Якщо DATABASE_URL неправильний або сервер недоступний, застосунок падає при старті - а не тихо ламається на першому запиті. Саме для цього і потрібні async factory providers.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.