Раздел 8 · Frameworks и Libs

Nest: потрошитель и альтернативы

senior~35 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

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.