刚学 Nest 时我总犯迷糊:写接口分不清@Query和@Param?注入依赖时总忘加@Inject?Filter 和 Interceptor 的装饰器总搞混?更头疼的是——重复的装饰器堆成山,内置功能满足不了定制需求?
今天这篇文章,我把**Nest 装饰器的“基础用法”和“自定义技巧”**揉成一份「实战指南」:从模块定义到请求处理,从依赖注入到异常控制,再到如何用自定义装饰器解决重复代码问题,一次性讲透!
一、先搭个 Demo 项目:边写边试才会会
学装饰器最好的方式是动手实践,先创建一个干净的 Nest 项目:
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,用@Query取page和size:
// 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 路径参数(/:id 部分)
比如请求/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
有时候需要直接操作req或res对象(比如自定义响应格式),这些装饰器帮你拿到原始对象:
@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 中取类的元数据
用Reflector的get方法,从 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 | 标记可注入的 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 接口会像搭积木一样轻松~
- 本文链接:https://fridolph.top/posts/2025-10-01__nest07-decorator
- 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 许可协议。