Nest: основы и IoC-контейнер
Fastify-трек мы прошли снизу вверх: голый node:http, потом система плагинов,
хуки, схемы, и в финале собрали боевой модульный API руками. Каждый кусок
инфраструктуры мы добавляли сами и видели, из чего он сделан.
Nest подходит к той же задаче с противоположного конца. Это фреймворк с жёсткими конвенциями: он заранее решил, как у тебя будут устроены модули, как сервисы находят друг друга, где живёт валидация и обработка ошибок. Ты не собираешь каркас, ты заполняешь готовый. Цена и выгода этого выбора, это и есть тема блока B.
Сердце Nest, это IoC-контейнер на декораторах. Поймёшь контейнер, поймёшь Nest. Поэтому начинаем не с роутов, а с того, как объекты находят друг друга.
1. Зачем вообще контейнер
Возьмём знакомую тройку слоёв из Fastify-урока 05: контроллер зовёт сервис, сервис зовёт репозиторий. Без контейнера сборка выглядит так:
const repository = new UsersRepository(dataSource);
const service = new UsersService(repository);
const controller = new UsersController(service);
Пока объектов три, это терпимо. Но репозиторию нужен коннект к базе, сервису, кроме репозитория, ещё логгер и почтовый клиент, почтовому клиенту, конфиг. Граф растёт, и руками собирать его становится больно: порядок создания, переиспользование одного коннекта на всех, замена реализации в тестах.
Dependency injection снимает эту боль. Каждый класс лишь объявляет, что ему нужно, а контейнер сам строит граф, создаёт объекты в правильном порядке и переиспользует синглтоны:
@Injectable()
export class UsersService {
// объявили зависимости в конструкторе, не создаём их сами
constructor(
private readonly repository: UsersRepository,
private readonly logger: Logger,
) {}
}
Контейнер видит по типам аргументов конструктора, что UsersService зависит
от UsersRepository и Logger, находит их, создаёт при необходимости и
передаёт. Это и называется инверсией: не сервис тянет зависимости, а контейнер
их вкладывает.
Откуда контейнер знает типы аргументов в рантайме? TypeScript при включённом
emitDecoratorMetadataзаписывает типы параметров конструктора в метаданные черезreflect-metadata. Это та самая рефлексия, которую мы вскроем в уроке 09. Пока достаточно знать, что декоратор@Injectable()помечает класс как участника графа.
2. Три кирпича: модуль, провайдер, контроллер
Nest-приложение собрано из трёх сущностей.
Провайдер (provider), это всё, что контейнер умеет создавать и внедрять:
сервисы, репозитории, фабрики, значения конфига. Помечается @Injectable().
Контроллер (controller), это провайдер с привязкой к HTTP: его методы
становятся обработчиками роутов через декораторы @Get, @Post и так далее.
Модуль (module), это граница. Он перечисляет, какие провайдеры и
контроллеры в него входят, что он импортирует из других модулей и что отдаёт
наружу. Модуль, это аналог области инкапсуляции из Fastify-урока 02: внутри
модуля провайдеры видят друг друга, наружу видно только то, что в exports.
@Module({
imports: [], // другие модули, чьи exports нам нужны
controllers: [UsersController],
providers: [UsersService, UsersRepository],
exports: [UsersService], // что увидят те, кто импортирует UsersModule
})
export class UsersModule {}
Запомни параллель с Fastify, она сквозная для всего блока:
| Fastify | Nest | Что это |
|---|---|---|
| плагин (область) | модуль | граница инкапсуляции |
| декоратор на инстансе | провайдер | переиспользуемая зависимость |
fastify-plugin (слом изоляции) | exports | сделать видимым наружу |
| хук | guard / interceptor / pipe | точка во входящем конвейере |
Разница в том, что в Fastify ты сам решаешь, что положить на инстанс и где
сломать инкапсуляцию через fp. В Nest контейнер решает за тебя по графу
типов, а ты лишь декларируешь связи.
3. Минимальный модуль целиком
Соберём users: контроллер, сервис, простой in-memory репозиторий. Это тот же
домен, что мы делали на Fastify, держим рядом для сравнения.
Репозиторий, обычный провайдер. Пока без базы, чтобы не отвлекаться:
// users.repository.ts
import { Injectable } from '@nestjs/common';
export interface User {
id: number;
email: string;
name: string;
}
@Injectable()
export class UsersRepository {
private readonly users = new Map<number, User>();
private sequence = 0;
create(data: Omit<User, 'id'>): User {
const id = ++this.sequence;
const user = { id, ...data };
this.users.set(id, user);
return user;
}
findById(id: number): User | undefined {
return this.users.get(id);
}
}
Сервис зависит от репозитория. Бизнес-логика и решение, что делать при отсутствии пользователя, живут здесь:
// users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UsersRepository } from './users.repository';
@Injectable()
export class UsersService {
constructor(private readonly repository: UsersRepository) {}
create(dto: CreateUserDto) {
return this.repository.create(dto);
}
findById(id: number) {
const user = this.repository.findById(id);
if (!user) {
throw new NotFoundException(`Пользователь ${id} не найден`);
}
return user;
}
}
Контроллер, тонкий слой HTTP. Он принимает запрос, зовёт сервис, отдаёт результат. Никакой логики, кроме привязки к транспорту:
// users.controller.ts
import { Body, Controller, Get, Param, ParseIntPipe, Post } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly service: UsersService) {}
@Post()
create(@Body() dto: CreateUserDto) {
return this.service.create(dto);
}
@Get(':id')
findById(@Param('id', ParseIntPipe) id: number) {
return this.service.findById(id);
}
}
Обрати внимание на три вещи, которые контейнер делает за тебя:
@Controller('users')задаёт префикс роутов,@Post()и@Get(':id'), методы и пути. Это декларативный аналогapp.post('/users', ...)из Fastify.@Body(),@Param(), это parameter-декораторы: Nest сам достанет тело и параметр пути из запроса.ParseIntPipe, встроенный pipe: превратит строковый:idв число или кинет 400. Pipes, тема урока 07, здесь просто увидь, что они есть.
Возврат значения из метода, это идиома Nest: что вернул, то и уйдёт в тело
ответа как JSON. Бросок NotFoundException превратится в ответ 404. Руками
трогать объект ответа почти никогда не нужно, как и в Fastify, где мы
предпочитали return вместо reply.send.
Связываем всё модулем (как в § 2) и собираем приложение:
// main.ts
import { NestFactory } from '@nestjs/core';
import { UsersModule } from './users/users.module';
const app = await NestFactory.create(UsersModule);
await app.listen(3000);
Меньше явной сборки, чем в Fastify: мы нигде не написали new UsersService(...).
Контейнер прочитал граф из декораторов и собрал его сам.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.