【NestJS】07 装饰器说明书

4081 字
20 分钟
【NestJS】07 装饰器说明书

刚学 Nest 时我总犯迷糊:写接口分不清@Query@Param?注入依赖时总忘加@Inject?Filter 和 Interceptor 的装饰器总搞混?更头疼的是——重复的装饰器堆成山,内置功能满足不了定制需求?

今天这篇文章,我把**Nest 装饰器的“基础用法”和“自定义技巧”**揉成一份「实战指南」:从模块定义到请求处理,从依赖注入到异常控制,再到如何用自定义装饰器解决重复代码问题,一次性讲透!

一、先搭个 Demo 项目:边写边试才会会#

学装饰器最好的方式是动手实践,先创建一个干净的 Nest 项目:

Terminal window
nest new nest-decorator-guide -p npm # -p npm指定用npm安装依赖
cd nest-decorator-guide
npm run start:dev # 启动开发服务器,修改代码自动热更新

二、基础篇:Nest 常用装饰器“说明书”#

Nest 的核心功能几乎都通过装饰器实现,先把常用基础装饰器的用法和坑点讲清楚!

1. 模块与组件的“基础标签”:@Module、@Controller、@Injectable#

Nest 的「模块系统」是组织代码的核心,这三个装饰器是模块的“基石”:

@Module:定义模块的“说明书”#

模块是 Nest 项目的「最小功能单元」,用@Module标记的类会被 Nest 识别为模块,负责组织 Controller、Provider 和依赖关系:

src/app.module.ts
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
@Module({
imports: [], // 导入其他模块(比如数据库模块)
controllers: [AppController], // 模块的请求处理入口
providers: [AppService], // 模块的依赖(Service、Repository等)
exports: [], // 导出给其他模块用的Provider
})
export class AppModule {}

💡 小技巧:imports是“进口”(用别人的模块),exports是“出口”(让别人用自己的模块),两者是模块间的“双向门”~

@Controller:请求的“处理入口”#

@Controller用来定义请求的路由前缀,比如@Controller('users')表示所有/users开头的请求(如/users/list)都由这个 Controller 处理:

src/users/users.controller.ts
import { Controller, Get } from '@nestjs/common'
@Controller('users') // 路由前缀:/users
export class UsersController {
@Get('list') // 完整路由:/users/list
getUsers() {
return ['张三', '李四']
}
}

@Injectable:可注入的“依赖对象”#

Nest 的**依赖注入(DI)**是核心功能,@Injectable告诉 Nest:这个类可以被注入到其他类中(比如 Service 层)。必须加这个装饰器,否则无法注入!

src/users/users.service.ts
import { Injectable } from '@nestjs/common'
@Injectable()
export class UsersService {
getUsers() {
return ['张三', '李四'] // 实际项目中这里会调用数据库
}
}

然后在 Controller 中构造器注入(推荐写法):

src/users/users.controller.ts
import { Controller, Get } from '@nestjs/common'
import { UsersService } from './users.service'
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {} // 构造器注入
@Get('list')
getUsers() {
return this.usersService.getUsers() // 调用Service的方法
}
}

2. 依赖注入的“进阶技巧”:@Inject、@Optional、@Global#

依赖注入不是只有构造器注入一种方式,这些装饰器帮你解决更复杂的依赖场景:

@Inject:非类依赖的“注入令牌”#

如果依赖不是类(比如常量配置、动态对象),需要用@Inject指定注入 token(字符串或 Symbol)。比如用useValue注入配置:

src/config/config.module.ts
import { Module } from '@nestjs/common'
const config = {
// 常量配置
db: 'mongodb://localhost:27017/nest',
port: 3000,
}
@Module({
providers: [
{
provide: 'CONFIG', // token:字符串
useValue: config, // 注入的值
},
],
exports: ['CONFIG'], // 导出给其他模块用
})
export class ConfigModule {}

在 Service 中注入配置:

src/users/users.service.ts
import { Injectable, Inject } from '@nestjs/common'
@Injectable()
export class UsersService {
constructor(@Inject('CONFIG') private readonly config: Record<string, any>) {} // 用@Inject指定token
getDbUrl() {
return this.config.db // 访问配置中的db地址
}
}

@Optional:可选依赖的“安全锁”#

