Skip to main content

Custom Decorators trong NestJS

Decorators là một tính năng mạnh mẽ của TypeScript cho phép bạn thêm annotations và modify classes, methods, properties, và parameters. NestJS cung cấp một set decorators built-in, nhưng bạn cũng có thể tạo custom decorators cho nhu cầu cụ thể.

Khái Niệm Decorator​

Decorator là một function trả về một function khác. Nó được sử dụng để modify hoặc enhance behavior của classes, methods, properties, hoặc parameters.

// Cách decorator hoạt động
function MyDecorator() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Modify hoặc enhance target
};
}

@MyDecorator()
class MyClass {}

Reflection và Metadata​

NestJS sử dụng reflect-metadata library để làm việc với decorators:

npm install reflect-metadata

Đảm bảo import ở top của main.ts:

// main.ts
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();

Method Decorators​

1. Simple Method Decorator​

import { SetMetadata } from '@nestjs/common';

export const PublicRoute = () => SetMetadata('public', true);

// Sử dụng
@Controller('auth')
export class AuthController {
@Post('login')
@PublicRoute()
login(@Body() credentials: any) {
return { token: 'abc123' };
}
}

2. Timer Decorator​

export const Timer = () => {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) {
const originalMethod = descriptor.value;

descriptor.value = async function (...args: any[]) {
const start = Date.now();
const result = await originalMethod.apply(this, args);
const end = Date.now();
console.log(`${propertyKey} took ${end - start}ms`);
return result;
};

return descriptor;
};
};

// Sử dụng
@Controller('users')
export class UsersController {
@Get()
@Timer()
findAll() {
return [];
}
}

3. Validate Decorator​

export const Validate = (validatorFn: (value: any) => boolean) => {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) {
const originalMethod = descriptor.value;

descriptor.value = function (...args: any[]) {
if (!validatorFn(args[0])) {
throw new BadRequestException(`Validation failed for ${propertyKey}`);
}
return originalMethod.apply(this, args);
};

return descriptor;
};
};

// Sử dụng
const isPositive = (value: number) => value > 0;

@Controller('products')
export class ProductsController {
@Get(':id')
@Validate(isPositive)
findOne(@Param('id', ParseIntPipe) id: number) {
return { id };
}
}

4. Authorize Decorator​

import { SetMetadata } from '@nestjs/common';

export const Authorize = (roles: string[]) => SetMetadata('roles', roles);

// Sử dụng
@Controller('admin')
export class AdminController {
@Get('dashboard')
@Authorize(['admin'])
getDashboard() {
return { data: 'admin' };
}

@Post('users')
@Authorize(['admin', 'moderator'])
createUser(@Body() createUserDto: CreateUserDto) {
return { message: 'User created' };
}
}

Parameter Decorators​

1. Custom Param Decorator​

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const UserId = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user?.id;
},
);

// Sử dụng
@Controller('users')
export class UsersController {
@Get('profile')
getProfile(@UserId() userId: string) {
return { userId };
}
}

2. User Decorator​

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;

return data ? user?.[data] : user;
},
);

// Sử dụng
@Controller('profile')
export class ProfileController {
@Get()
getProfile(@CurrentUser() user: any) {
return { user };
}

@Get('email')
getEmail(@CurrentUser('email') email: string) {
return { email };
}

@Get('id')
getId(@CurrentUser('id') userId: string) {
return { userId };
}
}

3. Paginate Decorator​

import { createParamDecorator, ExecutionContext, BadRequestException } from '@nestjs/common';

interface PaginationParams {
page: number;
limit: number;
skip: number;
}

export const Paginate = createParamDecorator(
(data: unknown, ctx: ExecutionContext): PaginationParams => {
const request = ctx.switchToHttp().getRequest();
const page = parseInt(request.query.page) || 1;
const limit = parseInt(request.query.limit) || 10;

if (page < 1 || limit < 1) {
throw new BadRequestException('Page and limit must be greater than 0');
}

return {
page,
limit,
skip: (page - 1) * limit,
};
},
);

// Sử dụng
@Controller('posts')
export class PostsController {
@Get()
findAll(@Paginate() pagination: PaginationParams) {
return {
page: pagination.page,
limit: pagination.limit,
data: [],
};
}
}

4. Headers Decorator​

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const GetHeaders = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();

if (data) {
return request.headers[data.toLowerCase()];
}

