Раздел 8 · Frameworks и Libs

Nest: продвинутый

middle-senior~35 мин

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

Nest: продвинутый

Уроки 06 и 07 дали ядро: контейнер, модули, конвейер запроса, идиому с TaskEither. Теперь, инструменты, из которых собирают настоящий боевой Nest-сервис: динамические модули как способ паковать инфраструктуру, кэш и расписание, аутентификация с ролями, Swagger из декораторов, транспорты микросервисов, CLI на том же контейнере, монорепо и продвинутый TypeORM.

Урок широкий и обзорный по природе: каждая тема, это отдельный пакет @nestjs/*, и глубина у каждого своя. Задача, не вызубрить API, а понять, что все эти куски, это один и тот же приём (динамический модуль плюс провайдеры), надетый на разные задачи, и научиться читать боевую раскладку.

1. Динамические модули как единица упаковки

В уроке 07 мы встретили ConfigModule.forRoot и TypeOrmModule.forRootAsync. Теперь напишем свой и поймём, зачем это главный приём упаковки в Nest.

Динамический модуль, это функция, возвращающая описание модуля. Вот боевой пример: модуль аутентификации, который снаружи подключают одной строкой IamModule.forRoot({ entity: User }), а внутри он собирает всю подсистему:

@Module({})
export class IamModule {
  static forRoot<T extends AuthEntity>(options: IIamModuleOptions<T>): DynamicModule {
    return {
      module: IamModule,
      imports: [
        ConfigModule.forRoot({ load: [jwtConfig, cryptoConfig, redisConfig] }),
        TypeOrmModule.forFeature([options.entity]),
        JwtModule.registerAsync(jwtConfig.asProvider()),
        RedisModule.forRootAsync(redisConfig.asProvider()),
      ],
      providers: [
        // репозиторий нужной сущности через фабрику
        {
          provide: AUTH_REPOSITORY_KEY,
          useFactory: (opts, ds: DataSource) => ds.getRepository(opts.entity),
          inject: [IAM_MODULE_OPTIONS_KEY, DataSource],
        },
        { provide: IAM_MODULE_OPTIONS_KEY, useValue: options },
        // несколько guard-ов стопкой через APP_GUARD
        { provide: APP_GUARD, useClass: AuthenticationGuard },
        { provide: APP_GUARD, useClass: RolesGuard },
        AuthenticationService, TokenService, RefreshTokenIdsStorage,
      ],
      exports: [AuthenticationService],
    };
  }
}

Что здесь видно сразу четыре приёма урока 06-07 в одном месте: фабрика-провайдер (репозиторий по сущности из опций), useValue для опций модуля, стопка глобальных guard через APP_GUARD, композиция чужих динамических модулей внутри своего. Это и есть способ масштабировать Nest: не размазывать инфраструктуру по AppModule, а паковать в самодостаточные настраиваемые модули. Соглашение по именам: forRoot/forRootAsync, для глобальной настройки, register/registerAsync, для локальной.

2. Конфиг типобезопасно

Подсистемы конфигурируются, и в проде конфиг хочется и валидировать на старте, и внедрять типизированно. Идиома, registerAs плюс класс-валидатор окружения:

// jwt.config.ts
export class JWTEnvironmentVariables {
  @IsString() JWT_SECRET: string;
  @IsInt() @IsPositive() JWT_TOKEN_EXPIRES_IN: number;
}

export const jwtConfig = registerAs('jwt', () => ({
  secret: process.env.JWT_SECRET,
  accessTokenTtl: Number.parseInt(process.env.JWT_TOKEN_EXPIRES_IN ?? '3600', 10),
}));

export type JWTConfiguration = ConfigType<typeof jwtConfig>;
export const InjectJWTConfig = () => Inject(jwtConfig.KEY);

registerAs('jwt', ...), регистрирует именованный кусок конфига. ConfigType выводит его тип. InjectJWTConfig, аккуратный декоратор-обёртка над @Inject(jwtConfig.KEY). На старте ConfigModule прогоняет validate(JWTEnvironmentVariables): plainToInstance плюс validateSync, если переменной нет или она кривая, приложение падает сразу, а не на первом запросе. Это та же мысль, что и конфиг в самой BatSchool: переменные окружения валидируются на старте через zod, не runtime-сюрприз.

3. Auth: JWT, роли, refresh-rotation

Аутентификация, классический повод собрать guard-конвейер. Боевая раскладка разделяет два вопроса: кто ты (authentication) и что тебе можно (authorization).

Декоратор-метка на роуте говорит, какой способ входа нужен:

@Auth(AuthType.Bearer) // метка через SetMetadata(AUTH_TYPE_KEY, [...])
@Role('admin')         // метка через SetMetadata(ROLES_KEY, [...])
@Controller('admin/clients')
export class AdminClientsController {}

Глобальный AuthenticationGuard читает метку и выбирает стратегию проверки токена; затем RolesGuard проверяет роль. Оба читают метаданные через Reflector.getAllAndOverride, который смотрит и на метод, и на класс (метка с метода переопределяет метку класса):

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext) {
    const roles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!roles) return true; // нет метки, роут публичный
    const { user } = context.switchToHttp().getRequest();
    return roles.includes(user.role);
  }
}

JWT-часть стоит на @nestjs/jwt (подпись и проверка токена) и обычно @nestjs/passport со стратегиями (passport-jwt, OAuth2-провайдеры соцсетей). Refresh-token rotation, важная деталь безопасности: access-token короткоживущий, refresh обменивается на новую пару и старый сразу инвалидируется (хранилище потраченных id обычно в Redis). Модели доступа: RBAC (по ролям), ABAC (по атрибутам), PBAC (по политикам), от простого к гибкому. Глубже про identity, в разделе 24 (микросервисы) и в проекте раздела 25.

4. Кэш и расписание

Две частые подсистемы, обе, динамические модули из § 1.

CacheModule, кэш с разными хранилищами (in-memory по умолчанию, Redis в проде). Вешается interceptor-ом на чтение:

@Module({ imports: [CacheModule.register({ ttl: 30_000 })] })
export class AppModule {}

@UseInterceptors(CacheInterceptor) // кэшировать GET-ответы контроллера
@Controller('articles')
export class ArticlesController {}

ScheduleModule, периодические задачи. @Cron, @Interval, @Timeout:

@Injectable()
export class ReportsJob {
  @Cron('0 3 * * *') // каждый день в 03:00
  async daily() {
    // собрать ночной отчёт
  }
}

Важная граница: @Cron живёт в процессе приложения и при нескольких репликах сработает на каждой. Для распределённых, идемпотентных задач берут очередь с repeat-опцией (BullMQ), это тема урока 11. @Cron хорош для одиночного инстанса и лёгких задач.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.