如果某个依赖不是必须的(比如可选的日志插件),加@Optional就不会因为找不到依赖而报错:

src/users/users.service.ts
import { Injectable, Inject, Optional } from '@nestjs/common'
@Injectable()
export class UsersService {
constructor(
@Optional() @Inject('LOGGER') private readonly logger?: LoggerService // 可选依赖
) {}
log(message: string) {
this.logger?.log(message) // 有logger就打印,没有就跳过
}
}

@Global:全局模块的“通行证”#

如果某个模块的 Provider全局都要用(比如数据库连接),用@Global标记为全局模块,不用每次都导入:

src/database/database.module.ts
import { Module, Global } from '@nestjs/common'
import { DatabaseService } from './database.service'
@Global() // 全局模块
@Module({
providers: [DatabaseService],
exports: [DatabaseService], // 必须导出,否则全局无法注入
})
export class DatabaseModule {}

3. 请求处理的“参数提取器”:@Query、@Param、@Body、@Headers#

写接口时最常用的就是提取请求参数,这些装饰器帮你快速拿到数据,不用手动解析req对象:

@Query:提取 URL 查询参数(?后面的部分)#

比如请求/users/list?page=1&size=10,用@Querypagesize

src/users/users.controller.ts
import { Controller, Get, Query } from '@nestjs/common'
@Controller('users')
export class UsersController {
@Get('list')
getUsers(
@Query('page') page: number = 1, // 取查询参数page,默认值1
@Query('size') size: number = 10 // 取查询参数size,默认值10
) {
return `当前页:${page},每页${size}条`
}
}

@Param:提取 URL 路径参数(/ 部分)#

比如请求/users/123,用@Param取路径中的id

src/users/users.controller.ts
import { Controller, Get, Param } from '@nestjs/common'
@Controller('users')
export class UsersController {
@Get(':id') // 路径中的id参数
getUserById(@Param('id') id: string) {
// 取路径参数id
return `用户ID:${id}`
}
}

@Body:提取请求体(POST/PUT 数据)#

POST 请求的 JSON 数据用@Body提取,配合**DTO(数据传输对象)**更规范(约束请求体格式):

// src/users/dto/create-user.dto.ts(定义DTO)
export class CreateUserDto {
name: string // 用户名(必填)
age?: number // 年龄(可选)
}

在 Controller 中接收请求体:

src/users/users.controller.ts
import { Controller, Post, Body } from '@nestjs/common'
import { CreateUserDto } from './dto/create-user.dto'
@Controller('users')
export class UsersController {
@Post()
createUser(@Body() createUserDto: CreateUserDto) {
// 用DTO约束格式
return `创建用户:${createUserDto.name},年龄${createUserDto.age}`
}
}

@Headers:提取请求头#

比如取Authorization头(token):

src/users/users.controller.ts
import { Controller, Get, Headers } from '@nestjs/common'
@Controller('users')
export class UsersController {
@Get('profile')
getProfile(@Headers('authorization') token: string) {
// 取Authorization头
return `Token:${token}`
}
}

4. 异常与响应的“控制工具”:@Catch、@UseFilters、@HttpCode、@Header#

处理异常和定制响应是接口开发的必经之路,这些装饰器帮你快速实现:

@Catch 与@UseFilters:处理未捕获异常#

Filter 用来处理未捕获的异常@Catch指定要处理的异常类型,@UseFilters应用 Filter:

// src/common/filters/http-exception.filter.ts(定义Filter)
import { ExceptionFilter, Catch, HttpException } from '@nestjs/common'
import { Request, Response } from 'express'
@Catch(HttpException) // 只处理Http异常(如404、500)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>()
const status = exception.getStatus() // 异常状态码
response.status(status).json({
// 自定义异常响应格式
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message,
})
}
}

在 Controller 中应用 Filter:

src/users/users.controller.ts
import { Controller, Get, UseFilters } from '@nestjs/common'
import { HttpExceptionFilter } from '../common/filters/http-exception.filter'
@Controller('users')
@UseFilters(HttpExceptionFilter) // 应用到整个Controller
export class UsersController {
@Get('error')
throwError() {
throw new HttpException('自定义错误', 400) // 抛出异常,由Filter处理
}
}

@HttpCode:修改响应状态码#