return request.headers;
},
);

// Sử dụng
@Controller('api')
export class ApiController {
@Get()
getData(@GetHeaders('x-api-key') apiKey: string) {
return { apiKey };
}

@Post()
create(@GetHeaders() headers: any) {
return { headers };
}
}

5. Query Decorator​

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const GetQuery = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();

if (data) {
return request.query[data];
}

return request.query;
},
);

// Sử dụng
@Controller('search')
export class SearchController {
@Get()
search(@GetQuery('keyword') keyword: string) {
return { keyword };
}

@Get('advanced')
advancedSearch(@GetQuery() filters: any) {
return { filters };
}
}

6. IP Decorator​

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const IP = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.ip;
},
);

// Sử dụng
@Controller('api')
export class ApiController {
@Post('log')
logRequest(@IP() ip: string, @Body() body: any) {
return { ip, body };
}
}

Class Decorators​

1. Controller with Prefix​

export const ApiController = (prefix: string = '') => {
return (target: any) => {
const originalController = target;

Reflect.defineMetadata('api:prefix', prefix, originalController);

const newController = class extends originalController {
constructor(...args: any[]) {
super(...args);
}
};

Object.setPrototypeOf(newController, originalController);
Object.setPrototypeOf(newController.prototype, originalController.prototype);

return newController;
};
};

2. Serializable Decorator​

import { ClassSerializerInterceptor } from '@nestjs/common';

export const Serialize = (dto: any) => {
return (target: any) => {
Reflect.defineMetadata('serialize:dto', dto, target);
};
};

// Sử dụng
@Controller('users')
@UseInterceptors(ClassSerializerInterceptor)
export class UsersController {
@Get()
findAll() {
return new UserDto();
}
}

Property Decorators​

1. Required Decorator​

export function Required() {
return function (target: any, propertyKey: string) {
Reflect.defineMetadata('required', true, target, propertyKey);
};
}

// Validator
export function ValidateRequired(obj: any) {
const requiredProps = [];

for (const key in obj) {
if (Reflect.getMetadata('required', obj, key)) {
if (!obj[key]) {
requiredProps.push(key);
}
}
}

return requiredProps.length === 0;
}

// Sử dụng
export class CreateUserDto {
@Required()
name: string;

@Required()
email: string;

age?: number;
}

2. Validate Min/Max​

export function Min(value: number) {
return function (target: any, propertyKey: string) {
Reflect.defineMetadata('min', value, target, propertyKey);
};
}

export function Max(value: number) {
return function (target: any, propertyKey: string) {
Reflect.defineMetadata('max', value, target, propertyKey);
};
}

export function ValidateMinMax(obj: any) {
for (const key in obj) {
const min = Reflect.getMetadata('min', obj, key);
const max = Reflect.getMetadata('max', obj, key);
const value = obj[key];

if (min !== undefined && value < min) {
return false;
}

if (max !== undefined && value > max) {
return false;
}
}

return true;
}

// Sử dụng
export class CreateProductDto {
@Min(0)
@Max(1000)
price: number;

@Min(1)
quantity: number;
}

Combining Multiple Decorators​

import { applyDecorators, UseGuards, UseInterceptors } from '@nestjs/common';

export const Protected = () => {
return applyDecorators(
UseGuards(JwtAuthGuard, RolesGuard),
UseInterceptors(LoggingInterceptor),
SetMetadata('isProtected', true),
);
};

// Sử dụng
@Controller('admin')
export class AdminController {
@Get('dashboard')
@Protected()
getDashboard() {
return { data: 'admin' };
}
}

// Hoặc tạo decorator phức tạp hơn
export const Auth = (roles: string[] = []) => {
return applyDecorators(
UseGuards(JwtAuthGuard),
roles.length > 0 && UseGuards(RolesGuard),
SetMetadata('roles', roles),
);
};

@Controller('users')
export class UsersController {
@Post()
@Auth(['admin', 'moderator'])
create(@Body() createUserDto: CreateUserDto) {
return { message: 'User created' };
}

@Get()
@Auth()
findAll() {
return [];
}

@Get('public')
getPublic() {
return [];
}
}

Ví Dụ Custom Decorators Thực Tế​

1. Timeout Decorator​

import { SetMetadata } from '@nestjs/common';

export const Timeout = (ms: number) => SetMetadata('timeout', ms);

