NestJS 有个看起来很神奇的地方:在类或方法上加一个 @Something(),框架就能理解你的意图。
@Controller('users') // 这是个控制器,处理 /users 路由
export class UserController {
@Get() // 这是个 GET 请求
@Roles('admin') // 需要 admin 权限
getUsers() {}
}这些 @ 开头的东西叫装饰器(Decorator),它们看起来像是在给代码”做标记”。那么问题来了:
@Controller 怎么让 NestJS 知道这是个控制器?@Get() 怎么让框架知道这是个 GET 路由? @Roles('admin') 怎么让 Guard 知道需要检查权限?
答案是:所有这些装饰器,本质上都是在写 Metadata(元数据)。
可以把 Metadata 理解成给代码贴便签:
- 装饰器负责”贴便签”(写入信息)
- NestJS 负责”读便签”(读取信息并执行对应逻辑)
从一个例子开始
假设我们要实现权限控制:只有 admin 可以删除用户。
传统方式可能是传参数:
// ❌ 参数方式:侵入业务逻辑
deleteUser({ requiredRole: 'admin' }) {
// 业务代码要关心权限
}NestJS 的方式:
// ✅ 装饰器方式:声明式,业务代码干净
@Roles('admin')
deleteUser() {
// 业务代码不关心权限
}@Roles('admin') 做了什么?它在 deleteUser 方法上贴了一张便签,写着:“这个方法需要 admin 角色”。
当请求到来时,RolesGuard 会去读这张便签,然后决定是否放行。
核心流程:
- 装饰器在编译时写入 Metadata
- Guard 在运行时读取 Metadata
- 根据读到的信息做决策(放行或拒绝)
Metadata 底层:reflect-metadata 库
“贴便签”这个比喻背后,是 JavaScript 的 reflect-metadata 库:
// 写入:给 deleteUser 方法贴便签
Reflect.defineMetadata('roles', ['admin'], UserController.prototype, 'deleteUser');
// 读取:需要的时候读取便签
const roles = Reflect.getMetadata('roles', UserController.prototype, 'deleteUser');
// ['admin']三个关键参数:
key:便签内容的标识('roles')target:贴在哪个对象上(UserController.prototype)propertyKey:具体哪个方法('deleteUser')
这就是 Metadata 的本质:一个键值对存储系统,可以在任何对象或方法上存储额外信息。
装饰器 = 写 Metadata 的函数
@Roles('admin') 看起来很特殊,但它就是个函数:
export const Roles = (...roles: string[]) => {
return (target: any, propertyKey: string) => {
Reflect.defineMetadata('roles', roles, target, propertyKey);
};
};当你写下:
@Roles('admin', 'moderator')
deleteUser() {}执行过程是:
Roles('admin', 'moderator')被调用,返回一个装饰器函数- 装饰器函数接收
target和propertyKey - 调用
Reflect.defineMetadata存储信息
所以装饰器的本质就是:编译时执行的函数,用来写 Metadata。
NestJS 的所有装饰器都遵循这个规律:
@Controller('users')→ 写入path: 'users'@Get()→ 写入method: 'GET'@UseGuards(RolesGuard)→ 写入guards: [RolesGuard]@Injectable()→ 标记这个类可以被依赖注入
Reflector:读取 Metadata
装饰器负责写,那谁来读?NestJS 提供了 Reflector:
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) return true;
// ... 检查用户角色
return roles.some(role => request.user?.roles?.includes(role));
}
}关键代码:
this.reflector.get<string[]>('roles', context.getHandler())context.getHandler():当前请求对应的方法(如deleteUser)- 从这个方法上读取
'roles'的值
类和方法都有装饰器怎么办?
实际场景中,装饰器可能同时出现在类和方法上:
@Controller('users')
@Roles('user') // 类级别:所有方法默认需要 user 角色
export class UserController {
@Get()
getUsers() {}
@Delete(':id')
@Roles('admin') // 方法级别:覆盖成 admin
deleteUser() {}
}Reflector 提供三种策略:
1. get() - 只读取指定位置
const roles = this.reflector.get<string[]>('roles', context.getHandler());只看方法上有没有,不管类上的。
2. getAllAndOverride() - 就近原则(最常用)
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(), // 先看方法
context.getClass(), // 再看类
]);方法上有就用方法的,没有才用类的。
3. getAllAndMerge() - 合并所有
const roles = this.reflector.getAllAndMerge<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);把类和方法的值合并在一起。
对比:
| 装饰器位置 | get(handler) | getAllAndOverride() | getAllAndMerge() |
|---|---|---|---|
只在方法:@Roles('admin') | ['admin'] | ['admin'] | ['admin'] |
只在类:@Roles('user') | undefined | ['user'] | ['user'] |
| 类+方法都有 | ['admin'] | ['admin'](方法覆盖) | ['admin', 'user'](合并) |
一个例子:从装饰器到 Guard
创建装饰器
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);SetMetadata 是 NestJS 提供的便捷方法,等价于 Reflect.defineMetadata。
创建 Guard
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
if (!roles) return true;
// ... 检查用户是否有对应角色
}
}使用
@Controller('users')
@UseGuards(RolesGuard)
export class UserController {
@Get()
@Roles('user')
getUsers() {}
@Delete(':id')
@Roles('admin')
deleteUser() {}
}Metadata 的执行时机
Metadata 不是实时读写的,而是分阶段发生:

