自定义装饰器
本教程共 47 篇 · 第 19 篇 · 更新于 2026-08-09 · 约 12 分钟阅读
本节目标:学会创建自定义参数装饰器和方法装饰器,用组合装饰器简化重复代码,写出更简洁的控制器。
为什么需要自定义装饰器
在 NestJS 里,你已经用过很多内置装饰器了:@Body()、@Query()、@Param() 等等。它们帮你从请求对象里提取数据,省去了手动解析的麻烦。
但有个场景很常见——你需要从 request.user 里拿当前登录用户的信息。没有自定义装饰器之前,你可能这样写:
@Get('profile')
getProfile(@Request() req) {
const user = req.user;
// 用 user 做点什么...
}
每个需要用户信息的方法都要写一遍 req.user。代码不复杂,但重复多了就很烦。
如果有一个 @CurrentUser() 装饰器,直接拿到用户对象,是不是清爽很多?
@Get('profile')
getProfile(@CurrentUser() user: UserEntity) {
// 直接拿到 user,干净利落
}
这就是自定义装饰器的价值——把重复的参数提取逻辑封装成一个语义清晰的装饰器。
参数装饰器
参数装饰器用在控制器方法的参数前面,用来自动提取或转换数据。NestJS 提供了 createParamDecorator 函数来创建它。
最基本的用法
创建一个 @CurrentUser() 装饰器,从请求对象上提取用户信息:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
createParamDecorator 接收一个工厂函数,这个函数有两个参数:
data:传给装饰器的参数(下面会讲)ctx:ExecutionContext,和守卫、拦截器里的一样
用起来很简单:
@Controller('users')
export class UsersController {
@Get('profile')
getProfile(@CurrentUser() user: UserEntity) {
return user;
}
}
传参
装饰器可以接收参数,实现更灵活的提取逻辑。比如你有时需要整个 user 对象,有时只需要 user 的某个字段:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
// 有 data 就取某个字段,没有就返回整个 user
return data ? user?.[data] : user;
},
);
使用方式:
@Get('profile')
getProfile(@CurrentUser() user: UserEntity) {
// 拿到完整的用户对象
return user;
}
@Get('email')
getEmail(@CurrentUser('email') email: string) {
// 只拿到邮箱字段
return { email };
}
@Get('name')
getName(@CurrentUser('firstName') firstName: string) {
// 只拿到名字
return { firstName };
}
Tip
data参数的类型可以是string、number、甚至一个对象。根据你的需求灵活设计。
和管道配合使用
自定义参数装饰器和内置装饰器一样,可以和管道搭配使用:
@Get(':id')
findOne(
@CurrentUser('id', new ParseIntPipe()) id: number,
) {
return this.usersService.findOne(id);
}
Note如果要用
ValidationPipe验证自定义装饰器的参数,需要把validateCustomDecorators选项设为true。默认情况下ValidationPipe不会验证自定义装饰器标注的参数。
@Get()
findOne(
@CurrentUser(new ValidationPipe({ validateCustomDecorators: true }))
user: UserEntity,
) {
// user 会被自动验证
}
实用参数装饰器示例
用户快捷装饰器
把常用的用户字段封装成独立的装饰器:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
);
// 快捷装饰器
export const UserId = () => User('id');
export const UserEmail = () => User('email');
export const UserRole = () => User('role');
控制器里用起来非常清爽:
@Get('profile')
getProfile(
@UserId() userId: number,
@UserEmail() email: string,
@UserRole() role: string,
) {
return { userId, email, role };
}
分页装饰器
分页参数几乎每个列表接口都要处理。与其每次手动解析 page 和 limit,不如封装一下:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
interface PaginationParams {
page: number;
limit: number;
offset: number;
}
export const Pagination = createParamDecorator(
(data: unknown, ctx: ExecutionContext): PaginationParams => {
const request = ctx.switchToHttp().getRequest();
const page = parseInt(request.query.page) || 1;
const limit = Math.min(parseInt(request.query.limit) || 10, 100);
return {
page,
limit,
offset: (page - 1) * limit,
};
},
);
控制器里直接拿到分页参数:
@Get()
findAll(@Pagination() pagination: PaginationParams) {
// pagination = { page: 1, limit: 10, offset: 0 }
return this.usersService.findAll(pagination);
}
Tip注意
Math.min(limit, 100)这个细节。限制最大limit值能防止恶意请求一次拉走太多数据。
客户端 IP 装饰器
获取客户端真实 IP 要考虑代理的情况,逻辑有点繁琐,封装起来就干净了:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const ClientIp = createParamDecorator(
(data: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest();
return (
request.headers['x-forwarded-for']?.split(',')[0] ||
request.headers['x-real-ip'] ||
request.socket?.remoteAddress
);
},
);
使用:
@Post('login')
login(@ClientIp() ip: string) {
this.authService.recordLoginIp(ip);
}
方法装饰器(基于 SetMetadata)
参数装饰器解决的是”怎么从请求里拿数据”的问题。而方法装饰器解决的是”怎么给路由贴标签”的问题。
还记得守卫那一章里的 @Roles() 和 @Public() 吗?它们就是方法装饰器。原理很简单——用 SetMetadata 在路由方法上存一些元数据,守卫或拦截器再去读取。
基本用法
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
SetMetadata 接收两个参数:一个 key 和一个 value。它的作用就是在路由方法上”贴个便签”,写上 isPublic: true。守卫通过 Reflector 读取这个便签。
更复杂的装饰器
比如创建一个缓存配置装饰器,同时设置缓存 key 和过期时间:
import { SetMetadata } from '@nestjs/common';
export const CACHE_KEY = 'cache_key';
export const CACHE_TTL = 'cache_ttl';
export const Cache = (key: string, ttl: number = 60) => {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
SetMetadata(CACHE_KEY, key)(target, propertyKey, descriptor);
SetMetadata(CACHE_TTL, ttl)(target, propertyKey, descriptor);
};
};
使用:
@Get()
@Cache('users_all', 120)
findAll() {
return this.usersService.findAll();
}
Note这里用了一个”装饰器工厂”的模式——外层函数接收参数,返回真正的装饰器函数。装饰器函数接收
target(类)、propertyKey(方法名)、descriptor(方法描述符)三个参数。
NestJS 11 推荐写法:Reflector.createDecorator
NestJS 11 中,官方更推荐用 Reflector.createDecorator 来创建类型安全的装饰器:
import { Reflector } from '@nestjs/core';
// 创建一个带类型约束的 Roles 装饰器
export const Roles = Reflector.createDecorator<string[]>();
使用的时候:
@Post()
@Roles(['admin'])
async create() {
// 只有 admin 角色才能访问
}
守卫里读取:
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get(Roles, context.getHandler());
if (!roles) return true;
// 检查用户角色...
return true;
}
}
Tip
Reflector.createDecorator的好处是类型安全。Roles装饰器只接受string[]类型的参数,传错了 TypeScript 编译器会直接报错。
组合装饰器
实际项目中,一个路由经常要同时加多个装饰器。比如一个需要认证的路由,可能要同时挂 @UseGuards、@Roles、@UseInterceptors 等。
每次写一堆装饰器很烦,而且不利于统一修改。applyDecorators 就是用来解决这个问题的——把多个装饰器组合成一个。
import { applyDecorators, UseGuards, UseInterceptors } from '@nestjs/common';
import { JwtAuthGuard } from './jwt-auth.guard';
import { RolesGuard } from './roles.guard';
import { Roles } from './roles.decorator';
import { Role } from './role.enum';
import { LoggingInterceptor } from './logging.interceptor';
export function Authenticated(...roles: Role[]) {
return applyDecorators(
UseGuards(JwtAuthGuard, RolesGuard),
Roles(...roles),
UseInterceptors(LoggingInterceptor),
);
}
用起来就一个装饰器搞定:
@Controller('admin')
export class AdminController {
@Get('dashboard')
@Authenticated(Role.ADMIN)
getDashboard() {
return '管理后台仪表盘';
}
@Get('settings')
@Authenticated(Role.SUPER_ADMIN)
getSettings() {
return '系统设置';
}
}
@Authenticated(Role.ADMIN) 一个装饰器,等价于同时加了 @UseGuards(JwtAuthGuard, RolesGuard) + @Roles(Role.ADMIN) + @UseInterceptors(LoggingInterceptor)。
Tip组合装饰器是减少重复代码的利器。如果你的项目里有多个路由都使用相同的装饰器组合,一定要封装一下。
装饰器工厂模式
装饰器工厂就是”制造装饰器的函数”。它接收配置参数,返回一个定制好的装饰器。
import { applyDecorators, Post, UsePipes, ValidationPipe } from '@nestjs/common';
interface CreateOptions {
path?: string;
validate?: boolean;
}
export function Create(options: CreateOptions = {}) {
const decorators = [
Post(options.path || ''),
];
if (options.validate !== false) {
decorators.push(UsePipes(new ValidationPipe({ transform: true })));
}
return applyDecorators(...decorators);
}
使用:
@Controller('users')
export class UsersController {
@Create()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Create({ path: 'bulk', validate: false })
bulkCreate(@Body() dtos: CreateUserDto[]) {
return this.usersService.createMany(dtos);
}
}
Note装饰器工厂的好处是可以把”默认行为 + 可配置项”封装在一起。调用的人不需要关心内部用了哪些装饰器,只需要传几个配置参数。
类装饰器
装饰器不只能用在方法和参数上,还能用在类上。
import { SetMetadata } from '@nestjs/common';
export const AUDIT_KEY = 'audit';
export function Audit(enabled: boolean = true) {
return function (target: Function) {
SetMetadata(AUDIT_KEY, enabled)(target);
};
}
使用:
@Controller('users')
@Audit(true)
export class UsersController {
// 这个控制器里的所有方法都会被审计
}
拦截器或守卫可以通过 Reflector 读取类级别的元数据:
const isAudited = this.reflector.get(AUDIT_KEY, context.getClass());
Tip类装饰器适合做”全局开关”——比如给整个控制器开启审计日志、限流、缓存等。
装饰器类型总结
| 类型 | 用在哪 | 创建方式 | 典型场景 |
|---|---|---|---|
| 参数装饰器 | 方法参数前 | createParamDecorator | 提取用户信息、分页参数、IP |
| 方法装饰器 | 方法上 | SetMetadata / Reflector.createDecorator | 标记角色、缓存配置、公开路由 |
| 组合装饰器 | 方法上 | applyDecorators | 合并多个装饰器为一个 |
| 类装饰器 | 类上 | 直接写函数返回装饰器 | 全局开关(审计、限流) |
最佳实践
1. 装饰器文件单独存放
建议创建 common/decorators/ 目录,把所有自定义装饰器集中管理:
src/
common/
decorators/
current-user.decorator.ts
pagination.decorator.ts
public.decorator.ts
index.ts # 统一导出
2. 命名要有语义
装饰器的名字应该让人一看就知道它干什么。@CurrentUser('email') 比 @Extract('user.email') 直观得多。
3. 组合装饰器要克制
applyDecorators 很好用,但不要过度封装。如果一个组合装饰器里塞了七八个装饰器,反而让人看不懂。3-5 个是比较合理的范围。
4. 给装饰器加类型约束
用 Reflector.createDecorator<T>() 或者在工厂函数里标注参数类型,让 TypeScript 帮你检查。
小结
自定义装饰器是 NestJS 提升代码可读性的重要手段。
关键知识点回顾:
createParamDecorator创建参数装饰器,从请求中提取数据SetMetadata/Reflector.createDecorator创建方法装饰器,给路由贴元数据标签applyDecorators组合多个装饰器为一个,减少重复代码- 自定义装饰器和管道一样,支持数据验证和转换
- 装饰器工厂模式可以封装配置,提供默认行为
接下来进入认证专题,先聊 Passport 认证的基础用法。