// Sử dụng trong interceptor
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
constructor(private reflector: Reflector) {}

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const timeout = this.reflector.get<number>('timeout', context.getHandler());
const timeoutMs = timeout || 30000;

return next.handle().pipe(
timeout(timeoutMs),
catchError((error) => {
if (error.name === 'TimeoutError') {
throw new RequestTimeoutException();
}
throw error;
}),
);
}
}

// Sử dụng
@Controller('api')
export class ApiController {
@Get('slow-endpoint')
@Timeout(60000)
slowEndpoint() {
return { data: 'slow' };
}
}

2. Deprecated Decorator​

import { SetMetadata } from '@nestjs/common';

export const Deprecated = (message?: string) => {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;

descriptor.value = function (...args: any[]) {
console.warn(
`⚠️ Method '${propertyKey}' is deprecated. ${message || 'Please use a newer version.'}`,
);
return originalMethod.apply(this, args);
};

return descriptor;
};
};

// Sử dụng
@Controller('api')
export class ApiController {
@Get('v1/users')
@Deprecated('Use /v2/users instead')
getOldUsers() {
return [];
}
}

3. Cache Decorator​

export const Cache = (ttl: number = 60) => {
return (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) => {
const originalMethod = descriptor.value;
const cache = new Map();

descriptor.value = function (...args: any[]) {
const cacheKey = JSON.stringify(args);

if (cache.has(cacheKey)) {
console.log(`Cache hit for ${propertyKey}`);
return cache.get(cacheKey);
}

const result = originalMethod.apply(this, args);

cache.set(cacheKey, result);

setTimeout(() => {
cache.delete(cacheKey);
}, ttl * 1000);

return result;
};

return descriptor;
};
};

// Sử dụng
@Injectable()
export class UsersService {
@Cache(300) // 5 minutes cache
getUsers() {
return [];
}
}

4. ValidateEmail Decorator​

import { createParamDecorator, ExecutionContext, BadRequestException } from '@nestjs/common';

export const ValidateEmail = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const email = request.body?.email;

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

if (!emailRegex.test(email)) {
throw new BadRequestException('Invalid email format');
}

return email;
},
);

// Sử dụng
@Controller('auth')
export class AuthController {
@Post('register')
register(@ValidateEmail() email: string) {
return { email };
}
}

5. RateLimit Decorator​

export const RateLimit = (limit: number = 10, window: number = 60) => {
return (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) => {
const originalMethod = descriptor.value;
const requests: { [key: string]: number[] } = {};

descriptor.value = function (this: any, ...args: any[]) {
const key = `${target.name}:${propertyKey}`;

if (!requests[key]) {
requests[key] = [];
}

const now = Date.now();
const windowStart = now - window * 1000;

requests[key] = requests[key].filter((time) => time > windowStart);

if (requests[key].length >= limit) {
throw new TooManyRequestsException(
`Rate limit exceeded: ${limit} requests per ${window}s`,
);
}

requests[key].push(now);

return originalMethod.apply(this, args);
};

return descriptor;
};
};

// Sử dụng
@Controller('api')
export class ApiController {
@Post('send-email')
@RateLimit(5, 60) // 5 requests per 60 seconds
sendEmail(@Body() data: any) {
return { success: true };
}
}

6. Log Decorator​

export const Log = (prefix: string = '') => {
return (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor,
) => {
const originalMethod = descriptor.value;

descriptor.value = async function (...args: any[]) {
const startTime = Date.now();
const logPrefix = prefix || propertyKey;

console.log(`[${logPrefix}] Started`);

try {
const result = await originalMethod.apply(this, args);
const duration = Date.now() - startTime;
console.log(`[${logPrefix}] Completed in ${duration}ms`);
return result;
} catch (error) {
const duration = Date.now() - startTime;
console.error(`[${logPrefix}] Failed after ${duration}ms`, error);
throw error;
}
};

return descriptor;
};
};

// Sử dụng
@Injectable()
export class UsersService {
@Log('GetAllUsers')
async getAll() {
return [];
}

@Log('CreateUser')
async create(createUserDto: CreateUserDto) {
return { id: 1, ...createUserDto };
}
}

7. Transform Decorator​

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const Transform = (transformFn: (value: any) => any) => {
return createParamDecorator((data: any, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const value = data ? request.body?.[data] : request.body;
return transformFn(value);
});
};

