【NestJS】07 装饰器说明书
刚学 Nest 时我总犯迷糊:写接口分不清@Query和@Param?注入依赖时总忘加@Inject?Filter 和 Interceptor 的装饰器总搞混?更头疼的是——重复的装饰器堆成山,内置功能满足不了定制需求?
今天这篇文章,我把**Nest 装饰器的“基础用法”和“自定义技巧”**揉成一份「实战指南」:从模块定义到请求处理,从依赖注入到异常控制,再到如何用自定义装饰器解决重复代码问题,一次性讲透!
一、先搭个 Demo 项目:边写边试才会会
学装饰器最好的方式是动手实践,先创建一个干净的 Nest 项目:
nest new nest-decorator-guide -p npm # -p npm指定用npm安装依赖cd nest-decorator-guidenpm run start:dev # 启动开发服务器,修改代码自动热更新二、基础篇:Nest 常用装饰器“说明书”
Nest 的核心功能几乎都通过装饰器实现,先把常用基础装饰器的用法和坑点讲清楚!
1. 模块与组件的“基础标签”:@Module、@Controller、@Injectable
Nest 的「模块系统」是组织代码的核心,这三个装饰器是模块的“基石”:
@Module:定义模块的“说明书”
模块是 Nest 项目的「最小功能单元」,用@Module标记的类会被 Nest 识别为模块,负责组织 Controller、Provider 和依赖关系:
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 处理:
import { Controller, Get } from '@nestjs/common'
@Controller('users') // 路由前缀:/usersexport class UsersController { @Get('list') // 完整路由:/users/list getUsers() { return ['张三', '李四'] }}@Injectable:可注入的“依赖对象”
Nest 的**依赖注入(DI)**是核心功能,@Injectable告诉 Nest:这个类可以被注入到其他类中(比如 Service 层)。必须加这个装饰器,否则无法注入!
import { Injectable } from '@nestjs/common'
@Injectable()export class UsersService { getUsers() { return ['张三', '李四'] // 实际项目中这里会调用数据库 }}然后在 Controller 中构造器注入(推荐写法):
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注入配置:
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 中注入配置:
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就不会因为找不到依赖而报错:
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标记为全局模块,不用每次都导入:
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,用@Query取page和size:
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:
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 中接收请求体:
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):
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:
import { Controller, Get, UseFilters } from '@nestjs/common'import { HttpExceptionFilter } from '../common/filters/http-exception.filter'
@Controller('users')@UseFilters(HttpExceptionFilter) // 应用到整个Controllerexport class UsersController { @Get('error') throwError() { throw new HttpException('自定义错误', 400) // 抛出异常,由Filter处理 }}@HttpCode:修改响应状态码
接口默认返回200 OK,用@HttpCode修改状态码:
import { Controller, Post, HttpCode } from '@nestjs/common'
@Controller('users')export class UsersController { @Post() @HttpCode(201) // 创建成功返回201 Created createUser() { return '用户创建成功' }}@Header:修改响应头
比如给响应加Cache-Control头,禁止浏览器缓存:
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
有时候需要直接操作req或res对象(比如自定义响应格式),这些装饰器帮你拿到原始对象:
@Req/@Request:注入请求对象
@Req是@Request的别名,用来注入 Express 的req对象:
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()返回:
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:
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”权限的接口,原始写法:
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,语义化更清晰:
import { SetMetadata } from '@nestjs/common'
export const ROLE_KEY = 'role' // 用常量避免拼错元数据键名
export function Role(role: string) { return SetMetadata(ROLE_KEY, role) // 相当于@SetMetadata('role', role)}使用自定义装饰器
现在接口可以简化为:
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能帮我们把它们合并成一个装饰器,减少冗余!
合并前的“重复”写法
比如三个接口都需要相同的装饰器组合:
@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装饰器,把三个装饰器“打包”成一个:
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) )}使用组合装饰器
现在三个接口可以简化成一行装饰器,代码瞬间清爽:
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):
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对比,看看效果:
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,逻辑完全一样:
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一毛一样:
@Get('query')getQuery( @Query('name') name1: string, // 内置 @MyQuery('name') name2: string, // 自定义) { return { name1, name2 }; // 结果完全一致!}4. 自定义类装饰器:给 Controller 加“全局”元数据
除了方法和参数装饰器,我们还能给类(比如 Controller)加自定义装饰器,用来设置全局元数据(比如模块标识)。
实现类装饰器:@ModuleTag
用SetMetadata给类设置元数据:
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”:
import { Controller } from '@nestjs/common'import { ModuleTag } from './decorators/module-tag.decorator'
@Controller()@ModuleTag('user-module') // 给整个Controller加模块标识export class AppController { /* ... */}在 Guard 中取类的元数据
用Reflector的get方法,从 Controller 类中提取元数据:
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 | 标记可注入的 Provider | Service、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 自定义装饰器的三大场景和关键技巧:
- 自定义装饰器的三大场景
| 装饰器类型 | 核心 API | 常见用途 |
|---|---|---|
| 方法装饰器 | SetMetadata、applyDecorators | 封装权限校验(如@Role)、合并重复装饰器(如@Auth) |
| 参数装饰器 | createParamDecorator | 定制请求参数提取(如@MyHeaders、@MyQuery) |
| 类装饰器 | SetMetadata | 给 Controller/Module 设置全局元数据(如@ModuleTag) |
- 关键技巧
- 语义化命名:装饰器名字要直观(比如
@Role比@SetMetadata好懂); - 常量存元数据键名:用常量避免拼错(比如
ROLE_KEY = 'role'); - 组合装饰器:用
applyDecorators合并重复代码,减少冗余; - ExecutionContext:参数装饰器中用它拿到
request/response,实现定制化提取。
- 最后一句提醒
自定义装饰器不是“花活”——它的核心是减少重复代码,提升代码可读性。如果一个装饰器能让你的代码少写 10 行重复逻辑,那就值得封装!
参考文档:Nest 官方自定义装饰器文档 参考文档:Nest 官方装饰器文档
到这里,Nest 的常用装饰器就讲完啦!其实核心逻辑是用装饰器标记「元数据」,😎 让 Nest 知道如何处理类、方法和参数。熟练掌握这些装饰器,写 Nest 接口会像搭积木一样轻松~
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!