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