// Sử dụng
const trimString = (value: string) => value?.trim();
const toLowerCase = (value: string) => value?.toLowerCase();

@Controller('users')
export class UsersController {
@Post()
create(
@Transform(trimString)() name: string,
@Transform(toLowerCase)() email: string,
) {
return { name, email };
}
}

Metadata with Reflector​

import { Reflector } from '@nestjs/core';

// Tạo decorator
export const Permissions = Reflector.createDecorator<string[]>();

// Sử dụng trong guard/interceptor
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(private reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const permissions = this.reflector.get(Permissions, context.getHandler());

if (!permissions) {
return true;
}

const request = context.switchToHttp().getRequest();
const user = (request as any).user;

return permissions.every((permission) =>
user?.permissions?.includes(permission),
);
}
}

// Sử dụng
@Controller('admin')
export class AdminController {
@Post('users')
@Permissions(['users:create'])
createUser(@Body() createUserDto: CreateUserDto) {
return { message: 'User created' };
}

@Delete('users/:id')
@Permissions(['users:delete'])
deleteUser(@Param('id') id: string) {
return { message: 'User deleted' };
}
}

Best Practices​

1. Tạo Reusable Decorators​

// ✅ Đúng - Generic, reusable
export const PublicRoute = () => SetMetadata('public', true);

// ❌ Sai - Specific to one use case
export const SkipJwtAuthForLoginRoute = () => SetMetadata('skipJwt', true);

2. Document Custom Decorators​

/**
* Đánh dấu route là public - không cần authentication
*
* @example
* @Get('public')
* @PublicRoute()
* getPublic() { ... }
*/
export const PublicRoute = () => SetMetadata('public', true);

3. Use applyDecorators for Compound Decorators​

// ✅ Đúng - Compound decorator
export const AdminOnly = () => {
return applyDecorators(
UseGuards(JwtAuthGuard, RolesGuard),
SetMetadata('roles', ['admin']),
UseInterceptors(LoggingInterceptor),
);
};

// Sử dụng
@Delete(':id')
@AdminOnly()
deleteUser(@Param('id') id: string) {
return { message: 'Deleted' };
}

4. Proper Error Handling in Parameter Decorators​

// ✅ Đúng - Handle errors properly
export const ValidUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;

if (!user) {
throw new UnauthorizedException('User not found');
}

return user;
},
);

5. Type-Safe Decorators​

// ✅ Đúng - Type-safe
export const CurrentUser = createParamDecorator<string | undefined, ExecutionContext, any>(
(data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;

return data ? user?.[data] : user;
},
);

Complete Example​

// decorators/auth.decorator.ts
import { applyDecorators, UseGuards, SetMetadata } from '@nestjs/common';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { RolesGuard } from '../guards/roles.guard';

export const Auth = (...roles: string[]) => {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(JwtAuthGuard, RolesGuard),
);
};

// decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
(data: string | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;

if (!user) {
throw new UnauthorizedException('User not found');
}

return data ? user[data] : user;
},
);

// controllers/users.controller.ts
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}

@Post()
@Auth('admin', 'moderator')
create(
@Body() createUserDto: CreateUserDto,
@CurrentUser('id') userId: string,
) {
return this.usersService.create(createUserDto, userId);
}

@Get()
findAll() {
return this.usersService.findAll();
}

@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}

@Patch(':id')
@Auth('admin')
update(
@Param('id') id: string,
@Body() updateUserDto: UpdateUserDto,
@CurrentUser() user: any,
) {
return this.usersService.update(id, updateUserDto, user.id);
}

@Delete(':id')
@Auth('admin')
remove(
@Param('id') id: string,
@CurrentUser('id') userId: string,
) {
return this.usersService.remove(id, userId);
}
}

Kết Luận​

Custom Decorators là công cụ mạnh mẽ để:

  • Reduce boilerplate code - Tránh lặp lại code
  • Enhance readability - Code dễ đọc hơn
  • Centralize logic - Tập trung logic ở một nơi
  • Reuse functionality - Tái sử dụng across routes
  • Add metadata - Gắn metadata vào classes/methods

Sử dụng Custom Decorators đúng cách giúp bạn:

  • Tạo elegant và maintainable code
  • Tái sử dụng authentication/authorization logic
  • Giảm code duplication
  • Cải thiện code organization
  • Tạo semantic API cho ứng dụng