Пошук уроків, статей та іншого контенту
Створите параметрні та методні decorators для повторного використання метаданих і доступу до даних запиту.
У NestJS decorators дають змогу додавати метадані до класів і методів або змінювати спосіб отримання параметрів методу.
Власні decorators корисні, коли одна й та сама логіка повторюється в кількох місцях. Наприклад:
отримання поточного користувача з request;
отримання окремого поля користувача;
позначення методів дозволеними ролями;
уніфікація метаданих для guard або interceptor.
NestJS підтримує кілька типів decorators. У цьому уроці розглянемо:
параметрні decorators через createParamDecorator;
методні decorators через SetMetadata;
читання метаданих через Reflector.
Параметрний decorator застосовується до параметра методу контролера.
Замість такого коду:
@Get('profile')
getProfile(@Req() request: Request) {
return request.user;
}можна створити власний decorator:
@Get('profile')
getProfile(@CurrentUser() user: User) {
return user;
}Такий підхід приховує деталі роботи з HTTP-запитом і робить контролер зрозумілішим.
CurrentUserДля створення параметрного decorator використовується функція createParamDecorator.
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(_data: unknown, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest();
return request.user;
},
);ExecutionContext містить інформацію про поточний контекст виконання. Для HTTP-запиту викликаємо:
context.switchToHttp().getRequest();Після цього отримуємо стандартний об'єкт запиту, з якого можна прочитати user.
Властивість request.user зазвичай додається middleware або guard автентифікації. Сам decorator не виконує автентифікацію — він лише дістає вже доступні дані.
CurrentUserimport { Controller, Get } from '@nestjs/common';
import { CurrentUser } from './current-user.decorator';
interface User {
id: string;
email: string;
roles: string[];
}
@Controller('users')
export class UsersController {
@Get('profile')
getProfile(@CurrentUser() user: User) {
return {
id: user.id,
email: user.email,
};
}
}Тепер метод контролера не залежить від структури HTTP-запиту. Він одразу отримує потрібний об'єкт User.
Перший параметр callback-функції createParamDecorator називається data. Через нього можна передати назву поля, яке потрібно отримати.
Створимо decorator UserField:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const UserField = createParamDecorator(
(field: string | undefined, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!field) {
return user;
}
return user?.[field];
},
);Його можна використовувати без аргументу:
@Get('profile')
getProfile(@UserField() user: User) {
return user;
}Або вказати конкретне поле:
@Get('email')
getEmail(@UserField('email') email: string) {
return {
email,
};
}У цьому прикладі:
@UserField() повертає весь об'єкт user;
@UserField('email') повертає user.email.
Якщо відомий тип користувача, його варто описати окремим інтерфейсом або класом:
export interface AuthenticatedUser {
id: string;
email: string;
roles: string[];
}Decorator може використовувати цей тип:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { AuthenticatedUser } from './authenticated-user';
export const CurrentUser = createParamDecorator(
(
_data: unknown,
context: ExecutionContext,
): AuthenticatedUser | undefined => {
const request = context.switchToHttp().getRequest<{
user?: AuthenticatedUser;
}>();
return request.user;
},
);Типізація не перевіряє, чи справді guard додав user до запиту під час виконання. Вона лише допомагає редактору та компілятору працювати з кодом без зайвих помилок.
Методний decorator застосовується до методу класу. У NestJS його часто використовують, щоб додати метадані, які потім прочитає guard або interceptor.
Наприклад, можна позначати методи ролями, доступними для виконання:
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);Тепер decorator можна застосувати до методу контролера:
import { Controller, Get } from '@nestjs/common';
import { Roles } from './roles.decorator';
@Controller('admin')
export class AdminController {
@Get('reports')
@Roles('admin', 'manager')
getReports() {
return {
message: 'Звіт доступний',
};
}
}Виклик:
@Roles('admin', 'manager')створює метадані приблизно такого змісту:
{
roles: ['admin', 'manager']
}Сам по собі SetMetadata не забороняє доступ. Він лише зберігає інформацію. Перевірити її має guard.
ReflectorДля читання метаданих у guard використовується сервіс Reflector.
import {
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
return false;
}
return requiredRoles.some((role) => user.roles?.includes(role));
}
}Метод getAllAndOverride шукає метадані спочатку на обробнику запиту, а потім на класі контролера.
У цьому прикладі:
guard читає ролі з @Roles(...);
отримує користувача з request.user;
перевіряє, чи має користувач хоча б одну потрібну роль;
повертає true або false.
Guard можна підключити до конкретного контролера або методу:
import {
Controller,
Get,
UseGuards,
} from '@nestjs/common';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';
@Controller('admin')
@UseGuards(RolesGuard)
export class AdminController {
@Get('reports')
@Roles('admin', 'manager')
getReports() {
return {
message: 'Звіт доступний',
};
}
}У цьому випадку RolesGuard виконуватиметься для методів AdminController.
Нижче наведено узгоджений приклад параметрного decorator, методного decorator і guard.
current-user.decorator.tsimport { createParamDecorator, ExecutionContext } from '@nestjs/common';
export interface AuthenticatedUser {
id: string;
email: string;
roles: string[];
}
interface RequestWithUser {
user?: AuthenticatedUser;
}
export const CurrentUser = createParamDecorator(
(
_data: unknown,
context: ExecutionContext,
): AuthenticatedUser | undefined => {
const request =
context.switchToHttp().getRequest<RequestWithUser>();
return request.user;
},
);roles.decorator.tsimport { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => {
return SetMetadata(ROLES_KEY, roles);
};roles.guard.tsimport {
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
interface AuthenticatedUser {
id: string;
email: string;
roles: string[];
}
interface RequestWithUser {
user?: AuthenticatedUser;
}
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
const request =
context.switchToHttp().getRequest<RequestWithUser>();
const user = request.user;
if (!user) {
return false;
}
return requiredRoles.some((role) =>
user.roles.includes(role),
);
}
}admin.controller.tsimport {
Controller,
Get,
UseGuards,
} from '@nestjs/common';
import { CurrentUser } from './current-user.decorator';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';
import { AuthenticatedUser } from './current-user.decorator';
@Controller('admin')
@UseGuards(RolesGuard)
export class AdminController {
@Get('profile')
getProfile(@CurrentUser() user: AuthenticatedUser) {
return {
id: user.id,
email: user.email,
roles: user.roles,
};
}
@Get('reports')
@Roles('admin', 'manager')
getReports() {
return {
message: 'Звіт доступний',
};
}
}Щоб guard можна було створити через dependency injection, він має бути зареєстрований у модулі як provider:
import { Module } from '@nestjs/common';
import { AdminController } from './admin.controller';
import { RolesGuard } from './roles.guard';
@Module({
controllers: [AdminController],
providers: [RolesGuard],
})
export class AdminModule {}Якщо кілька decorators завжди використовуються разом, їх можна об'єднати через applyDecorators.
Наприклад, контролер може часто використовувати однаковий набір decorators:
import {
applyDecorators,
UseGuards,
} from '@nestjs/common';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';
export function AdminOnly() {
return applyDecorators(
Roles('admin'),
UseGuards(RolesGuard),
);
}Використання:
import { Controller, Get } from '@nestjs/common';
import { AdminOnly } from './admin-only.decorator';
@Controller('settings')
export class SettingsController {
@Get()
@AdminOnly()
getSettings() {
return {
message: 'Налаштування доступні лише адміністратору',
};
}
}Складений decorator зручний для повторюваних комбінацій, але не варто об'єднувати в ньому логіку, яка потрібна лише один раз.
ExecutionContext може представляти не лише HTTP-запит. NestJS також працює з WebSocket і мікросервісами.
Тому параметрний decorator, який використовує:
context.switchToHttp()є специфічним саме для HTTP-контролерів.
У HTTP-контексті можна отримати:
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();Для параметрного decorator контролера зазвичай достатньо getRequest().
Методний decorator не обов'язково має читати дані запиту. Його основне завдання може полягати лише в додаванні метаданих:
import { SetMetadata } from '@nestjs/common';
export const CACHE_TIME_KEY = 'cacheTime';
export const CacheTime = (seconds: number) => {
return SetMetadata(CACHE_TIME_KEY, seconds);
};Застосування:
@CacheTime(60)
@Get('products')
getProducts() {
return [];
}Пізніше interceptor може прочитати CACHE_TIME_KEY через Reflector і використати значення 60. Методний decorator не повинен самостійно виконувати роботу interceptor або guard.
@CurrentUser() сам виконає автентифікаціюПараметрний decorator лише читає значення:
return request.user;Якщо request.user ніхто не додав, результатом буде undefined. Автентифікація та додавання користувача до запиту мають виконуватися окремим guard або middleware.
Для HTTP-запиту потрібно використовувати:
context.switchToHttp().getRequest();Якщо decorator застосовується в іншому типі транспорту, HTTP-контекст може бути недоступним.
SetMetadataТакий decorator:
export const Roles = (...roles: string[]) =>
SetMetadata('roles', roles);не перевіряє ролі та не блокує запит. Він лише додає метадані. Для перевірки потрібен guard, який прочитає ці метадані через Reflector.
request.userНе варто безпечно припускати, що користувач завжди існує:
return request.user.id;Якщо запит не пройшов потрібний guard, це може спричинити помилку під час виконання. Перевіряйте наявність користувача або гарантуйте порядок виконання guards.
Ключ, переданий у SetMetadata, і ключ, використаний у Reflector, мають збігатися:
export const ROLES_KEY = 'roles';Якщо в одному місці використати 'roles', а в іншому 'userRoles', guard не знайде метадані.
Власний decorator має вирішувати одну конкретну задачу:
отримувати дані з запиту;
додавати метадані;
поєднувати кілька decorators.
Складну бізнес-логіку краще залишати в сервісах, guards або interceptors.
Параметрні decorators створюються через createParamDecorator.
Через ExecutionContext можна отримати поточний HTTP-запит.
Методні decorators можуть додавати метадані за допомогою SetMetadata.
Reflector використовується для читання метаданих у guard або interceptor.
@CurrentUser() спрощує доступ до request.user.
@Roles(...) може описувати вимоги до методу, а RolesGuard — перевіряти їх.
applyDecorators дає змогу об'єднати кілька decorators в один.
Decorator не повинен містити логіку, яка належить сервісу, guard або interceptor.