首页 / NestJS 入门教程 / 自定义装饰器

NestJS 入门教程

自定义装饰器

本教程共 47 篇 · 第 19 篇 · 更新于 2026-08-09 · 约 12 分钟阅读

NestJS自定义装饰器createParamDecoratorapplyDecoratorsSetMetadata参数装饰器

本节目标:学会创建自定义参数装饰器和方法装饰器,用组合装饰器简化重复代码,写出更简洁的控制器。

为什么需要自定义装饰器

在 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:传给装饰器的参数(下面会讲)
  • ctxExecutionContext,和守卫、拦截器里的一样

用起来很简单:

@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 参数的类型可以是 stringnumber、甚至一个对象。根据你的需求灵活设计。

和管道配合使用

自定义参数装饰器和内置装饰器一样,可以和管道搭配使用:

@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 };
}

分页装饰器

分页参数几乎每个列表接口都要处理。与其每次手动解析 pagelimit,不如封装一下:

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 认证的基础用法。