三个阶段:
-
编译时(TypeScript → JavaScript)
- 装饰器执行,写入 Metadata
-
启动时(应用启动)
- NestJS 扫描所有类和方法
- 读取 Metadata,构建路由表、依赖关系
-
运行时(请求处理)
- Guard/Interceptor 读取 Metadata
- 根据标签决定是否放行
NestJS 内置的 Metadata Key
NestJS 内部大量使用 Metadata,常见的 key:
// 路由相关
'path' // @Controller('users')
'method' // @Get(), @Post()
// 参数相关
'design:paramtypes' // 构造函数参数类型(DI 的核心)
'design:type' // 属性类型
'design:returntype' // 返回值类型
// 装饰器相关
'guards' // @UseGuards()
'interceptors' // @UseInterceptors()
'filters' // @UseFilters()
'pipes' // @UsePipes()举个例子:
@Injectable()
export class UserService {
constructor(private repo: UserRepository) {}
}NestJS 如何知道要注入 UserRepository?
因为 TypeScript 编译时自动写入了 design:paramtypes 元数据:
Reflect.defineMetadata(
'design:paramtypes',
[UserRepository],
UserService
);NestJS 启动时读取这个 Metadata,知道 UserService 的构造函数需要 UserRepository,然后自动注入。
来点实践技巧
技巧 1:自定义复杂 Metadata
除了存储简单的数组,还可以存储复杂对象:
// 限流装饰器
export const RateLimit = (limit: number, window: number) =>
SetMetadata('rateLimit', { limit, window });
// 使用
@RateLimit(100, 60) // 每分钟最多 100 次请求
getUsers() {}
// Guard 中读取
const config = this.reflector.get('rateLimit', context.getHandler());
// { limit: 100, window: 60 }技巧 2:组合多个装饰器
export const AdminOnly = () => {
return applyDecorators(
Roles('admin'),
RateLimit(1000, 60),
UseGuards(RolesGuard, RateLimitGuard)
);
};
// 使用
@AdminOnly()
getAdminData() {}技巧 3:类型安全的 Metadata
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// Guard 中读取
const roles = this.reflector.get<string[]>(ROLES_KEY, context.getHandler());避免硬编码字符串,减少拼写错误。
技巧 4:调试 Metadata
想知道某个方法上有哪些 Metadata?
@Controller('users')
export class UserController {
@Get()
@Roles('admin')
getUsers() {}
}
// 读取所有 Metadata Key
const allKeys = Reflect.getMetadataKeys(UserController.prototype, 'getUsers');
// ['path', 'method', 'roles', 'design:paramtypes', ...]
// 逐个读取值
allKeys.forEach(key => {
const value = Reflect.getMetadata(key, UserController.prototype, 'getUsers');
console.log(`${key}:`, value);
});输出示例:
method: GET
roles: ['admin']
design:paramtypes: []
总结
Metadata 是 NestJS 装饰器系统的底层机制:装饰器在编译时执行并写入 Metadata,这些 Metadata 以键值对形式存储在类和方法上,然后通过 Reflector 读取。整个过程分三个阶段:编译时写入、启动时构建依赖图、运行时读取做权限检查。
所有 NestJS 的装饰器都基于 Metadata:
@Injectable()让 DI 知道可以注入这个类@Controller()让框架知道这是个控制器@Get()让框架知道路由和 HTTP 方法@Roles()让 Guard 知道权限要求@UseGuards()让框架知道要应用哪些 Guard
理解 Metadata,就能明白 NestJS 的装饰器的”魔法”本质上是元数据设计。
继续了解我的其他相关文章请移步: