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.