接口默认返回200 OK,用@HttpCode修改状态码:

src/users/users.controller.ts
import { Controller, Post, HttpCode } from '@nestjs/common'
@Controller('users')
export class UsersController {
@Post()
@HttpCode(201) // 创建成功返回201 Created
createUser() {
return '用户创建成功'
}
}

@Header:修改响应头#

比如给响应加Cache-Control头,禁止浏览器缓存:

src/users/users.controller.ts
import { Controller, Get, Header } from '@nestjs/common'
@Controller('users')
export class UsersController {
@Get('profile')
@Header('Cache-Control', 'no-store') // 禁止缓存
getProfile() {
return '用户信息'
}
}

5. 请求与响应的“直接操作”:@Req、@Res、@Next#

有时候需要直接操作reqres对象(比如自定义响应格式),这些装饰器帮你拿到原始对象:

@Req/@Request:注入请求对象#

@Req@Request的别名,用来注入 Express 的req对象:

src/users/users.controller.ts
import { Controller, Get, Req } from '@nestjs/common'
import { Request } from 'express'
@Controller('users')
export class UsersController {
@Get('info')
getUserInfo(@Req() req: Request) {
// 注入req对象
return `请求IP:${req.ip},请求方法:${req.method}`
}
}

@Res/@Response:注入响应对象#

⚠️ 重点注意:注入@Res后,Nest不会自动返回响应(避免和你手动返回的响应冲突),必须自己用res.send()res.json()返回:

src/users/users.controller.ts
import { Controller, Get, Res } from '@nestjs/common'
import { Response } from 'express'
@Controller('users')
export class UsersController {
@Get('custom')
customResponse(@Res() res: Response) {
// 注入res对象
res.status(200).json({
// 手动返回响应
code: 0,
message: '自定义响应',
data: null,
})
}
}

如果想让 Nest 继续处理返回值,加passthrough: true

@Get('custom')
customResponse(@Res({ passthrough: true }) res: Response) {
res.header('X-Custom-Header', 'value'); // 加自定义头
return 'Nest会处理这个返回值'; // Nest会把返回值作为响应内容
}

@Next:请求转发#

当多个 Handler 处理同一个路由时,用@Next把请求转发到下一个 Handler:

src/users/users.controller.ts
import { Controller, Get, Next } from '@nestjs/common'
import { NextFunction } from 'express'
@Controller('users')
export class UsersController {
@Get('multi')
firstHandler(@Next() next: NextFunction) {
// 第一个Handler
console.log('第一个Handler执行')
next() // 转发到下一个Handler
}
@Get('multi')
secondHandler() {
// 第二个Handler
return '第二个Handler的返回值'
}
}

三、进阶篇:自定义装饰器——从“重复造轮子”到“优雅封装”#

当内置装饰器不够用,或重复代码太多时,自定义装饰器就是解决之道!

1. 自定义方法装饰器:告别“原始”的@SetMetadata#

Nest 的@SetMetadata用来给 Handler 设置元数据(比如权限角色),但直接用太“裸”——我们可以封装成语义化的装饰器

原始痛点:@SetMetadata 的“不优雅”#

比如一个需要“admin”权限的接口,原始写法:

src/app.controller.ts
import { Controller, Get, SetMetadata, UseGuards } from '@nestjs/common'
import { AuthGuard } from './auth.guard'
@Controller()
export class AppController {
@Get('profile')
@SetMetadata('role', 'admin') // 写死的元数据键名,容易拼错
@UseGuards(AuthGuard) // 还要单独加Guard
getProfile() {
return '管理员专属页面'
}
}

封装自定义装饰器:@Role#

@SetMetadata封装成@Role,语义化更清晰:

src/decorators/role.decorator.ts
import { SetMetadata } from '@nestjs/common'
export const ROLE_KEY = 'role' // 用常量避免拼错元数据键名
export function Role(role: string) {
return SetMetadata(ROLE_KEY, role) // 相当于@SetMetadata('role', role)
}

使用自定义装饰器#

现在接口可以简化为:

src/app.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common'
import { Role } from './decorators/role.decorator'
import { AuthGuard } from './auth.guard'
@Controller()
export class AppController {
@Get('profile')
@Role('admin') // 语义化的装饰器,比@SetMetadata清爽10倍!
@UseGuards(AuthGuard)
getProfile() {
return '管理员专属页面'
}
}

2. 组合装饰器:用 applyDecorators 合并重复代码#

如果一个接口需要多个装饰器(比如@Get+@Role+@UseGuards),重复写三次太麻烦。Nest 的applyDecorators能帮我们把它们合并成一个装饰器,减少冗余!

合并前的“重复”写法#

比如三个接口都需要相同的装饰器组合:

src/app.controller.ts
@Controller()
export class AppController {
@Get('profile')
@Role('admin')
@UseGuards(AuthGuard)
getProfile() {
return '管理员页面'
}
@Get('dashboard')
@Role('admin')
@UseGuards(AuthGuard)
getDashboard() {
return '控制台'
}
@Get('settings')
@Role('admin')
@UseGuards(AuthGuard)
getSettings() {
return '设置页'
}
}

用 applyDecorators 合并:@Auth#

我们封装一个@Auth装饰器,把三个装饰器“打包”成一个:

src/decorators/auth.decorator.ts
import { applyDecorators, Get, UseGuards } from '@nestjs/common'
import { Role } from './role.decorator'
import { AuthGuard } from '../auth.guard'
// 传入接口路径和角色,返回组合后的装饰器
export function Auth(path: string, role: string) {
return applyDecorators(
Get(path), // 对应@Get(path)
Role(role), // 对应@Role(role)
UseGuards(AuthGuard) // 对应@UseGuards(AuthGuard)
)
}

使用组合装饰器#

现在三个接口可以简化成一行装饰器,代码瞬间清爽:

src/app.controller.ts
import { Controller } from '@nestjs/common'
import { Auth } from './decorators/auth.decorator'
@Controller()
export class AppController {
@Auth('profile', 'admin') // 一个装饰器顶三个!
getProfile() {
return '管理员页面'
}
@Auth('dashboard', 'admin')
getDashboard() {
return '控制台'
}
@Auth('settings', 'admin')
getSettings() {
return '设置页'
}
}

💡 小技巧:applyDecorators支持任意多个装饰器,顺序和你传入的一致——比如先加Get,再加Role,最后加UseGuards,和手动写的顺序完全一样!

3. 自定义参数装饰器:实现自己的@Query/@Headers#

Nest 的内置参数装饰器(如@Query@Headers)都是用createParamDecorator创建的。我们也能自己写一个,解决定制化需求

需求:保持请求头的键名原样#

内置@Headers会把键名转成小写(比如Authorization变成authorization),但我们想保持键名原样——自己写个@MyHeaders

实现自定义参数装饰器:@MyHeaders#

createParamDecorator创建参数装饰器,它的回调函数能拿到装饰器参数执行上下文ExecutionContext):

src/decorators/my-headers.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
import { Request } from 'express'
export const MyHeaders = createParamDecorator(
(key: string, ctx: ExecutionContext) => {
// 1. 从执行上下文拿到HTTP请求对象
const request: Request = ctx.switchToHttp().getRequest()
// 2. 有key就返回对应请求头,没有就返回所有请求头
return key ? request.headers[key] : request.headers
}
)

使用自定义参数装饰器#

和内置@Headers对比,看看效果:

src/app.controller.ts
import { Controller, Get, Headers } from '@nestjs/common'
import { MyHeaders } from './decorators/my-headers.decorator'
@Controller()
export class AppController {
@Get('headers')
getHeaders(
@Headers('Authorization') auth1: string, // 内置装饰器:转小写→undefined
@MyHeaders('Authorization') auth2: string // 自定义装饰器:保持原样→Bearer xxx
) {
return { auth1, auth2 }
}
}

再试一个:实现自己的@Query#

内置@Query能取 URL 查询参数,我们也能写个@MyQuery,逻辑完全一样:

src/decorators/my-query.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
import { Request } from 'express'
export const MyQuery = createParamDecorator(
(key: string, ctx: ExecutionContext) => {
const request: Request = ctx.switchToHttp().getRequest()
return key ? request.query[key] : request.query
}
)

用起来和内置@Query一毛一样:

src/app.controller.ts
@Get('query')
getQuery(
@Query('name') name1: string, // 内置
@MyQuery('name') name2: string, // 自定义
) {
return { name1, name2 }; // 结果完全一致!
}

4. 自定义类装饰器:给 Controller 加“全局”元数据#

除了方法和参数装饰器,我们还能给(比如 Controller)加自定义装饰器,用来设置全局元数据(比如模块标识)。

实现类装饰器:@ModuleTag#

SetMetadata给类设置元数据:

src/decorators/module-tag.decorator.ts
import { SetMetadata } from '@nestjs/common'
export const MODULE_TAG_KEY = 'module_tag' // 元数据键名(常量避免拼错)
export function ModuleTag(tag: string) {
return SetMetadata(MODULE_TAG_KEY, tag) // 给类设置模块标识
}

给 Controller 加类装饰器#

AppController@ModuleTag,标记它属于“user-module”:

src/app.controller.ts
import { Controller } from '@nestjs/common'
import { ModuleTag } from './decorators/module-tag.decorator'
@Controller()
@ModuleTag('user-module') // 给整个Controller加模块标识
export class AppController {
/* ... */
}

在 Guard 中取类的元数据#

Reflectorget方法,从 Controller 类中提取元数据:

src/auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { MODULE_TAG_KEY } from './decorators/module-tag.decorator'
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 取Controller类的元数据:MODULE_TAG_KEY
const moduleTag = this.reflector.get(MODULE_TAG_KEY, context.getClass())
console.log('当前模块:', moduleTag) // 打印:user-module
return true
}
}

四、总结:自定义装饰器的“核心玩法”#

最后把所有常用装饰器整理成速查表,方便你快速查询:

装饰器作用说明常用场景
@Module定义模块组织 Controller、Provider
@Controller定义请求处理的 Controller处理 HTTP 请求
@Injectable标记可注入的 ProviderService、Repository 层
@Inject指定注入的 token非类依赖注入
@Optional标记可选依赖可选的插件或配置
@Global标记全局模块数据库连接、配置服务
@Query提取 URL 查询参数/list?page=1
@Param提取 URL 路径参数/users/:id
@Body提取请求体POST/PUT 请求
@Headers提取请求头取 Authorization token
@Catch指定 Filter 处理的异常类型自定义异常处理
@UseFilters应用 Filter处理 Controller/接口异常
@HttpCode修改响应状态码自定义状态码(如 201)
@Header修改响应头加自定义头(如 Cache-Control)
@Req/@Request注入 req 对象手动处理请求
@Res/@Response注入 res 对象手动处理响应

到这里,Nest 的常用装饰器就讲完啦!其实核心逻辑是用装饰器标记「元数据」,让 Nest 知道如何处理类、方法和参数。熟练掌握这些装饰器,写 Nest 接口会像搭积木一样轻松~

通过上面的实战,我们学会了 Nest 自定义装饰器的三大场景关键技巧

  1. 自定义装饰器的三大场景
装饰器类型核心 API常见用途
方法装饰器SetMetadataapplyDecorators封装权限校验(如@Role)、合并重复装饰器(如@Auth
参数装饰器createParamDecorator定制请求参数提取(如@MyHeaders@MyQuery
类装饰器SetMetadata给 Controller/Module 设置全局元数据(如@ModuleTag
  1. 关键技巧
  • 语义化命名:装饰器名字要直观(比如@Role@SetMetadata好懂);
  • 常量存元数据键名:用常量避免拼错(比如ROLE_KEY = 'role');
  • 组合装饰器:用applyDecorators合并重复代码,减少冗余;
  • ExecutionContext:参数装饰器中用它拿到request/response,实现定制化提取。
  1. 最后一句提醒

自定义装饰器不是“花活”——它的核心是减少重复代码,提升代码可读性。如果一个装饰器能让你的代码少写 10 行重复逻辑,那就值得封装!

参考文档Nest 官方自定义装饰器文档 参考文档Nest 官方装饰器文档

到这里,Nest 的常用装饰器就讲完啦!其实核心逻辑是用装饰器标记「元数据」,😎 让 Nest 知道如何处理类、方法和参数。熟练掌握这些装饰器,写 Nest 接口会像搭积木一样轻松~

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

【NestJS】07 装饰器说明书
https://blog.fridolph.top/posts/2025-10-01__nest07-decorator/
作者
Fridolph
发布于
2025-10-01
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录