Nest: потрошитель и альтернативы
Три урока мы пользовались контейнером как данностью: повесил @Injectable(),
объявил зависимости в конструкторе, контейнер собрал граф. Пора вскрыть, как
именно он это делает. «Магии» там нет, есть reflect-metadata и аккуратная
рекурсия. Поняв механику, ты увидишь и цену этой модели, и где у неё пределы, и
почему в 2026 рядом с Nest живёт целый зоопарк альтернатив с другими
компромиссами.
Финал урока, и всего блока B, честный разговор: когда выходить из Nest и куда.
1. Декоратор, это запись метаданных
Начнём с самого механизма. Декоратор в TypeScript, это функция, которая получает то, к чему её приписали, и может навесить на него метаданные. Сами по себе они ничего не делают: данные лежат, пока кто-то их не прочитает.
import 'reflect-metadata';
const ROLES = Symbol('roles');
// декоратор пишет метку
const Roles = (...roles: string[]): MethodDecorator =>
(target, key) => Reflect.defineMetadata(ROLES, roles, target, key);
class UsersController {
@Roles('admin')
remove() {}
}
// кто-то другой её читает
const meta = Reflect.getMetadata(ROLES, UsersController.prototype, 'remove');
// => ['admin']
Это ровно то, что делали @Roles, @Auth, @SkipValidation в уроках 07-08:
декоратор кладёт метку, guard или pipe её читает через Reflector (тонкая
обёртка над Reflect.getMetadata). Никакого рантайм-поведения у декоратора
нет, только запись в боковое хранилище метаданных, привязанное к классу.
2. Откуда контейнер знает типы зависимостей
Главный вопрос IoC: как контейнер по конструктору
@Injectable()
class UsersService {
constructor(private repo: UsersRepository, private logger: Logger) {}
}
понимает, что нужны именно UsersRepository и Logger? Ответ, в опции
компилятора emitDecoratorMetadata.
При её включении TypeScript для класса с декоратором записывает типы аргументов
конструктора в метаданные под ключом design:paramtypes:
const deps = Reflect.getMetadata('design:paramtypes', UsersService);
// => [UsersRepository, Logger] (ссылки на сами классы, не строки)
Вот почему @Injectable() обязателен даже у класса без других декораторов: он,
триггер эмита метаданных. Без декоратора TypeScript не запишет paramtypes, и
контейнер не увидит зависимостей. И вот почему интерфейс нельзя внедрить по
типу: интерфейс стирается при компиляции, в paramtypes попадёт Object.
Отсюда же @Inject('TOKEN') для нестроковых токенов из урока 06.
3. Свой IoC-контейнер на 60 строк
Соберём то, что Nest делает под капотом: контейнер, который по классу рекурсивно строит граф зависимостей и кэширует синглтоны. Этого хватит, чтобы увидеть всю механику без «магии».
import 'reflect-metadata';
const INJECTABLE = Symbol('injectable');
type Ctor<T = unknown> = new (...args: any[]) => T;
// декоратор-триггер: помечает класс и заставляет TS эмитить paramtypes
export const Injectable = (): ClassDecorator =>
(target) => Reflect.defineMetadata(INJECTABLE, true, target);
export class Container {
private readonly singletons = new Map<Ctor, unknown>();
// карта подмен: интерфейс-токен -> конкретная реализация (как useClass)
private readonly overrides = new Map<Ctor, Ctor>();
bind<T>(token: Ctor<T>, impl: Ctor<T>): void {
this.overrides.set(token, impl);
}
resolve<T>(token: Ctor<T>): T {
const target = (this.overrides.get(token) ?? token) as Ctor<T>;
// синглтон: уже создан, возвращаем тот же инстанс
const cached = this.singletons.get(target);
if (cached) return cached as T;
if (!Reflect.getMetadata(INJECTABLE, target)) {
throw new Error(`${target.name} не помечен @Injectable`);
}
// читаем типы аргументов конструктора и рекурсивно резолвим каждый
const deps: Ctor[] = Reflect.getMetadata('design:paramtypes', target) ?? [];
const args = deps.map((dep) => this.resolve(dep));
const instance = new target(...args);
this.singletons.set(target, instance);
return instance;
}
}
Использование:
@Injectable() class Logger {}
@Injectable() class UsersRepository { constructor(public log: Logger) {} }
@Injectable() class UsersService { constructor(public repo: UsersRepository, public log: Logger) {} }
const container = new Container();
const service = container.resolve(UsersService);
// Logger создан один раз и переиспользован в repo и в service
service.repo.log === service.log; // true
Шестьдесят строк, и это вся суть Nest-контейнера: метка через декоратор,
design:paramtypes через рефлексию, рекурсия по графу, кэш синглтонов, карта
подмен для тестов. Настоящий Nest добавляет поверх модульные границы (видимость
из урока 06), scopes (урок 07), circular-dependency detection, lazy-загрузку и
динамические модули, но фундамент ровно этот. Когда напишешь свой в ДЗ,
«магия» окончательно исчезнет.
4. Цена рефлексии
Теперь видно, чем платим за эту модель.
reflect-metadataв рантайме: полифилл рефлексии должен быть загружен, метаданные занимают память, и они живут в бандле, tree-шейкингу их не выкинуть (контейнер обращается к ним динамически по строковым ключам).- Декораторы не tree-shake-аются: классы и их метаданные удерживаются ссылками из графа, поэтому бандл Nest-приложения объективно крупнее, чем у фреймворка без рефлексии. Для сервера это терпимо, для edge/serverless, где важны холодный старт и размер, болезненно.
- Время старта: контейнер обходит граф и инстанцирует провайдеры при старте (а request-scoped, на каждый запрос, см. урок 07). На больших графах это заметные сотни миллисекунд, плюс к холодному старту.
- Связанность с моделью декораторов: код пропитан декораторами и
предполагает
experimentalDecoratorsплюсemitDecoratorMetadata. Это усложняет переход на другие раннеры и на новый стандарт декораторов ECMAScript (он метаданных типов не эмитит).
Вывод не «Nest медленный», а «у рефлексивного IoC есть постоянная цена, и она не окупается на задачах, где главное, холодный старт и размер». Именно здесь начинаются альтернативы.
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.