Сервисы и DI
Что такое сервис
Сервис — класс с бизнес-логикой. Не знает про HTTP. Используется контроллерами и другими сервисами.
Создание
nest g service users
Что создаст:
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {}
@Injectable()
Декоратор — помечает класс как провайдер, который NestJS может внедрять.
Без него — DI не сработает.
Базовый сервис
import { Injectable } from '@nestjs/common';
export interface User {
id: number;
name: string;
email: string;
}
@Injectable()
export class UsersService {
private users: User[] = [];
findAll(): User[] {
return this.users;
}
findOne(id: number): User | undefined {
return this.users.find(u => u.id === id);
}
create(data: Omit<User, 'id'>): User {
const user = { id: this.users.length + 1, ...data };
this.users.push(user);
return user;
}
update(id: number, data: Partial<User>): User {
const user = this.findOne(id);
if (!user) throw new Error('Not found');
Object.assign(user, data);
return user;
}
remove(id: number): void {
this.users = this.users.filter(u => u.id !== id);
}
}
Использование в контроллере
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
}
NestJS автоматически создаст UsersService и передаст в конструктор.
Как работает DI
Dependency Injection — передача зависимостей через конструктор.
NestJS IoC-контейнер:
- Сканирует модули.
- Регистрирует провайдеры.
- Резолвит зависимости.
- Создаёт граф.
- Инстанцирует в правильном порядке.
Инъекция сервис в сервис
orders.service.ts:
@Injectable()
export class OrdersService {
constructor(private readonly usersService: UsersService) {}
findAll() {
return [{
id: 1,
user: this.usersService.findOne(1),
}];
}
}
Сервис использует другой сервис. Нужно, чтобы UsersService был экспортирован из своего модуля.
Scope провайдеров
Три scope:
- DEFAULT — singleton, один на всё приложение.
- REQUEST — новый на каждый запрос.
- TRANSIENT — новый на каждый инжект.
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {}
Правило: по умолчанию — DEFAULT. REQUEST — если нужен доступ к req.
Опциональная зависимость
constructor(
@Optional() private readonly logger?: LoggerService,
) {}
@Optional() — не ошибка, если не найден.
Кастомный провайдер
useClass:
@Module({
providers: [
{
provide: LoggerService,
useClass: process.env.NODE_ENV === 'production'
? FileLogger
: ConsoleLogger,
},
],
})
useValue:
@Module({
providers: [
{
provide: 'API_KEY',
useValue: 'secret-key',
},
],
})
Инъекция:
constructor(@Inject('API_KEY') private apiKey: string) {}
useFactory:
@Module({
providers: [
{
provide: 'DATABASE',
useFactory: async (config: ConfigService) => {
return createConnection(config.get('DB_URL'));
},
inject: [ConfigService],
},
],
})
Асинхронный провайдер
{
provide: 'CONNECTION',
useFactory: async () => {
const conn = await connect();
return conn;
},
}
NestJS дождётся промиса.
Абстракция через интерфейс
Правильный подход — интерфейс + токен:
// payment.service.ts
export interface PaymentService {
charge(amount: number): Promise<boolean>;
}
export const PAYMENT_SERVICE = 'PAYMENT_SERVICE';
Реализации:
@Injectable()
export class StripeService implements PaymentService {
async charge(amount: number): Promise<boolean> {
return true;
}
}
Провайдер:
@Module({
providers: [
{
provide: PAYMENT_SERVICE,
useClass: StripeService,
},
],
})
Инъекция:
constructor(
@Inject(PAYMENT_SERVICE)
private readonly payment: PaymentService,
) {}
Теперь легко подменить в тестах.
Repository pattern
Сервис + репозиторий:
@Injectable()
export class UsersService {
constructor(private readonly usersRepository: UsersRepository) {}
async findAll() {
return this.usersRepository.findAll();
}
}
@Injectable()
export class UsersRepository {
constructor(
@InjectRepository(User) private repo: Repository<User>,
) {}
async findAll() {
return this.repo.find();
}
}
Разделение: сервис — логика, репозиторий — БД.
Общие сервисы
Global module:
@Global()
@Module({
providers: [LoggerService],
exports: [LoggerService],
})
export class LoggerModule {}
Подключить один раз в AppModule.
Тестирование сервиса
Unit-тест:
import { Test } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should create user', () => {
const user = service.create({ name: 'Иван', email: 'ivan@example.com' });
expect(user.id).toBe(1);
expect(user.name).toBe('Иван');
});
});
Мок зависимости
describe('UsersService', () => {
let service: UsersService;
let repo: UsersRepository;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
UsersService,
{
provide: UsersRepository,
useValue: {
findAll: jest.fn().mockResolvedValue([]),
findOne: jest.fn(),
},
},
],
}).compile();
service = module.get(UsersService);
repo = module.get(UsersRepository);
});
it('should call repo', async () => {
await service.findAll();
expect(repo.findAll).toHaveBeenCalled();
});
});
Практика: сервис с логгером
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
private users: User[] = [];
create(data: Omit<User, 'id'>): User {
const user = { id: this.users.length + 1, ...data };
this.users.push(user);
this.logger.log(`Создан пользователь ${user.id}`);
return user;
}
findOne(id: number): User | undefined {
this.logger.debug(`Поиск пользователя ${id}`);
return this.users.find(u => u.id === id);
}
}
Logger — встроенный в NestJS.
Порядок инициализации
OnModuleInit:
@Injectable()
export class UsersService implements OnModuleInit {
onModuleInit() {
console.log('UsersService готов');
}
}
Хуки жизненного цикла:
OnModuleInit— после инициализации.OnModuleDestroy— при завершении.OnApplicationBootstrap— после bootstrap.OnApplicationShutdown— при shutdown.
Итог
@Injectable()— провайдер.- Constructor injection — основной способ.
@Inject('token')— для не-классов.- Scope: DEFAULT, REQUEST, TRANSIENT.
useClass,useValue,useFactory— кастомные.- Интерфейс + токен — для абстракций.
- Repository — доступ к БД.
Test.createTestingModule— тесты.Logger— встроенный.
Дальше — DTO и валидация.