Пошук уроків, статей та іншого контенту
Створите параметричні, методи й класи-декоратори та застосуєте їх для повторного використання метаданих і логіки.
Декоратор — це функція, яка додає метадані або змінює поведінку класу, методу, властивості чи параметра.
У NestJS декоратори використовуються для:
опису маршрутів (@Get(), @Post());
роботи з залежностями (@Injectable());
доступу до параметрів HTTP-запиту (@Body(), @Param());
зберігання метаданих;
повторного використання логіки в контролерах, сервісах і guard'ах.
Власні декоратори корисні, коли одна й та сама конструкція повторюється в різних частинах застосунку.
Наприклад:
отримання поточного користувача з request.user;
додавання ролей до метаданих маршруту;
вимірювання часу виконання методу;
опис функціональної області контролера;
об'єднання кількох стандартних декораторів в один.
У NestJS найчастіше створюють такі декоратори:
параметричні — застосовуються до параметра методу;
методи-декоратори — застосовуються до методу класу;
клас-декоратори — застосовуються до класу;
декоратори метаданих — зберігають інформацію для guard'ів, interceptor'ів або інших компонентів.
Декоратор сам по собі не завжди виконує бізнес-логіку. Часто він лише записує метадані, а інший компонент їх читає та приймає рішення.
createParamDecoratorNestJS надає фабрику createParamDecorator, за допомогою якої можна створити власний параметричний декоратор.
Фабрика отримує функцію з двома аргументами:
data — додаткове значення, передане в декоратор;
ExecutionContext — контекст поточного HTTP-запиту.
Розглянемо декоратор @CurrentUser(), який отримує користувача з request.user.
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
type User = {
id: number;
email: string;
role: 'user' | 'admin';
};
export const CurrentUser = createParamDecorator(
(field: keyof User | undefined, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest<{
user?: User;
}>();
if (field === undefined) {
return request.user;
}
return request.user?.[field];
},
);Тепер декоратор можна застосувати в контролері:
import { Controller, Get } from '@nestjs/common';
import { CurrentUser } from './current-user.decorator';
@Controller('profile')
export class ProfileController {
@Get()
getProfile(@CurrentUser() user: unknown) {
return user;
}
@Get('id')
getUserId(@CurrentUser('id') userId: number | undefined) {
return {
userId,
};
}
}Виклики декоратора:
@CurrentUser()повертають весь об'єкт користувача, а:
@CurrentUser('id')повертають лише поле id.
Це зменшує залежність контролера від структури HTTP-запиту. Метод більше не працює безпосередньо з request.
ExecutionContextExecutionContext містить інформацію про поточний виклик. Для HTTP-контексту використовується:
context.switchToHttp().getRequest()Також можна отримати відповідь:
context.switchToHttp().getResponse()Параметричний декоратор може працювати не лише з HTTP. За потреби контекст можна перемкнути на інший транспорт, наприклад WebSocket або RPC.
SetMetadataSetMetadata зберігає значення під певним ключем. Інший компонент NestJS може прочитати це значення через Reflector.
Наприклад, створимо декоратор @Roles():
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = Symbol('roles');
export type Role = 'user' | 'admin' | 'manager';
export const Roles = (...roles: Role[]) => {
return SetMetadata(ROLES_KEY, roles);
};Застосування:
import { Controller, Get } from '@nestjs/common';
import { Roles } from './roles.decorator';
@Controller('reports')
export class ReportsController {
@Get()
@Roles('admin', 'manager')
getReports() {
return {
reports: [],
};
}
}У цьому прикладі @Roles() лише записує метадані. Він не перевіряє права користувача.
ReflectorЩоб використати метадані, їх потрібно прочитати, наприклад, у guard'і:
import {
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY, Role } from './roles.decorator';
type RequestUser = {
role: Role;
};
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
const request = context.switchToHttp().getRequest<{
user?: RequestUser;
}>();
const currentRole = request.user?.role;
return currentRole !== undefined && requiredRoles.includes(currentRole);
}
}getAllAndOverride перевіряє метадані методу та класу. Якщо метадані визначені на рівні методу, вони мають пріоритет над метаданими класу.
Guard можна застосувати до контролера:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';
@Controller('reports')
@UseGuards(RolesGuard)
export class ReportsController {
@Get()
@Roles('admin', 'manager')
getReports() {
return {
reports: [],
};
}
}Важливо, щоб до виконання цього guard'а інший компонент додавав користувача до request.user, наприклад після автентифікації.
Метадані можна додавати не лише до методів, а й до класів:
import { SetMetadata } from '@nestjs/common';
export const FEATURE_KEY = Symbol('feature');
export const Feature = (name: string): ClassDecorator => {
return (target) => {
SetMetadata(FEATURE_KEY, name)(target);
};
};Застосування:
import { Controller } from '@nestjs/common';
import { Feature } from './feature.decorator';
@Controller('orders')
@Feature('orders')
export class OrdersController {}Доступ до метаданих класу:
import {
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { FEATURE_KEY } from './feature.decorator';
@Injectable()
export class FeatureGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const featureName = this.reflector.get<string>(
FEATURE_KEY,
context.getClass(),
);
if (featureName) {
console.log(`Обробляється функціональна область: ${featureName}`);
}
return true;
}
}Клас-декоратор зручно використовувати для спільної конфігурації всіх методів контролера.
Метод-декоратор отримує:
клас, якому належить метод;
ім'я методу;
дескриптор методу.
Через дескриптор можна прочитати або замінити оригінальну функцію.
Створимо декоратор, який вимірює час виконання асинхронного методу:
export function LogExecutionTime(): MethodDecorator {
return (
_target,
propertyKey,
descriptor: PropertyDescriptor,
): void => {
const originalMethod = descriptor.value as (
...args: unknown[]
) => unknown;
descriptor.value = async function (...args: unknown[]) {
const startedAt = Date.now();
try {
return await originalMethod.apply(this, args);
} finally {
const elapsedTime = Date.now() - startedAt;
console.log(
`Метод ${String(propertyKey)} виконано за ${elapsedTime} мс`,
);
}
};
};
}Застосування в контролері:
import { Controller, Get } from '@nestjs/common';
import { LogExecutionTime } from './log-execution-time.decorator';
@Controller('orders')
export class OrdersController {
@Get()
@LogExecutionTime()
async getOrders() {
await new Promise((resolve) => setTimeout(resolve, 50));
return {
orders: [],
};
}
}Декоратор зберігає оригінальний метод, замінює його обгорткою та викликає оригінальну реалізацію через apply.
Використання finally гарантує, що час буде записано навіть у разі помилки.
Обгортка повинна:
передати всі аргументи в оригінальний метод;
зберегти контекст this;
повернути результат;
коректно обробляти помилки;
не змінювати поведінку методу без необхідності.
Саме тому використовується:
originalMethod.apply(this, args)а не простий виклик:
originalMethod(args);Якщо на одному методі постійно використовуються кілька декораторів, їх можна об'єднати через applyDecorators.
import {
applyDecorators,
UseGuards,
} from '@nestjs/common';
import { Roles, Role } from './roles.decorator';
import { RolesGuard } from './roles.guard';
export function AdminOnly(): MethodDecorator {
return applyDecorators(
Roles('admin' as Role),
UseGuards(RolesGuard),
);
}Тепер замість:
@Roles('admin')
@UseGuards(RolesGuard)
@Get()
getAdminData() {
return {
secret: true,
};
}можна написати:
@AdminOnly()
@Get()
getAdminData() {
return {
secret: true,
};
}applyDecorators не змінює логіку окремих декораторів. Він лише дозволяє представити їх як один повторно використовуваний декоратор.
Нижче наведено приклад, у якому використовуються:
параметричний декоратор @CurrentUser;
декоратор метаданих @Roles;
клас-декоратор @Feature;
метод-декоратор @LogExecutionTime;
Reflector для читання метаданих.
import {
CanActivate,
Controller,
createParamDecorator,
ExecutionContext,
Get,
Injectable,
SetMetadata,
UseGuards,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
type User = {
id: number;
email: string;
role: 'user' | 'admin';
};
type Role = User['role'];
const ROLES_KEY = Symbol('roles');
const FEATURE_KEY = Symbol('feature');
export const CurrentUser = createParamDecorator(
(field: keyof User | undefined, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest<{
user?: User;
}>();
if (field === undefined) {
return request.user;
}
return request.user?.[field];
},
);
export const Roles = (...roles: Role[]) => {
return SetMetadata(ROLES_KEY, roles);
};
export const Feature = (name: string): ClassDecorator => {
return (target) => {
SetMetadata(FEATURE_KEY, name)(target);
};
};
export function LogExecutionTime(): MethodDecorator {
return (
_target,
propertyKey,
descriptor: PropertyDescriptor,
): void => {
const originalMethod = descriptor.value as (
...args: unknown[]
) => unknown;
descriptor.value = async function (...args: unknown[]) {
const startedAt = Date.now();
try {
return await originalMethod.apply(this, args);
} finally {
const elapsedTime = Date.now() - startedAt;
console.log(
`Метод ${String(propertyKey)} виконано за ${elapsedTime} мс`,
);
}
};
};
}
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
const request = context.switchToHttp().getRequest<{
user?: User;
}>();
const currentRole = request.user?.role;
return currentRole !== undefined && requiredRoles.includes(currentRole);
}
}
@Injectable()
export class FeatureGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const featureName = this.reflector.get<string>(
FEATURE_KEY,
context.getClass(),
);
if (featureName) {
console.log(`Функціональна область: ${featureName}`);
}
return true;
}
}
@Controller('orders')
@Feature('orders')
@UseGuards(FeatureGuard, RolesGuard)
export class OrdersController {
@Get()
@Roles('admin')
@LogExecutionTime()
async getOrders(@CurrentUser('id') userId: number | undefined) {
return {
userId: userId ?? null,
orders: [],
};
}
@Get('profile')
@LogExecutionTime()
async getProfile(@CurrentUser() user: User | undefined) {
return {
user,
};
}
}Для використання FeatureGuard і RolesGuard їх потрібно додати до провайдера відповідного модуля:
import { Module } from '@nestjs/common';
@Module({
controllers: [OrdersController],
providers: [FeatureGuard, RolesGuard],
})
export class OrdersModule {}У реальному застосунку request.user має бути заповнений до виконання RolesGuard. Сам декоратор @CurrentUser лише читає це значення.
Декоратори класів і методів виконуються під час завантаження класів, а не під час кожного HTTP-запиту.
Наприклад:
@Feature('orders')
@Controller('orders')
export class OrdersController {}під час запуску застосунку додає метадані до класу.
Під час HTTP-запиту:
NestJS визначає маршрут;
запускає guard'и;
guard'и читають метадані через Reflector;
NestJS викликає метод контролера;
параметричний декоратор формує значення параметра;
метод виконується разом з обгорткою методу-декоратора.
Це розділяє декларативний опис і виконання логіки:
декоратор описує правила;
guard або interceptor застосовує ці правила.
Для метаданих краще використовувати Symbol, щоб уникнути конфліктів між різними декораторами:
const ROLES_KEY = Symbol('roles');Рядкові ключі також підтримуються, але однаковий рядок може випадково використовуватися в різних частинах застосунку.
Такий декоратор має лише описувати правило:
@Roles('admin')А перевірку ролі краще виконувати в guard'і. Це робить код передбачуваним і полегшує тестування.
Якщо декоратор приймає обмежений набір значень, опишіть їх через тип:
type Role = 'user' | 'admin';
const Roles = (...roles: Role[]) => {
return SetMetadata(ROLES_KEY, roles);
};Тоді TypeScript не дозволить передати невідому роль.
Метод-декоратор, який замінює descriptor.value, може змінити:
тип результату;
спосіб обробки помилок;
значення this;
порядок виконання логіки.
Обгортку слід робити якомога меншою та зберігати поведінку оригінального методу.
@Roles('admin')
@Get()
getData() {
return [];
}Якщо в застосунку немає guard'а або іншого компонента, який читає ROLES_KEY, цей декоратор не впливає на доступ.
Для HTTP-запиту потрібно використовувати:
context.switchToHttp().getRequest()Якщо отримати дані без перемикання контексту, параметричний декоратор може працювати неправильно.
thisПід час обгортання методу потрібно викликати оригінальну функцію з правильним контекстом:
originalMethod.apply(this, args);Прямий виклик без apply може спричинити помилки, якщо метод використовує властивості екземпляра.
Якщо синхронний метод обгорнути в async, він почне повертати Promise, навіть якщо раніше повертав звичайне значення.
Тому декоратор має враховувати контракт методу або застосовуватися лише до асинхронних методів.
Для пошуку метаданих на обох рівнях використовуйте:
this.reflector.getAllAndOverride(KEY, [
context.getHandler(),
context.getClass(),
]);Якщо потрібні всі значення з обох рівнів, можна використати getAllAndMerge.
@CurrentUser() лише дістає користувача із запиту. Він не автентифікує користувача і не перевіряє його роль.
Перевірку доступу потрібно реалізувати в guard'і.
Параметричні декоратори створюються через createParamDecorator.
ExecutionContext дає доступ до поточного HTTP-запиту.
SetMetadata дозволяє зберігати правила та інші значення як метадані.
Reflector використовується для читання метаданих у guard'ах та інших компонентах NestJS.
Метод-декоратор може обгорнути метод і змінити його поведінку.
Клас-декоратор застосовується до всього класу та зручно описує спільні налаштування.
applyDecorators об'єднує кілька декораторів у один.
Метадані не виконують логіку самостійно — їх має прочитати та застосувати відповідний компонент.
Власні декоратори допомагають прибрати повторення і зробити контролери декларативнішими.