Пошук уроків, статей та іншого контенту
Дізнаєтеся, як зберігати й читати метадані декораторів через Reflector у guard, interceptor та інших компонентах.
Метадані — це додаткова інформація, яку можна прикріпити до класу або методу. У NestJS їх часто використовують разом із власними декораторами, щоб передати налаштування до:
guard;
interceptor;
pipe;
middleware;
інших компонентів фреймворку.
Наприклад, декоратор @Roles('admin') може зберегти інформацію про дозволені ролі. Guard прочитає ці метадані й вирішить, чи має користувач доступ до маршруту.
Метадані не змінюють поведінку методу самі по собі. Вони лише зберігають опис або налаштування, які інший компонент повинен прочитати та використати.
SetMetadataДля створення власного декоратора в NestJS використовується SetMetadata:
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);Тепер декоратор можна застосувати до методу або класу:
@Controller('reports')
export class ReportsController {
@Get()
@Roles('admin', 'manager')
getReports() {
return [];
}
}У цьому прикладі до методу getReports буде прикріплено метадані:
ключ: roles
значення: ['admin', 'manager']Ключ метаданих бажано винести в константу. Це зменшує ризик помилок через різне написання рядка в декораторі та guard.
ReflectorReflector — сервіс NestJS для читання метаданих.
Його можна імпортувати з @nestjs/core та використати через dependency injection:
import { Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class ExampleGuard {
constructor(private readonly reflector: Reflector) {}
}Найпростіший спосіб прочитати метадані — метод get:
const roles = this.reflector.get<string[]>(
ROLES_KEY,
context.getHandler(),
);Другий аргумент — об'єкт, до якого були прикріплені метадані. Для HTTP-контексту ним часто є:
context.getHandler() — метод контролера;
context.getClass() — клас контролера.
Guard отримує доступ до поточного handler і класу через ExecutionContext.
const handler = context.getHandler();
const controller = context.getClass();Після цього Reflector може прочитати метадані.
getget читає метадані лише з одного об'єкта:
const roles = this.reflector.get<string[]>(
ROLES_KEY,
context.getHandler(),
);Цей виклик знайде ролі, встановлені безпосередньо на методі. Метадані класу при цьому не враховуються.
getAllAndOverridegetAllAndOverride читає метадані з кількох об'єктів і повертає перше знайдене значення.
const isPublic = this.reflector.getAllAndOverride<boolean>(
IS_PUBLIC_KEY,
[
context.getHandler(),
context.getClass(),
],
);У цьому випадку спочатку перевіряється метод, а потім клас. Якщо метадані є на методі, вони мають пріоритет над метаданими класу.
Це зручно для налаштувань, які можуть бути задані за замовчуванням на класі, але перевизначені для окремого методу.
getAllAndMergegetAllAndMerge об'єднує значення метаданих із кількох джерел.
const roles = this.reflector.getAllAndMerge<string[]>(
ROLES_KEY,
[
context.getHandler(),
context.getClass(),
],
);Цей варіант підходить для масивів. Наприклад, клас може визначити спільну роль, а метод — додаткову.
@Roles('employee')
@Controller('reports')
export class ReportsController {
@Get('financial')
@Roles('accountant')
getFinancialReports() {
return [];
}
}Під час читання через getAllAndMerge можна отримати ролі з класу та методу.
Нижче наведено приклад застосунку, у якому:
@Public() позначає відкритий маршрут;
@Roles() зберігає дозволені ролі;
RolesGuard читає метадані через Reflector;
роль для демонстрації береться з HTTP-заголовка x-role;
AuditInterceptor також читає власні метадані.
import {
CallHandler,
Controller,
ExecutionContext,
Get,
Injectable,
Module,
NestInterceptor,
SetMetadata,
UseGuards,
UseInterceptors,
CanActivate,
} from '@nestjs/common';
import { NestFactory, Reflector } from '@nestjs/core';
import { Observable, tap } from 'rxjs';
const IS_PUBLIC_KEY = 'isPublic';
const ROLES_KEY = 'roles';
const AUDIT_KEY = 'audit';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
export const Roles = (...roles: string[]) =>
SetMetadata(ROLES_KEY, roles);
export const Audit = (eventName: string) =>
SetMetadata(AUDIT_KEY, eventName);
@Injectable()
class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const isPublic = this.reflector.getAllAndOverride<boolean>(
IS_PUBLIC_KEY,
[
context.getHandler(),
context.getClass(),
],
);
if (isPublic) {
return true;
}
const requiredRoles = this.reflector.getAllAndMerge<string[]>(
ROLES_KEY,
[
context.getHandler(),
context.getClass(),
],
);
// Якщо ролі не вказані, маршрут доступний для цього прикладу.
if (requiredRoles.length === 0) {
return true;
}
const request = context
.switchToHttp()
.getRequest<{ headers: Record<string, string | string[] | undefined> }>();
const roleHeader = request.headers['x-role'];
const userRole = Array.isArray(roleHeader)
? roleHeader[0]
: roleHeader;
return typeof userRole === 'string'
&& requiredRoles.includes(userRole);
}
}
@Injectable()
class AuditInterceptor implements NestInterceptor {
constructor(private readonly reflector: Reflector) {}
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
const eventName = this.reflector.getAllAndOverride<string>(
AUDIT_KEY,
[
context.getHandler(),
context.getClass(),
],
);
return next.handle().pipe(
tap(() => {
if (eventName) {
console.log(`Аудит-подія: ${eventName}`);
}
}),
);
}
}
@Controller('reports')
@UseGuards(RolesGuard)
@UseInterceptors(AuditInterceptor)
export class ReportsController {
@Get('public')
@Public()
getPublicReport() {
return {
message: 'Цей звіт доступний усім',
};
}
@Get('internal')
@Roles('employee', 'admin')
@Audit('view_internal_report')
getInternalReport() {
return {
message: 'Цей звіт доступний працівникам та адміністраторам',
};
}
@Get('financial')
@Roles('admin')
@Audit('view_financial_report')
getFinancialReport() {
return {
message: 'Цей звіт доступний лише адміністраторам',
};
}
}
@Module({
controllers: [ReportsController],
providers: [
Reflector,
RolesGuard,
AuditInterceptor,
],
})
export class AppModule {}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();Для перевірки можна виконати запити:
curl http://localhost:3000/reports/publicВідкритий маршрут поверне відповідь без заголовка ролі.
curl http://localhost:3000/reports/internal \
-H "x-role: employee"Цей запит буде дозволено, тому що роль employee є серед метаданих маршруту.
curl http://localhost:3000/reports/financial \
-H "x-role: employee"Цей запит отримає відмову, оскільки маршрут вимагає роль admin.
curl http://localhost:3000/reports/financial \
-H "x-role: admin"Цей запит буде дозволено.
У реальному застосунку роль зазвичай береться з автентифікованого користувача, наприклад із request.user, який додається після перевірки токена. Заголовок у прикладі використано лише для демонстрації роботи метаданих і guard.
Декоратор можна застосувати до всього контролера:
@Roles('employee')
@Controller('reports')
export class ReportsController {
@Get()
getReports() {
return [];
}
}У такому випадку всі методи контролера матимуть метадані roles, якщо вони не перевизначать їх власними декораторами.
@Roles('employee')
@Controller('reports')
export class ReportsController {
@Get()
getReports() {
return [];
}
@Get('financial')
@Roles('admin')
getFinancialReports() {
return [];
}
}Якщо guard використовує getAllAndOverride, для getFinancialReports буде вибрано ['admin'].
Якщо guard використовує getAllAndMerge, значення класу та методу будуть об'єднані. Вибір методу залежить від призначення метаданих:
getAllAndOverride — для значення, яке має перевизначатися;
getAllAndMerge — для списків і наборів налаштувань;
get — коли потрібно перевірити лише конкретний handler або клас.
Interceptor також отримує ExecutionContext, тому працює з метаданими так само, як guard.
const eventName = this.reflector.getAllAndOverride<string>(
AUDIT_KEY,
[
context.getHandler(),
context.getClass(),
],
);Після цього interceptor може:
записати подію в журнал;
змінити формат відповіді;
виконати додаткову логіку до або після handler;
використати значення як налаштування своєї поведінки.
Важливо, що Reflector лише читає метадані. Сам interceptor повинен реалізувати логіку, яка використовує отримане значення.
Під час читання бажано явно вказувати тип:
const roles = this.reflector.get<string[]>(
ROLES_KEY,
context.getHandler(),
);Для простих значень:
const isPublic = this.reflector.get<boolean>(
IS_PUBLIC_KEY,
context.getHandler(),
);Тип не перевіряє дані під час виконання, але допомагає TypeScript і робить код зрозумілішим.
Для складніших метаданих можна створити окремий тип:
type RateLimitOptions = {
limit: number;
windowSeconds: number;
};Після цього декоратор і читання можуть використовувати цей тип:
const RATE_LIMIT_KEY = 'rateLimit';
export const RateLimit = (options: RateLimitOptions) =>
SetMetadata(RATE_LIMIT_KEY, options);const options = this.reflector.get<RateLimitOptions>(
RATE_LIMIT_KEY,
context.getHandler(),
);Reflector.createDecoratorУ сучасних версіях NestJS для деяких сценаріїв можна створити типізований декоратор через Reflector.createDecorator:
import { Reflector } from '@nestjs/core';
export const Roles = Reflector.createDecorator<string[]>();Використання:
@Roles(['admin', 'manager'])
getReports() {
return [];
}Читання:
const roles = this.reflector.get(
Roles,
context.getHandler(),
);Такий підхід пов'язує декоратор безпосередньо з ключем, який передається в Reflector. Для навчальних і прикладних проєктів також широко використовується варіант із SetMetadata, особливо коли декоратор приймає змінну кількість аргументів:
export const Roles = (...roles: string[]) =>
SetMetadata(ROLES_KEY, roles);Головне — використовувати однаковий підхід для створення та читання метаданих.
Метадані методу потрібно читати через context.getHandler(), а метадані класу — через context.getClass().
const methodMetadata = this.reflector.get(
KEY,
context.getHandler(),
);
const classMetadata = this.reflector.get(
KEY,
context.getClass(),
);Якщо читати тільки клас, метадані, встановлені на методі, не будуть знайдені.
get замість getAllAndOverrideЯкщо налаштування може бути встановлене і на класі, і на методі, get не об'єднає ці рівні.
Для пріоритету методу використовуйте:
this.reflector.getAllAndOverride(KEY, [
context.getHandler(),
context.getClass(),
]);У getAllAndOverride порядок важливий:
[
context.getHandler(),
context.getClass(),
]Спочатку слід передавати метод, якщо метод має перевизначати налаштування класу.
undefinedМетадані можуть бути не встановлені:
const value = this.reflector.get<boolean>(
KEY,
context.getHandler(),
);value може дорівнювати undefined, тому потрібно передбачити значення за замовчуванням:
const enabled =
this.reflector.get<boolean>(KEY, context.getHandler()) ?? false;@Roles('admin') не перевіряє користувача й не захищає маршрут самостійно. Декоратор лише зберігає вимогу.
Перевірку повинен виконати guard, який:
отримує дані поточного користувача;
читає необхідні ролі через Reflector;
порівнює їх;
повертає true або false.
Не варто використовувати один ключ для різних типів даних:
SetMetadata('config', ['admin']);
SetMetadata('config', true);Краще створювати окремі константи:
const ROLES_KEY = 'roles';
const IS_PUBLIC_KEY = 'isPublic';Метадані зберігають додаткову інформацію про клас або метод.
SetMetadata використовується для створення власних декораторів.
Reflector читає метадані в guard, interceptor та інших компонентах.
context.getHandler() посилається на метод контролера.
context.getClass() посилається на клас контролера.
get читає метадані з одного об'єкта.
getAllAndOverride повертає перше знайдене значення, зазвичай із пріоритетом методу.
getAllAndMerge об'єднує значення з метаданих класу та методу.
Самі метадані не виконують перевірок — логіку на їх основі реалізує guard, interceptor або інший компонент.