INTRODUCTION
一、简介
Nest(NestJS)是一款用于构建高效、可扩展的 Node.js 服务端应用框架。
- 采用渐进式 JavaScript 设计,基于并完整支持 TypeScript(同时也支持纯 JavaScript 开发)。
融合了三大编程范式:
- OOP:面向对象编程
- FP:函数式编程
- FRP:函数式响应式编程
底层原理:
- 默认使用成熟的 HTTP 服务框架 Express。
- 也可灵活配置切换为 Fastify。
框架特点:
- 在 Express / Fastify 等通用 Node 框架之上做了一层上层抽象。
同时直接暴露底层原生 API,开发者可以自由使用平台上大量的第三方模块。
Node.js 服务端、TS 优先、三大编程范式、Express/Fastify 底层、抽象 + 原生 API 双支持二、设计理念
近年来,得益于 Node.js,JavaScript 已经成为前后端通用的“网络通用语言”。
这也催生了 Angular、React、Vue 等优秀前端框架,大幅提升开发效率,让前端应用更快速、可测试、易扩展。
但在 Node.js(服务端 JavaScript)生态中:
- 虽然已有大量优质库、工具和辅助模块
- 但没有一个能有效解决架构问题
NestJS 提供开箱即用的应用架构,让开发者和团队可以轻松构建:
- 高可测试性
- 高可扩展性
- 低耦合
- 易维护
的应用程序。
其架构设计深受 Angular 启发。
三、安装
方式 1:使用 Nest CLI 搭建(推荐新手)
通过命令行脚手架直接生成项目目录、核心文件和标准目录结构。
# 全局安装 CLI
npm i -g @nestjs/cli
# 创建项目
nest new project-name提示:
如需创建更严格类型检查的 TypeScript 项目,可添加 --strict 参数:
nest new project-name --strict方式 2:Git 克隆官方起步模板
# 克隆 TS 模板
git clone https://github.com/nestjs/typescript-starter.git project
cd project
npm install
npm run start提示:
如果不想克隆 Git 历史,可以使用 degit。
启动后访问:
如需纯 JavaScript 版本,将仓库地址换成 javascript-starter.git 即可。
方式 3:从零手动安装(不推荐新手)
只安装核心包,需自己配置项目文件,最少依赖:
NestJS 四大核心依赖作用
核心必装依赖(缺一不可,支撑 Nest 基础运行):
- @nestjs/core:Nest 核心引擎,提供应用初始化、模块挂载、依赖注入底层实现,是应用运行基石。
- @nestjs/common:通用工具库,提供控制器、服务等核心装饰器及异常处理、管道等基础组件,简化开发。
- rxjs:响应式编程库,处理异步数据流,支撑 Nest 异步操作(如请求、数据库查询)。
- reflect-metadata:元数据反射工具,支撑装饰器识别和依赖注入机制,保障组件自动装配。
补充:CLI 搭建/克隆模板会自动安装,从零搭建需手动安装这四个依赖。
OVERVIEW
first steps
一、入门基础
- 入门目标
通过构建基础CRUD应用,掌握Nest核心基础知识和关键组成部分,覆盖入门级核心知识点。
- 语言支持
- 兼容TypeScript(推荐)和纯JavaScript,纯JS需搭配Babel编译器(Nest依赖最新语言特性)。
- 示例默认使用TypeScript,代码片段可点击右上角按钮,切换为纯JavaScript语法。
- 前置条件
必须安装Node.js,且版本≥20,否则无法正常搭建和运行Nest项目。
二、项目搭建(推荐CLI方式)
npm i -g @nestjs/cli # 全局安装Nest CLI工具
nest new project-name # 创建新的Nest项目(project-name为自定义项目名)提示:添加--strict标志,可创建启用TypeScript严格特性集的项目。
项目核心目录与文件
CLI搭建后会生成src目录,包含5个核心文件,分工明确:
- app.controller.ts:基础控制器,包含单一路由,负责接收和响应请求。
- app.controller.spec.ts:控制器的单元测试文件,用于测试控制器功能。
- app.module.ts:应用的根模块,是Nest应用的核心组织单元。
- app.service.ts:基础服务,包含单个方法,用于封装业务逻辑。
- main.ts:应用入口文件,通过NestFactory创建应用实例并启动。
三、平台支持
- Nest是平台无关框架,开箱即支持两种HTTP平台:Express(默认)和Fastify。
- 默认使用@nestjs/platform-express包,Express成熟稳定、社区资源丰富;Fastify高性能、低开销,可按需切换。
- 无需指定平台类型,仅当需要访问底层平台API时,才需在创建应用实例时指定类型。
四、应用运行命令
npm run start # 普通启动,监听HTTP请求
npm run start:dev # 热更新启动,监听文件变化,自动重新编译并重启服务器
npm run start -- -b swc # 加速构建(构建速度提升20倍)补充:应用启动后,打开浏览器访问http://localhost:3000/,可看到Hello World! 提示,说明启动成功。
五、代码检查与格式化
CLI生成的项目默认预装eslint(代码检查工具)和prettier(代码格式化工具),支持IDE集成和无头环境使用。
npm run lint # 检查代码规范,并自动修复可修复的问题
npm run format # 格式化代码,统一代码风格Controllers
一、控制器核心定位
Controllers(控制器)负责处理客户端传入的请求,并向客户端返回响应,是Nest应用中处理请求的核心组件。
- 核心作用:接收特定请求、执行对应逻辑、返回响应,路由机制决定了哪个控制器处理哪个请求。
- 一个控制器可包含多个路由,每个路由对应不同的请求处理逻辑(动作)。
- 创建方式:通过类和装饰器定义,装饰器为类添加元数据,让Nest生成请求与控制器的路由映射。
提示:使用CLI命令快速生成CRUD控制器(带内置验证):nest g resource [name];快速生成普通控制器:nest g controller [name]。
二、核心基础:路由(Routing)
- 路由装饰器核心用法
- @Controller():定义基础控制器,可指定路由路径前缀(如@Controller('cats')),用于分组相关路由、减少重复代码。
- HTTP请求方法装饰器:@Get()、@Post()、@Put()、@Delete()等,修饰控制器方法,指定该方法处理的HTTP请求类型。
- 路由路径拼接:控制器前缀 + 方法装饰器中的路径(可选),例如:@Controller('cats') + @Get('breed') → 路由为GET /cats/breed。
- 基础示例(cats.controller.ts)
import { Controller, Get } from '@nestjs/common';
@Controller('cats') // 路由前缀:/cats
export class CatsController {
@Get() // 处理GET /cats请求
findAll(): string {
return 'This action returns all cats';
}
}- 路由通配符
支持模式匹配路由,常用作为通配符(匹配路径末尾任意字符),例如:@Get('abcd/') 可匹配 /abcd/123、/abcd/abc 等路径。
注意:Express v5及以上对路由要求更严格,Nest提供兼容层,可正常使用通配符;中间带的路由(如ab*cd)仅Express支持(需命名通配符),Fastify不支持。
三、请求处理核心细节
- 请求对象(Request Object)
- 通过@Req()(或@Request())装饰器注入底层平台(默认Express)的请求对象,需导入express的Request类型(需安装@types/express)。
- 无需手动获取请求参数,Nest提供专用装饰器,简化开发(常用装饰器如下):
@Req() / @Request() | 获取完整请求对象(req) |
|---|---|
@Param(key?) | 获取路由参数(req.params),可指定具体key(如@Param('id')) |
@Body(key?) | 获取请求体(req.body),可指定具体key,常用于POST/PUT请求 |
@Query(key?) | 获取查询参数(req.query),可指定具体key |
@Headers(name?) | 获取请求头(req.headers),可指定具体请求头名称 |
- 路由参数(Route Parameters)
用于接收URL中的动态数据(如GET /cats/1,获取id为1的猫),通过@Param()装饰器访问。
// 路由:GET /cats/:id
@Get(':id')
findOne(@Param('id') id: string): string {
return `This action returns a #${id} cat`;
}注意:带参数的路由需声明在静态路由之后,避免拦截静态路由请求。
- 请求体(Request Payloads)
- 通过@Body()装饰器获取请求体,TypeScript环境下需定义DTO(数据传输对象),指定请求体格式。
- DTO推荐用类(而非接口),因为接口会在编译时被移除,Nest无法在运行时引用(如管道验证需依赖DTO元数据)。
// create-cat.dto.ts(DTO定义)
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
// cats.controller.ts(使用DTO)
import { Controller, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
@Controller('cats')
export class CatsController {
@Post() // 处理POST /cats请求
create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
}- 查询参数(Query Parameters)
通过@Query()装饰器获取URL中的查询参数(如GET /cats?age=2&breed=Persian),支持复杂查询参数(需配置HTTP适配器的查询解析器)。
四、响应处理(Response Handling)
- 两种响应方式
标准方式(推荐):直接返回值,Nest自动处理序列化和状态码。
- 返回对象/数组:自动序列化为JSON;返回基础类型(字符串、数字):直接返回值。
- 默认状态码:GET/PUT/DELETE等为200,POST为201,可通过@HttpCode()装饰器修改。
库特定方式:通过@Res()(或@Response())注入底层响应对象(如Express的res),手动控制响应。
- 注意:使用@Res()后,Nest会禁用标准方式;需手动调用res.send()/res.json()等,否则请求会挂起。
- 兼容两种方式:添加passthrough: true(@Res({ passthrough: true })),可手动设置响应头/ cookies,同时让Nest处理返回值。
- 状态码与响应头设置
- 状态码:@HttpCode(状态码) 装饰器(需从@nestjs/common导入),动态状态码可通过响应对象或抛出异常设置。
- 响应头:@Header(键, 值) 装饰器,或通过响应对象手动设置(res.header())。
import { Controller, Post, HttpCode, Header } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
@HttpCode(204) // 设置状态码为204
@Header('Cache-Control', 'no-store') // 设置响应头
create() {
return 'This action adds a new cat';
}
}- 重定向(Redirection)
通过@Redirect(url, 状态码) 装饰器实现重定向,状态码默认302;动态重定向可返回一个包含url和statusCode的对象,覆盖装饰器参数。
五、控制器进阶特性
1. 子域名路由(Sub-domain Routing)
@Controller()装饰器可传入host选项,指定请求的主机名(如@Controller({ host: 'admin.example.com' })),支持动态主机名(用:占位符),通过@HostParam()获取主机名参数。
注意:Fastify不支持嵌套路由器,使用子域名路由推荐用默认Express适配器。
2. 异步处理(Asynchronicity)
- 完全支持async/await,异步方法返回Promise,Nest自动解析。
- 支持返回RxJS Observable流,Nest自动订阅,流完成后返回最终值。
3. 状态共享(State Sharing)
Nest中几乎所有资源(数据库连接池、单例服务等)都在请求间共享,Node.js不使用“请求/响应多线程无状态模型”,因此使用单例实例是安全的;特殊场景需请求级生命周期,可配置注入作用域。???
六、控制器注册与完整示例
- 控制器注册
控制器必须属于某个模块,需在@Module()装饰器的controllers数组中注册(通常注册在根模块AppModule或对应功能模块)。
// app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
@Module({
controllers: [CatsController], // 注册CatsController
})
export class AppModule {}- 完整CRUD控制器示例
import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';
@Controller('cats')
export class CatsController {
@Post() // 新增
create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
@Get() // 查询所有
findAll(@Query() query: ListAllEntities) {
return `This action returns all cats (limit: ${query.limit} items)`;
}
@Get(':id') // 查询单个
findOne(@Param('id') id: string) {
return `This action returns a #${id} cat`;
}
@Put(':id') // 更新
update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
return `This action updates a #${id} cat`;
}
@Delete(':id') // 删除
remove(@Param('id') id: string) {
return `This action removes a #${id} cat`;
}
}七、关键提示
- DTO用于规范请求数据格式,配合ValidationPipe可实现请求数据校验和过滤(过滤非白名单属性)。
- 库特定响应方式(@Res())会使代码平台依赖,增加测试难度,非必要不推荐使用。
- CLI生成器可自动创建控制器及相关模板,提升开发效率。
Provides
一、Providers 核心定位
Providers(提供者)是 Nest 的核心概念,许多基础 Nest 类(如服务、仓库、工厂、工具类等)都可作为提供者。
- 核心思想:可作为依赖被注入,使对象之间形成各种关联关系,对象间的“连接”工作主要由 Nest 运行时系统处理。
- 核心作用:封装业务逻辑、数据处理(如数据存储与查询)等复杂任务,供控制器(Controller)调用,实现“控制器负责请求响应、提供者负责业务逻辑”的分离。
- 本质:普通 JavaScript/TypeScript 类,需在 Nest 模块中声明为提供者才能被 Nest 管理和注入。
提示:推荐遵循 SOLID 原则设计和组织依赖,让代码更具可维护性和扩展性。
**SOLID 原则是 5 条面向对象设计(OOP)的核心准则
S(单一职责原则) 核心:一个 Provider(如 Service)只负责一项核心职责,不承担无关逻辑。
O(开放 / 封闭原则) 核心:Provider 对扩展开放、对修改封闭。新增功能时,通过扩展类 / 方法实现,不改动原有代码。
L(里氏替换原则) 核心:子类可完全替代父类,且不影响程序正常运行(父类与子类遵循同一接口 / 规范)。
I(接口隔离原则) 核心:不强迫 Provider 依赖它用不到的接口 / 方法,拆分细化接口,避免 “胖接口”。
D(依赖倒置原则) 核心:依赖抽象(接口 / 抽象类),不依赖具体实现;高层模块(如 Controller)不依赖低层模块(如 Service),二者都依赖抽象。**
二、核心实现:Services(服务)
Services 是最常用的 Providers 类型,专门用于封装业务逻辑,是控制器的“业务处理助手”。
- 服务的创建与基础示例
通过 @Injectable() 装饰器标记类为服务(提供者),CLI 命令快速生成服务:nest g service [name](如 nest g service cats)。
// cats.service.ts(服务定义)
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface'; // 导入接口规范数据格式
@Injectable() // 标记此类为Nest可管理的提供者
export class CatsService {
private readonly cats: Cat[] = []; // 模拟数据存储
// 新增猫的逻辑
create(cat: Cat) {
this.cats.push(cat);
}
// 查询所有猫的逻辑
findAll(): Cat[] {
return this.cats;
}
}2. 配套接口(Interface)
通常会用接口(Interface)规范数据格式(如 Cat 接口),仅用于 TypeScript 类型校验,编译后会被移除。
// interfaces/cat.interface.ts
export interface Cat {
name: string;
age: number;
breed: string;
}3. 服务的使用(注入到控制器)
通过构造函数注入服务,控制器即可调用服务中的方法,实现业务逻辑分离。
// cats.controller.ts(使用CatsService)
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
// 构造函数注入CatsService,private关键字简化声明+初始化
constructor(private catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
// 调用服务的create方法处理新增逻辑
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
// 调用服务的findAll方法获取数据
return this.catsService.findAll();
}
}三、核心机制:依赖注入(Dependency Injection)
依赖注入(DI)是 Nest 的核心设计模式,Nest 基于 TypeScript 的特性,让依赖管理变得简单。
- 核心原理:Nest 会根据构造函数中声明的类型,自动解析并注入对应的依赖实例(如构造函数中声明
private catsService: CatsService,Nest 会自动创建/获取 CatsService 实例并注入)。 - 单例特性:默认情况下,提供者是单例的(整个应用生命周期内只有一个实例),多次注入会获取同一个实例。
- 优势:降低代码耦合度,便于测试和维护,无需手动创建和管理依赖实例。
四、Providers 核心特性
1. 作用域(Scopes)
提供者的生命周期(作用域)默认与应用生命周期一致:
- 应用启动时:所有依赖被解析,提供者实例化。
- 应用关闭时:所有提供者被销毁。
- 可选作用域:可配置为“请求作用域”(request-scoped),即每个请求对应一个提供者实例,生命周期与单个请求绑定(详见注入作用域章节)。
2. 自定义提供者(Custom Providers)
Nest 内置控制反转(IoC)容器,支持多种方式定义提供者,不止于类:
- 支持类型:普通值、类、异步工厂、同步工厂等。
- 核心用途:灵活配置依赖,如注入第三方库、模拟测试依赖等(详见依赖注入章节)。
3. 可选提供者(Optional Providers)
用于处理“非必需依赖”(如可选的配置对象),即使依赖未被提供,也不会报错,可使用默认值。
通过 @Optional() 装饰器标记为可选依赖,通常配合自定义提供者的令牌使用。
import { Injectable, Optional, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
// @Optional() 标记为可选依赖,@Inject() 指定自定义令牌
constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}4. 属性注入(Property-based Injection)
除了常用的“构造函数注入”,还可通过@Inject() 装饰器直接注入到类属性上。
适用场景:子类继承父类时,避免通过 super() 层层传递依赖(非必要不推荐,构造函数注入更清晰)。
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class HttpService<T> {
// 直接注入到属性上
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}五、提供者注册(Provider Registration)
提供者必须在某个 Nest 模块中注册,才能被 Nest 识别、管理和注入(与控制器注册类似)。
注册方式:在模块的 @Module() 装饰器中,将提供者添加到 providers 数组中。
// app.module.ts(注册CatsService)
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
@Module({
controllers: [CatsController], // 注册控制器
providers: [CatsService], // 注册提供者(服务)
})
export class AppModule {}注册后,Nest 即可正常解析控制器(如 CatsController)对该提供者的依赖。
六、目录结构参考
结合此前的控制器、DTO、接口,整合提供者后的标准目录结构如下:
src
├─ cats
│ ├─ dto
│ │ └─ create-cat.dto.ts
│ ├─ interfaces
│ │ └─ cat.interface.ts
│ ├─ cats.controller.ts
│ └─ cats.service.ts
├─ app.module.ts
└─ main.ts七、手动实例化(补充)
默认情况下,Nest 自动处理依赖的解析和实例化,特殊场景下可手动获取/实例化提供者:
- Module reference:动态获取已存在的提供者实例或动态实例化提供者。
- Standalone applications:在 bootstrap() 函数中获取提供者(如独立应用、启动时使用配置服务)。
八、关键提示
- @Injectable() 装饰器是核心:必须给提供者类添加该装饰器,否则 Nest 无法识别和管理。
- 分离原则:控制器只处理 HTTP 请求/响应,复杂业务逻辑全部交给提供者(服务),提升代码可维护性。
- 动态获取已存在的提供者实例或动态实例化提供者 (默认情况下,Nest 会在应用启动时(bootstrap 阶段),自动解析所有依赖、实例化提供者(单例),并在需要时(如控制器注入)自动注入。而 “动态获取 / 动态实例化”,是打破这种 “启动时自动处理” 的固定模式,在应用运行过程中(比如请求处理中、某个逻辑执行时),手动获取已经存在的提供者实例,或手动创建新的提供者实例(按需实例化,而非启动时就创建)。)
Modules
一、Modules 核心定位
Modules是被@Module()装饰器标记的类,核心作用是作为应用“组织单元”,封装相关控制器、提供者,管理依赖、划分功能边界,让应用结构清晰可维护。
- 核心特性:每个Nest应用至少有一个根模块(AppModule),是Nest构建应用图(解析依赖关系)的起点。
- 使用场景:小型应用可仅用根模块;中大型应用拆分多个模块,每个模块封装一组紧密相关功能(贴合SOLID原则)。
提示:CLI快速生成模块:nest g module [name](如nest g module cats)。
二、@Module() 装饰器核心配置
@Module()接收一个对象,包含4个核心属性(按需配置,无需全部填写),用于描述模块配置:
providers | 模块内的提供者(如Service),由Nest实例化,可在当前模块内共享。 |
|---|---|
controllers | 模块内的控制器,Nest自动实例化并注册路由。 |
imports | 需导入的模块列表,导入模块需导出当前模块所需提供者(导入才可使用)。 |
exports | 模块对外暴露的提供者(模块“公共API”),其他模块导入后可使用,可导出提供者本身或其令牌。 |
关键规则:模块默认封装提供者,仅可注入“当前模块内提供者”或“其他导入模块明确导出的提供者”。
三、常见模块类型及用法
1. 功能模块(Feature Modules)
最常用,封装某一特定功能的控制器和提供者,划分功能边界(如Cats相关功能封装为CatsModule)。
示例:创建并使用CatsModule
// cats/cats.module.ts(功能模块定义)
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}需导入根模块才可使用:
// app.module.ts(根模块导入)
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({ imports: [CatsModule] })
export class AppModule {}2. 共享模块(Shared Modules)
Nest模块默认是单例,可在多模块间共享同一提供者实例,只需导出该提供者即可。
- 核心用法:在
exports数组中添加需共享的提供者,其他模块导入该模块即可使用(共享同一实例)。 - 优势:避免重复注册,减少内存占用,保证状态一致性。
// cats/cats.module.ts(共享CatsService)
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService] // 导出供其他模块共享
})
export class CatsModule {}3. 模块重导出(Module Re-exporting)
模块可重导出其导入的模块,其他模块导入当前模块后,即可使用被重导出模块的功能,无需单独导入。
// core.module.ts(重导出CommonModule)
import { Module } from '@nestjs/common';
import { CommonModule } from './common.module';
@Module({
imports: [CommonModule],
exports: [CommonModule] // 重导出
})
export class CoreModule {}4. 全局模块(Global Modules)
用于提供全局可用的提供者(如工具类、数据库连接),无需重复导入,用@Global()标记。
- 核心用法:添加
@Global(),同时导出需全局共享的提供者;只需注册一次(通常由根模块注册)。 - 注意:不推荐过度使用,会增加模块耦合,优先用“导入+导出”共享功能。
// cats/cats.module.ts(全局模块)
import { Module, Global } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService] // 必须导出才可全局使用
})
export class CatsModule {}5. 动态模块(Dynamic Modules)
可在运行时配置的模块,适用于灵活定制场景(如根据配置创建数据库连接)。
- 核心用法:通过静态方法(通常
forRoot()/forRootAsync())返回DynamicModule,动态配置providers、exports。 - 特性:静态方法可同步/异步返回;动态配置扩展(不覆盖)模块默认配置。
// database/database.module.ts(动态模块)
import { Module, DynamicModule } from '@nestjs/common';
import { createDatabaseProviders } from './database.providers';
import { Connection } from './connection.provider';
@Module({ providers: [Connection], exports: [Connection] })
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities); // 动态生成提供者
return { module: DatabaseModule, providers, exports: providers };
}
}使用示例(导入时传配置):
// app.module.ts
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
import { User } from './users/entities/user.entity';
@Module({ imports: [DatabaseModule.forRoot([User])] })
export class AppModule {}四、模块相关补充
1. 模块中的依赖注入
模块类可注入提供者(用于自身配置),但不能作为提供者被注入(会导致循环依赖)。
模块(如 UserModule)管理着服务(UserService),若服务再注入模块,会形成「模块 → 服务 → 模块」的循环
模块是“管理者”(管理控制器、提供者),可以用提供者来辅助自己工作,但模块不能当“被管理者”(不能被当作提供者,供其他组件注入使用),否则会出现依赖循环,Nest无法解析。
// cats/cats.module.ts(模块注入提供者)
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({ controllers: [CatsController], providers: [CatsService] })
export class CatsModule {
constructor(private catsService: CatsService) {} // 模块注入
}
- 标准目录结构
src
├─ cats // 功能模块目录
│ ├─ dto // 数据传输对象
│ ├─ interfaces // 接口定义
│ ├─ cats.controller.ts
│ ├─ cats.module.ts // 模块文件
│ └─ cats.service.ts // 提供者文件
├─ app.module.ts // 根模块
└─ main.ts // 入口文件五、关键提示
- 模块核心价值:划分功能边界、管理依赖、实现复用,是中大型Nest应用的核心组织方式。
- exports注意:仅导出其他模块需用的提供者,减少模块耦合。
- 全局模块vs共享模块:优先用共享模块(结构清晰),避免过度使用全局模块。
- 动态模块核心:运行时配置,适配多场景(如多数据库、环境差异化配置)。
Middleware
一、核心概念:NestJS 中间件是什么?
NestJS 中间件本质上和 Express 中间件等价,是在路由处理器执行前被调用的函数/类,它能访问 request(请求)、response(响应)对象,以及请求-响应周期中的 next() 函数,主要作用包括:
- 执行任意代码(如日志打印、权限校验);
- 修改请求/响应对象(如添加请求头、解析参数);
- 结束请求-响应周期(如直接返回错误响应);
- 调用下一个中间件(必须调用
next(),否则请求会“挂起”)。
二、NestJS 中间件的两种实现方式
1. 类式中间件(推荐:需要依赖注入时) nestmiddleware+injectable
适用于中间件需要注入服务/依赖的场景,必须实现 NestMiddleware 接口,并使用 @Injectable() 装饰器。
// src/common/middleware/logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
// 核心方法:use 接收 req、res、next 三个参数
use(req: Request, res: Response, next: NextFunction) {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl}`);
// 必须调用 next(),否则请求会卡住
next();
}
}2. 函数式中间件(推荐:无依赖时)
更简洁,无需类和装饰器,适合简单场景(如基础日志、跨域)。
// src/common/middleware/logger.middleware.ts
import { Request, Response, NextFunction } from 'express';
// 直接导出一个函数即可
export function logger(req: Request, res: Response, next: NextFunction) {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl}`);
next();
}三、中间件的应用方式
1. 局部应用(指定路由/控制器)
通过模块实现 NestModule 接口,在 configure() 方法中使用 MiddlewareConsumer 绑定中间件,支持:
- 指定单个/多个路由;
- 指定请求方法(GET/POST 等);
- 指定控制器;
- 排除特定路由。
示例1:绑定到指定路由(GET /cats)
// src/app.module.ts
import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsController } from './cats/cats.controller';
@Module({
controllers: [CatsController],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware) // 也可以用函数式:apply(logger)
.exclude( // 排除不需要应用的路由
{ path: 'cats/ignore', method: RequestMethod.GET }, // 排除 GET /cats/ignore
'cats/*/delete' // 通配符排除
)
.forRoutes(
{ path: 'cats', method: RequestMethod.GET }, // 仅绑定 GET /cats
CatsController // 也可以直接绑定整个控制器(所有 /cats 下的路由)
);
}
}2. 全局应用(所有路由)
通过 app.use() 绑定到所有路由,注意:全局中间件无法使用依赖注入(类式中间件不行),优先用函数式。
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { logger } from './common/middleware/logger.middleware';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局应用中间件(所有路由都会触发)
app.use(logger);
await app.listen(3000);
}
bootstrap();3. 多个中间件顺序执行
apply() 方法可传入多个中间件,按传入顺序执行(如先跨域 → 再安全校验 → 再日志)。
// src/app.module.ts
import * as cors from 'cors';
import * as helmet from 'helmet'; // 安全相关中间件
import { logger } from './common/middleware/logger.middleware';
// 在 configure 中
consumer
.apply(cors(), helmet(), logger) // 顺序:cors → helmet → logger
.forRoutes(CatsController);四、关键注意事项
通配符路由:
'abcd/*':匹配abcd/1、abcd/abc,但不匹配abcd/;'abcd/{*splat}':加花括号后,可匹配abcd/、abcd/123等所有以abcd/开头的路由;
- Express/Fastify 差异:两者中间件的方法签名不同,需根据适配器调整;
全局中间件依赖注入:如果全局中间件需要依赖,不要用
app.use(),而是在模块中通过forRoutes('*')绑定类式中间件:// 模块中绑定全局类式中间件(支持依赖注入)
默认中间件:使用 Express 适配器时,NestJS 会默认注册
json()和urlencoded()中间件,如需自定义,创建应用时关闭:
总结
- NestJS 中间件分类式(支持依赖注入)和函数式(简洁无依赖),核心是实现
use()方法/函数并调用next(); - 中间件可局部绑定(指定路由/控制器,通过
MiddlewareConsumer)或全局绑定(所有路由,通过app.use()); configure(consumer: MiddlewareConsumer)
configure(consumer:middlewareconsumer) - 关键技巧:用
exclude()排除特定路由、用通配符匹配批量路由、多个中间件按apply()传入顺序执行。
通过这些方式,你可以灵活实现日志、权限校验、参数解析、跨域处理等通用功能,让业务代码更聚焦核心逻辑。
Exception filters
一、Exception filters 核心概念
Nest 内置异常层(exceptions layer),负责处理应用中所有未被手动处理的异常;当应用代码未捕获异常时,该层会自动捕获并返回用户友好的响应。
默认由内置全局异常过滤器处理,核心负责 HttpException 及其子类异常;对于未识别的异常(非 HttpException 及其子类),默认返回如下 JSON 响应:
{
"statusCode": 500,
"message": "Internal server error"
}提示:全局异常过滤器部分支持 http-errors 库,任何包含 statusCode 和 message 属性的抛出异常,都会被正确格式化返回(而非默认的 500 异常)。
二、抛出标准异常(Throwing standard exceptions)
Nest 从 @nestjs/common 暴露内置 HttpException 类,适用于 HTTP REST/GraphQL API,推荐在错误场景下返回标准 HTTP 响应对象。
2.1 基础示例
// cats.controller.ts
import { Get, HttpException, HttpStatus } from '@nestjs/common';
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}客户端调用该接口时,响应如下:
{
"statusCode": 403,
"message": "Forbidden"
}2.2 HttpException 构造函数参数
- response(必选):定义 JSON 响应体,可传入字符串(仅覆盖 message)或对象(覆盖整个响应体);
- status(必选):定义 HTTP 状态码,推荐使用
HttpStatus枚举(来自@nestjs/common); - options(可选):传入错误原因(cause),不序列化到响应体,仅用于日志记录。
2.3 自定义响应体 + 错误原因示例
// cats.controller.ts
@Get()
async findAll() {
try {
await this.service.findAll()
} catch (error) {
throw new HttpException({
status: HttpStatus.FORBIDDEN, //httpstatus/forbidden
error: 'This is a custom message',
}, HttpStatus.FORBIDDEN, {
cause: error // 错误原因,用于日志
});
}
}对应响应:
{
"status": 403,
"error": "This is a custom message"
}三、异常日志(Exceptions logging)
- 默认情况下,异常过滤器不记录
HttpException及其子类、WsException、RpcException等内置异常(视为正常应用流程); - 这些异常均继承自
IntrinsicException(来自@nestjs/common),用于区分“正常应用异常”和“非预期异常”; - 若需记录这些异常,需自定义异常过滤器。
四、自定义异常(Custom exceptions)
多数场景可直接使用内置异常,若需定制,推荐继承 HttpException 构建异常层级,Nest 会自动识别并处理响应。
示例:自定义 ForbiddenException
// forbidden.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
export class ForbiddenException extends HttpException {
constructor() {
super('Forbidden', HttpStatus.FORBIDDEN);
}
}使用方式(与内置异常一致):
// cats.controller.ts
@Get()
async findAll() {
throw new ForbiddenException();
}五、内置 HTTP 异常(Built-in HTTP exceptions)
Nest 提供一系列继承自 HttpException 的标准异常(均来自 @nestjs/common),覆盖常见 HTTP 错误场景:
- BadRequestException、UnauthorizedException、NotFoundException、ForbiddenException
- NotAcceptableException、RequestTimeoutException、ConflictException、GoneException
- HttpVersionNotSupportedException、PayloadTooLargeException、UnsupportedMediaTypeException
- UnprocessableEntityException、InternalServerErrorException、NotImplementedException
- ImATeapotException、MethodNotAllowedException、BadGatewayException
- ServiceUnavailableException、GatewayTimeoutException、PreconditionFailedException
内置异常使用示例(带原因和描述)
throw new BadRequestException('Something bad happened', {
cause: new Error(), // 错误原因(日志用)
description: 'Some error description' // 错误描述(响应中显示)
});对应响应:
{
"message": "Something bad happened",
"error": "Some error description",
"statusCode": 400
}六、异常过滤器(Exception filters)
内置过滤器可处理多数场景,若需完全控制异常层(如添加日志、自定义响应格式),可自定义异常过滤器,控制响应流程和内容。
6.1 自定义 HttpExceptionFilter(捕获 HttpException)
// http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException) // 绑定要捕获的异常类型
export class HttpExceptionFilter implements ExceptionFilter {
// 必须实现 catch 方法
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 切换到 HTTP 上下文
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,
});
}
}提示:所有异常过滤器需实现 ExceptionFilter<T> 接口,T 为要捕获的异常类型,必须实现 catch(exception: T, host: ArgumentsHost) 方法。
警告:若使用 @nestjs/platform-fastify,需用 response.send() 替代 response.json(),并导入 fastify 相关类型。
6.2 @Catch() 装饰器
用于绑定过滤器要捕获的异常类型,可传入单个类型或多个逗号分隔的类型,仅处理指定类型的异常。
6.3 Arguments Host
强大的工具对象,适用于所有执行上下文(HTTP、微服务、WebSocket),用于获取当前上下文的请求、响应等对象。
示例中通过 host.switchToHttp() 切换到 HTTP 上下文,再通过 getResponse()、getRequest() 获取对应对象。
七、绑定过滤器(Binding filters)
通过 @UseFilters() 装饰器绑定(来自 @nestjs/common),支持方法级、控制器级、全局级三种作用域,推荐传入类(而非实例),支持依赖注入并减少内存占用。
7.1 方法级作用域(Method-scoped)
// cats.controller.ts
import { Post, UseFilters, Body } from '@nestjs/common';
import { HttpExceptionFilter } from './http-exception.filter';
import { CreateCatDto } from './dto/create-cat.dto';
@Post()
@UseFilters(HttpExceptionFilter) // 传入类,推荐方式
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}也可传入实例(不推荐):@UseFilters(new HttpExceptionFilter())
7.2 控制器级作用域(Controller-scoped)
作用于控制器内所有路由处理器:
// cats.controller.ts
import { Controller, UseFilters } from '@nestjs/common';
import { HttpExceptionFilter } from './http-exception.filter';
@Controller()
@UseFilters(HttpExceptionFilter)
export class CatsController {}7.3 全局级作用域(Global-scoped)
作用于整个应用的所有控制器和路由处理器,有两种实现方式。
方式1:main.ts 中使用 useGlobalFilters()
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter()); // 传入实例
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();警告:useGlobalFilters() 不支持网关(gateways)或混合应用;且该方式注册的全局过滤器,无法注入依赖(脱离模块上下文)。
方式2:模块中使用 APP_FILTER 令牌(支持依赖注入)
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './http-exception.filter';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter, // 传入过滤器类
},
],
})
export class AppModule {}提示:该方式注册的过滤器仍为全局,推荐在过滤器所在的模块中配置;可注册多个过滤器,依次添加到 providers 数组即可。
八、捕获所有异常(Catch everything)
若需捕获所有未处理异常(无论类型),只需让 @Catch() 装饰器为空(不传入任何参数)。
以下为平台无关的实现(使用 HTTP 适配器,不直接依赖 Express/Fastify 原生对象):
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
@Catch() // 空装饰器,捕获所有异常
export class CatchEverythingFilter implements ExceptionFilter {
constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
catch(exception: unknown, host: ArgumentsHost): void {
const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
// 确定响应状态码
const httpStatus =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
// 自定义响应体
const responseBody = {
statusCode: httpStatus,
timestamp: new Date().toISOString(),
path: httpAdapter.getRequestUrl(ctx.getRequest()),
};
// 发送响应
httpAdapter.reply(ctx.getResponse(), responseBody, httpStatus);
}
}警告:若同时使用“捕获所有异常”的过滤器和“捕获特定异常”的过滤器,需先声明“捕获所有”的过滤器,确保特定过滤器能正常生效。
九、继承内置过滤器(Inheritance)
若需扩展内置全局异常过滤器的行为,可继承 BaseExceptionFilter,并调用 super.catch() 委托异常处理给父类。
9.1 自定义继承过滤器
// all-exceptions.filter.ts
import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
@Catch()
export class AllExceptionsFilter extends BaseExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
// 可添加自定义逻辑(如日志)
super.catch(exception, host); // 委托给父类处理
}
}警告:方法级、控制器级的继承过滤器,不可用 new 实例化,需让框架自动实例化。
9.2 全局继承过滤器的注册方式
方式1:main.ts 中注入 HttpAdapter
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const { httpAdapter } = app.get(HttpAdapterHost);
// 传入 httpAdapter 实例化过滤器
app.useGlobalFilters(new AllExceptionsFilter(httpAdapter));
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();方式2:模块中使用 APP_FILTER 令牌(推荐,支持依赖注入)
// app.module.ts
@Module({
providers: [
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
},
],
})
export class AppModule {}Pipes
一、Pipes 核心概念
Pipe 是一个使用 @Injectable() 装饰器注解、并实现 PipeTransform 接口的类,主要作用于控制器路由处理器的参数,在方法被调用前介入,对参数进行处理后,再将(可能经过转换的)参数传递给路由处理器。
1.1 Pipes 的两大典型用途
- 转换(transformation):将输入数据转换为期望的格式(例如,将字符串转换为整数);
- 验证(validation):校验输入数据,若合法则直接原样传递;若不合法则抛出异常。
提示:Pipes 运行在异常区域内。这意味着当 Pipe 抛出异常时,会由异常层(全局异常过滤器和当前上下文应用的任何异常过滤器)处理;一旦 Pipe 抛出异常,后续的控制器方法将不会执行。这是在系统边界校验外部输入数据的最佳实践。
二、内置 Pipes(Built-in pipes)
Nest 提供了多个可直接开箱即用的内置 Pipes,均从 @nestjs/common 包导出,覆盖常见的转换和验证场景:
- ValidationPipe、ParseIntPipe、ParseFloatPipe、ParseBoolPipe
- ParseArrayPipe、ParseUUIDPipe、ParseEnumPipe、DefaultValuePipe
- ParseFilePipe、ParseDatePipe
2.1 内置 Pipe 示例:ParseIntPipe
ParseIntPipe 是转换类 Pipes 的典型示例,确保方法处理器的参数被转换为 JavaScript 整数;若转换失败则抛出异常。以下示例同样适用于其他 Parse* 系列 Pipes(ParseBoolPipe、ParseFloatPipe 等)。
三、绑定 Pipes(Binding pipes)
使用 Pipe 需将其实例绑定到合适的上下文,常用参数级绑定(针对特定路由方法的参数),也支持方法级、控制器级、全局级绑定。绑定方式分为“传入类”(推荐,支持依赖注入)和“传入实例”(可自定义配置)。
3.1 参数级绑定(最常用)
示例1:传入类(默认配置)
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}效果:若请求为 GET localhost:3000/abc(非数字 id),Nest 会抛出如下异常,阻止 findOne() 方法执行:
{
"statusCode": 400,
"message": "Validation failed (numeric string is expected)",
"error": "Bad Request"
}示例2:传入实例(自定义配置)
通过实例化 Pipe 并传入配置,自定义异常状态码等行为:
@Get(':id')
async findOne(
@Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }))
id: number,
) {
return this.catsService.findOne(id);
}3.2 其他参数类型的绑定
示例1:绑定到查询参数(@Query)
@Get()
async findOne(@Query('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}示例2:ParseUUIDPipe 绑定(校验 UUID)
@Get(':uuid')
async findOne(@Param('uuid', new ParseUUIDPipe()) uuid: string) {
return this.catsService.findOne(uuid);
}提示:ParseUUIDPipe 默认支持 UUID 3、4、5 版本,若需指定特定版本,可在 Pipe 配置中传入 version 参数。
提示:验证类 Pipes(如 ValidationPipe)的绑定方式与转换类 Pipes 略有不同,详见后续“绑定验证 Pipes”章节。更多验证技巧可参考官方“Validation techniques”文档。
四、自定义 Pipes(Custom pipes)
尽管 Nest 提供了完善的内置 Pipes,但可通过自定义 Pipes 实现个性化需求。自定义 Pipes 需实现 PipeTransform 接口,并编写 transform() 方法。
4.1 自定义 Pipe 基础结构
所有自定义 Pipes 必须实现 transform() 方法,该方法接收两个参数:
- value:当前处理的方法参数(路由处理器接收前的值);
- metadata:当前处理参数的元数据,描述参数的类型、元类型等信息。
基础示例:空验证 Pipe(Identity Function)
// validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value; // 原样返回参数,无任何处理
}
}提示:PipeTransform<T, R> 是泛型接口,T 表示输入值类型,R 表示 transform() 方法的返回值类型。
4.2 参数元数据(ArgumentMetadata)
metadata 参数是 ArgumentMetadata 类型的对象,包含以下属性:
export interface ArgumentMetadata {
type: 'body' | 'query' | 'param' | 'custom'; // 参数类型(body/query/param/自定义)
metatype?: Type<unknown>; // 参数的元类型(如 String、Number,未声明类型则为 undefined)
data?: string; // 装饰器中传入的字符串(如 @Body('name') 中的 'name',空则为 undefined)
}警告:TypeScript 接口在转译时会消失,若方法参数类型声明为接口(而非类),则 metatype 值为 Object。
五、基于 Schema 的验证(Schema based validation)
通过 Schema 定义参数规则,实现统一、可复用的验证逻辑,常用 Zod 库实现(Schema 简洁、API 易读)。
5.1 基于 Zod 的自定义验证 Pipe
步骤1:安装依赖
$ npm install --save zod步骤2:实现 ZodValidationPipe
import { PipeTransform, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod';
export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) {} // 接收 Zod Schema 作为参数
transform(value: unknown, metadata: ArgumentMetadata) {
try {
const parsedValue = this.schema.parse(value); // 校验参数
return parsedValue; // 校验通过,返回解析后的值
} catch (error) {
throw new BadRequestException('Validation failed'); // 校验失败,抛出异常
}
}
}5.2 绑定 Zod 验证 Pipe
步骤1:定义 Zod Schema 和 DTO 类型
import { z } from 'zod';
// 定义 Zod Schema
export const createCatSchema = z
.object({
name: z.string(),
age: z.number(),
breed: z.string(),
})
.required();
// 从 Schema 推导 DTO 类型
export type CreateCatDto = z.infer<typeof createCatSchema>;步骤2:通过 @UsePipes() 绑定到方法
// cats.controller.ts
import { Post, UsePipes, Body } from '@nestjs/common';
import { ZodValidationPipe } from './zod-validation.pipe';
import { createCatSchema, CreateCatDto } from './dto/create-cat.dto';
@Post()
@UsePipes(new ZodValidationPipe(createCatSchema)) // 传入 Schema 实例化 Pipe
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}提示:@UsePipes() 装饰器从 @nestjs/common 导入。
警告:zod 库要求在 tsconfig.json 文件中启用 strictNullChecks 配置。
六、基于 Class Validator 的验证(Class validator)
结合 class-validator 库,使用装饰器实现验证逻辑,让 DTO 类成为参数验证的唯一数据源(无需单独定义 Schema)。该方式仅支持 TypeScript,不支持原生 JavaScript。
6.1 实现步骤
步骤1:安装依赖
$ npm i --save class-validator class-transformer步骤2:给 DTO 类添加验证装饰器
// create-cat.dto.ts
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString() // 验证 name 为字符串
name: string;
@IsInt() // 验证 age 为整数
age: number;
@IsString() // 验证 breed 为字符串
breed: string;
}提示:更多 class-validator 装饰器用法,可参考其官方文档。
步骤3:实现 ValidationPipe
// validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
// 跳过原生 JavaScript 类型的参数(无验证装饰器)
if (!metatype || !this.toValidate(metatype)) {
return value;
}
// 将普通对象转换为带类型的实例(让 class-validator 能识别装饰器)
const object = plainToInstance(metatype, value);
// 执行验证
const errors = await validate(object);
// 验证失败,抛出异常
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
// 验证通过,原样返回
return value;
}
// 辅助方法:判断是否需要验证(排除原生 JavaScript 类型)
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}提示:Nest 已内置 ValidationPipe,功能比自定义示例更完善,本示例仅用于演示自定义验证 Pipe 的实现逻辑。内置 ValidationPipe 的详细用法可参考官方文档。
说明:class-transformer 库与 class-validator 同属一个作者,兼容性极佳,用于将普通对象转换为带类型的实例,让验证装饰器生效。
步骤4:绑定 ValidationPipe
可绑定到参数级(仅验证单个参数),也支持方法级、控制器级、全局级绑定:
// cats.controller.ts
@Post()
async create(
@Body(new ValidationPipe()) createCatDto: CreateCatDto, // 参数级绑定
) {
this.catsService.create(createCatDto);
}参数级绑定适用于验证逻辑仅针对单个参数的场景。
七、全局级 Pipes(Global scoped pipes)
通用的 Pipes(如 ValidationPipe)可设置为全局级,作用于整个应用的所有控制器和路由处理器,无需重复绑定。
7.1 方式1:main.ts 中使用 useGlobalPipes()
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from './validation.pipe';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe()); // 注册全局 Pipe
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();注意:混合应用中,useGlobalPipes() 不会为网关和微服务注册 Pipes;非混合微服务应用中,useGlobalPipes() 会全局挂载 Pipes。
缺点:该方式注册的全局 Pipe 脱离模块上下文,无法注入依赖。
7.2 方式2:模块中使用 APP_PIPE 令牌(推荐)
通过模块 providers 注册,支持依赖注入,注册后仍为全局生效:
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { ValidationPipe } from './validation.pipe';
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe, // 传入全局 Pipe 类
},
],
})
export class AppModule {}提示:建议在 Pipe 所在的模块中配置;可注册多个全局 Pipe,依次添加到 providers 数组即可;useClass 并非唯一的自定义提供者注册方式,可参考官方文档了解更多。
八、转换场景实战(Transformation use case)
转换类 Pipes 的核心作用是修改输入参数,使其符合路由处理器的期望格式(返回值会完全覆盖原参数值),常见场景:类型转换、默认值填充、参数预处理等。
8.1 示例1:自定义 ParseIntPipe
以下为简化版 ParseIntPipe(Nest 内置版本更完善),演示转换类 Pipe 的实现逻辑:
// parse-int.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10); // 将字符串转换为整数
if (isNaN(val)) { // 转换失败,抛出异常
throw new BadRequestException('Validation failed');
}
return val; // 转换成功,返回整数
}
}绑定使用:
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return this.catsService.findOne(id);
}8.2 示例2:根据 ID 查询实体
通过 Pipe 抽象“根据 ID 查询实体”的逻辑,减少路由处理器的重复代码:
@Get(':id')
findOne(@Param('id', UserByIdPipe) userEntity: UserEntity) {
return userEntity; // Pipe 已返回查询到的实体,直接返回
}说明:UserByIdPipe 接收 id 作为输入,查询数据库并返回 UserEntity 实例,实现代码可自行扩展。
九、默认值填充(Providing defaults)
Parse 系列 Pipes 要求参数必须存在(null/undefined 会抛出异常),若需处理参数缺失的场景,可使用 DefaultValuePipe 先填充默认值,再由 Parse Pipe 处理。
示例:默认值 + Parse* Pipe 组合使用
@Get()
async findAll(
// 默认值为 false,再转换为布尔值
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
// 默认值为 0,再转换为整数
@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}Guards
一、Guards 核心概念
Guard 是一个使用 @Injectable() 装饰器注解、并实现 CanActivate 接口的类,核心职责是根据运行时的特定条件(如权限、角色、访问控制列表等),决定请求是否能被路由处理器处理,这一过程通常被称为“授权”。
在传统 Express 应用中,授权(及与其协作的认证)通常由中间件处理。中间件适合处理认证(如令牌验证、给请求对象附加属性),因为这些操作与特定路由上下文及其元数据关联不强;但中间件本身“无感知”,不知道调用 next() 后会执行哪个处理器。
与中间件不同,Guards 可访问 ExecutionContext 实例,能精确知晓接下来要执行的内容。它与异常过滤器、管道、拦截器类似,可在请求/响应周期的正确节点插入处理逻辑,且支持声明式使用,能保持代码简洁、复用(DRY 原则)。
提示:Guards 在所有中间件执行完毕后运行,但在任何拦截器或管道之前执行。
二、授权 Guard(Authorization guard)
授权是 Guards 的典型应用场景——特定路由仅允许具有足够权限的调用者(通常是已认证用户)访问。下面实现的 AuthGuard 假设用户已认证(请求头中包含令牌),其核心逻辑是提取并验证令牌,根据验证结果决定请求是否可继续。
2.1 AuthGuard 基础实现
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements AuthGuard {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request); // 自定义令牌验证逻辑
}
}提示:若需真实场景的认证机制实现示例,可参考官方对应章节;如需更复杂的授权示例,可查看官方相关文档。
2.2 canActivate() 方法核心说明
所有 Guards 必须实现 canActivate() 方法,该方法返回一个布尔值(或 Promise
- 返回
true:请求将被正常处理(路由处理器执行); - 返回
false:Nest 将拒绝请求,阻止路由处理器执行。
注:validateRequest() 函数的逻辑可根据需求灵活设计,示例重点展示 Guards 在请求周期中的作用。
三、执行上下文(Execution context)
canActivate() 方法接收唯一参数——ExecutionContext 实例,该实例继承自 ArgumentsHost(在异常过滤器章节中已介绍)。
示例中通过 context.switchToHttp().getRequest() 获取请求对象,与异常过滤器中使用 ArgumentsHost 的方式一致。
除了继承 ArgumentsHost 的辅助方法,ExecutionContext 还新增了多个辅助方法,可提供当前执行过程的更多细节,有助于构建可跨多个控制器、方法和执行上下文的通用 Guards。更多关于 ExecutionContext 的用法,可参考官方文档。
AuthGuard:NestJS 的 “路由安全门卫”,用于接口的认证 / 授权,在请求到达 Controller 前验证权限,支持局部 / 全局使用;
Observable:RxJS 的 “异步数据流容器”,用于处理多值、可取消的异步场景,是 NestJS 处理实时数据、微服务的核心方式,比 Promise 更灵活。
四、基于角色的认证(Role-based authentication)
下面实现一个更实用的 Guards——仅允许具有特定角色的用户访问路由。先从基础模板开始,后续逐步完善功能(当前默认允许所有请求)。
4.1 RolesGuard 基础模板
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true; // 暂时允许所有请求
}
}五、绑定 Guards(Binding guards)
与管道、异常过滤器类似,Guards 支持三种作用域:控制器级、方法级、全局级。通过 @UseGuards() 装饰器绑定,该装饰器可接收单个参数或多个逗号分隔的参数,方便一次性应用多个 Guards。
5.1 控制器级绑定
作用于控制器内所有路由处理器:
import { Controller, UseGuards } from '@nestjs/common';
import { RolesGuard } from './roles.guard';
@Controller('cats')
@UseGuards(RolesGuard) // 绑定到整个控制器
export class CatsController {}提示:@UseGuards() 装饰器从 @nestjs/common 导入。
两种绑定方式
- 传入类(推荐):如上述示例,由框架负责实例化,支持依赖注入;
- 传入实例:直接实例化 Guards,适用于需要自定义配置的场景:
`@Controller('cats')@UseGuards(new RolesGuard()) // 传入实例export class CatsController {} `
5.2 方法级绑定
仅作用于单个路由处理器,在方法上添加 @UseGuards() 装饰器即可:
@Controller('cats')
export class CatsController {
@Post()
@UseGuards(RolesGuard) // 仅绑定到 create 方法
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
}5.3 全局级绑定(Global scoped guards)
作用于整个应用的所有控制器和路由处理器,有两种实现方式。
方式1:main.ts 中使用 useGlobalGuards()
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { RolesGuard } from './roles.guard';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard()); // 注册全局 Guard
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();注意:混合应用中,useGlobalGuards() 默认不会为网关和微服务注册 Guards(可参考官方“混合应用”章节修改此行为);非混合微服务应用中,useGlobalGuards() 会全局挂载 Guards。
缺点:该方式注册的全局 Guard 脱离模块上下文,无法注入依赖。
方式2:模块中使用 APP_GUARD 令牌(推荐)
通过模块 providers 注册,支持依赖注入,注册后仍为全局生效:
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { RolesGuard } from './roles.guard';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: RolesGuard, // 传入全局 Guard 类
},
],
})
export class AppModule {}提示:建议在 Guard 所在的模块中配置;可注册多个全局 Guard,依次添加到 providers 数组即可;useClass 并非唯一的自定义提供者注册方式,可参考官方文档了解更多。
六、为处理器设置角色(Setting roles per handler)
当前的 RolesGuard 仅能固定允许/拒绝请求,无法根据不同路由的角色要求灵活控制。Nest 提供自定义元数据功能,可通过 Reflector.createDecorator 静态方法创建装饰器,或使用内置 @SetMetadata() 装饰器,将角色信息附加到路由处理器上。
6.1 方式1:使用 Reflector 创建 @Roles() 装饰器
Reflector 是 Nest 内置工具类,从 @nestjs/core 导出,用于创建自定义元数据装饰器:
// roles.decorator.ts
import { Reflector } from '@nestjs/core';
// 创建 Roles 装饰器,接收字符串数组(角色列表)
export const Roles = Reflector.createDecorator<string[]>();6.2 为路由处理器附加角色元数据
使用 @Roles() 装饰器标注路由,指定该路由允许的角色:
// cats.controller.ts
import { Post, UseGuards, Body } from '@nestjs/common';
import { Roles } from './roles.decorator';
import { CreateCatDto } from './dto/create-cat.dto';
@Post()
@Roles(['admin']) // 仅允许 admin 角色访问该接口
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}6.3 方式2:使用内置 @SetMetadata() 装饰器
除了自定义装饰器,也可直接使用 Nest 内置的 @SetMetadata() 装饰器附加元数据,具体用法可参考官方文档。
七、整合实现 RolesGuard(Putting it all together)
完善 RolesGuard 逻辑:通过 Reflector 提取当前路由的角色元数据,对比当前用户的角色与路由要求的角色,根据匹配结果决定是否允许请求。
完整实现
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
// 注入 Reflector,用于提取元数据
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 提取当前路由处理器的角色元数据(通过 Roles 装饰器附加)
const roles = this.reflector.get(Roles, context.getHandler());
// 若未设置角色元数据,默认允许请求
if (!roles) {
return true;
}
// 获取当前请求对象,提取用户信息(假设 request.user 包含用户角色)
const request = context.switchToHttp().getRequest();
const user = request.user;
// 对比用户角色与路由要求的角色(matchRoles 为自定义逻辑)
return matchRoles(roles, user.roles);
}
}提示:在 Node.js 生态中,通常会将已认证用户信息附加到 request.user 上。实际应用中,可在自定义认证 Guard(或中间件)中实现这一关联,更多细节可参考官方认证相关章节。
警告:matchRoles() 函数的逻辑可根据需求灵活设计(如精确匹配、包含匹配等),示例重点展示 Guards 如何结合元数据实现角色授权。
提示:如需更详细的元数据提取用法,可参考官方“执行上下文”章节中的“反射与元数据”部分。
八、Guards 异常处理
默认异常响应
当用户权限不足(Guards 返回 false)时,Nest 会自动返回如下响应:
{
"statusCode": 403,
"message": "Forbidden resource",
"error": "Forbidden"
}底层逻辑:当 Guards 返回 false 时,框架会抛出 ForbiddenException。
自定义异常响应
若需返回自定义错误,可在 Guards 中直接抛出特定异常(如未授权异常):
import { UnauthorizedException } from '@nestjs/common';
// 在 canActivate 方法中抛出异常
throw new UnauthorizedException();注:Guards 中抛出的任何异常,都会由异常层(全局异常过滤器和当前上下文应用的任何异常过滤器)处理。
提示:若需真实场景的授权实现示例,可参考官方对应章节。
Interceptors
一、Interceptors 核心概念
Interceptor(拦截器)是一个使用 @Injectable() 装饰器注解、并实现 NestInterceptor 接口的类,其设计灵感来源于面向切面编程(AOP)技术,核心作用是在路由处理器执行的前后插入自定义逻辑,对请求/响应流进行灵活控制和处理。
Interceptors 的核心能力
- 在方法执行前/后绑定额外逻辑;
- 转换函数返回的结果(响应数据);
- 转换函数抛出的异常;
- 扩展基础函数的行为;
- 根据特定条件完全重写函数(如实现缓存功能)。
二、基础用法(Basics)
所有拦截器必须实现 intercept() 方法,该方法接收两个核心参数,构成拦截器的核心工作流。
2.1 intercept() 方法参数
- ExecutionContext(执行上下文):与 Guards 中的 ExecutionContext 完全一致,继承自 ArgumentsHost,封装了当前请求的执行信息(如请求对象、路由处理器元数据等)。 ArgumentsHost 是一个包装原始处理器参数的工具类,会根据应用类型(HTTP、微服务等)提供不同的参数数组,详细用法可参考异常过滤器章节中的相关内容。
- CallHandler(调用处理器):实现了
handle()方法,用于触发路由处理器的执行。若在 intercept() 方法中不调用 handle(),则路由处理器将完全不会执行。
2.2 核心工作原理
intercept() 方法本质上是对请求/响应流的“包裹”,因此可以在路由处理器执行前后分别实现自定义逻辑:
- 在调用
next.handle()之前编写的代码,会在路由处理器执行前执行; next.handle()会返回一个 RxJS Observable,该流包含路由处理器的返回结果,通过 RxJS 操作符可对响应流进行后续处理(如转换、异常捕获),实现“执行后”的逻辑。
提示:在面向切面编程(AOP)术语中,调用 next.handle()(即触发路由处理器执行)的操作称为“切入点(Pointcut)”,是插入额外逻辑的关键节点。
2.3 示例说明
以 POST /cats 请求为例:该请求最终会触发 CatsController 中的 create() 方法。若拦截器中未调用 handle(),则 create() 方法不会执行;一旦调用 handle() 并返回其 Observable,create() 方法会被触发,且可通过 Observable 操作符对响应流进行后续处理。
三、执行上下文(Execution context)
ExecutionContext 继承自 ArgumentsHost,除了拥有 ArgumentsHost 的所有辅助方法(如获取请求对象),还新增了多个辅助方法,可提供当前执行过程的更多细节(如当前路由处理器、控制器信息)。
这些方法有助于构建通用型拦截器,使其能够跨多个控制器、方法和执行上下文(如 HTTP、微服务)工作。更多关于 ExecutionContext 的用法,可参考官方文档。
四、调用处理器(Call handler)
CallHandler 的核心方法是 handle(),其核心作用是触发后续的路由处理器执行,并返回一个 RxJS Observable,该 Observable 包含路由处理器的返回结果(响应数据)或抛出的异常。
通过 handle() 返回的 Observable,可借助 RxJS 丰富的操作符(如 tap、map、catchError)对响应流进行灵活处理,这是拦截器实现“转换响应、捕获异常”等能力的核心基础。
五、切面拦截(Aspect interception)
切面拦截是拦截器的典型应用场景之一,例如记录用户交互(存储用户调用记录、异步分发事件、计算请求耗时等)。以下是一个简单的 LoggingInterceptor(日志拦截器),演示拦截器的基础实现。
5.1 LoggingInterceptor 实现
// logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...'); // 路由处理器执行前的逻辑
const now = Date.now();
return next
.handle()
.pipe(
// tap 操作符:监听流的终止(正常/异常),不干扰响应流
tap(() => console.log(`After... ${Date.now() - now}ms`)), // 路由处理器执行后的逻辑
);
}
}提示:NestInterceptor<T, R> 是泛型接口,T 表示 Observable
说明:拦截器与控制器、提供者、Guards 等一样,可通过构造函数注入依赖。
5.2 关键操作符说明
示例中使用了 RxJS 的 tap() 操作符,其作用是:在 Observable 流正常终止或异常终止时,执行自定义逻辑(如日志打印),但不会修改或干扰响应流本身,适合用于记录日志、统计耗时等场景。
六、绑定 Interceptors(Binding interceptors)
与 Pipes、Guards 类似,拦截器支持三种作用域:控制器级、方法级、全局级。通过 @UseInterceptors() 装饰器绑定,该装饰器可接收单个参数或多个逗号分隔的参数,方便一次性应用多个拦截器。
提示:@UseInterceptors() 装饰器从 @nestjs/common 导入。
6.1 控制器级绑定
作用于控制器内所有路由处理器:
// cats.controller.ts
import { Controller, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';
@Controller('cats')
@UseInterceptors(LoggingInterceptor) // 绑定到整个控制器
export class CatsController {}效果:调用 CatsController 中的任何路由(如 GET /cats、POST /cats),都会触发 LoggingInterceptor 的逻辑,控制台会输出:
Before...
After... 1ms两种绑定方式
- 传入类(推荐):如上述示例,由框架负责实例化,支持依赖注入;
- 传入实例:直接实例化拦截器,适用于需要自定义配置的场景:
`@Controller('cats')@UseInterceptors(new LoggingInterceptor()) // 传入实例export class CatsController {} `
6.2 方法级绑定
仅作用于单个路由处理器,在方法上添加 @UseInterceptors() 装饰器即可:
@Controller('cats')
export class CatsController {
@Get()
@UseInterceptors(LoggingInterceptor) // 仅绑定到 findAll 方法
async findAll() {
return this.catsService.findAll();
}
}6.3 全局级绑定(Global scoped interceptors)
作用于整个应用的所有控制器和路由处理器,有两种实现方式。
方式1:main.ts 中使用 useGlobalInterceptors()
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor()); // 注册全局拦截器
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();方式2:模块中使用 APP_INTERCEPTOR 令牌(推荐)
通过模块 providers 注册,支持依赖注入,注册后仍为全局生效。解决了方式1“无法注入依赖”的问题(方式1注册的全局拦截器脱离模块上下文):
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './logging.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor, // 传入全局拦截器类
},
],
})
export class AppModule {}提示:建议在拦截器所在的模块中配置;可注册多个全局拦截器,依次添加到 providers 数组即可;useClass 并非唯一的自定义提供者注册方式,可参考官方文档了解更多。
七、响应映射(Response mapping)
由于 next.handle() 返回 RxJS Observable,可借助 map() 操作符轻松修改响应流中的数据,实现响应格式的统一转换(如给所有响应包裹一层固定结构)。
警告:响应映射功能不支持库特定的响应策略(禁止直接使用 @Res() 对象)。
7.1 TransformInterceptor 实现(统一响应格式)
该拦截器将路由处理器返回的结果,包裹到 { data: T } 结构中,实现全局响应格式统一:
// transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
// 定义统一响应格式的接口
export interface Response<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
// map 操作符:修改响应流中的数据
return next.handle().pipe(map(data => ({ data })));
}
}7.2 效果演示
若路由处理器返回空数组 [](如 GET /cats),经过拦截器处理后,客户端收到的响应将是:
{
"data": []
}提示:Nest 拦截器同时支持同步和异步的 intercept() 方法,必要时可直接将方法改为 async。
7.3 示例2:空值转换
将响应中的所有 null 值转换为空字符串 '',绑定全局后可作用于所有接口:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(map(value => value === null ? '' : value )); // 空值转换逻辑
}
}八、异常映射(Exception mapping)
借助 RxJS 的 catchError()操作符,可捕获路由处理器抛出的异常,并将其转换为自定义异常,实现全局异常统一处理。
8.1 ErrorsInterceptor 实现(异常转换)
// errors.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
BadGatewayException,
CallHandler,
} from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
// catchError 操作符:捕获流中的异常并转换
catchError(err => throwError(() => new BadGatewayException())),
);
}
}效果:无论路由处理器抛出何种异常,都会被捕获并转换为 BadGatewayException(状态码 502)。
九、流重写(Stream overriding)
拦截器可根据特定条件,完全阻止路由处理器的执行,直接返回自定义的响应流(如实现缓存功能)。核心是:不调用 next.handle(),而是返回一个新的 Observable。
9.1 CacheInterceptor 实现(基础缓存)
以下是一个简单的缓存拦截器示例(实际场景需考虑缓存过期时间、缓存失效、缓存大小等因素):
// cache.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, of } from 'rxjs';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true; // 模拟缓存存在(实际场景需判断缓存是否有效)
if (isCached) {
return of([]); // 返回自定义响应流,不执行路由处理器
}
return next.handle(); // 缓存不存在,执行路由处理器并返回其响应流
}
}关键说明
- 使用 RxJS 的
of()操作符创建新的 Observable,返回固定响应[]; - 当
isCached = true时,不调用next.handle(),路由处理器不会执行,直接返回缓存数据; - 若要实现通用缓存方案,可结合 Reflector(参考 Guards 章节)和自定义装饰器,为不同路由配置不同缓存规则。
十、更多操作符应用(More operators)
借助 RxJS 丰富的操作符,可实现更多实用功能。以下示例实现“请求超时处理”:当路由处理器在指定时间内未返回响应,自动终止并抛出超时异常。
10.1 TimeoutInterceptor 实现(请求超时)
// timeout.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000), // 设置超时时间:5秒
catchError(err => {
// 捕获超时异常,转换为 RequestTimeoutException(状态码 408)
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
// 非超时异常,原样抛出
return throwError(() => err);
}),
);
};
};效果说明
若路由处理器执行时间超过 5 秒,请求会被自动终止,客户端收到 408(Request Timeout)响应。可在抛出异常前添加自定义逻辑(如释放资源、记录超时日志)。
Custom decorators
一、装饰器核心基础
Nest 框架的核心构建基于 装饰器(Decorators) 这一语言特性。装饰器在许多常用编程语言中是成熟的概念,但在 JavaScript 领域仍相对较新。
1.1 装饰器定义(ES2016 标准)
ES2016 装饰器是一个返回函数的表达式,可接收三个参数:目标(target)、名称(name)和属性描述符(property descriptor)。
使用方式:在需要装饰的对象(类、方法、属性)顶部,添加 @ 前缀 + 装饰器名称。装饰器可用于修饰类、方法或属性。
提示:若需深入理解装饰器工作原理,建议参考官方推荐的相关文章。
二、参数装饰器(Param decorators)
Nest 提供了一组实用的内置参数装饰器,可直接用于 HTTP 路由处理器中,用于快速提取请求中的相关数据,对应底层 Express(或 Fastify)的请求对象。
2.1 内置参数装饰器列表
以下是 Nest 内置参数装饰器及其对应的 Express/Fastify 对象:
| Nest 装饰器 | 对应底层对象 |
|---|---|
@Request()、@Req() | req(完整请求对象) |
@Response()、@Res() | res(完整响应对象) |
@Next() | next(下一步中间件函数) |
@Session() | req.session(请求会话对象) |
@Param(param?: string) | req.params(路由参数对象);传入参数名时,获取 req.params[param] |
@Body(param?: string) | req.body(请求体对象);传入参数名时,获取 req.body[param] |
@Query(param?: string) | req.query(查询参数对象);传入参数名时,获取 req.query[param] |
@Headers(param?: string) | req.headers(请求头对象);传入参数名时,获取 req.headers[param] |
@Ip() | req.ip(请求客户端 IP 地址) |
@HostParam() | req.hosts(请求主机信息) |
2.2 自定义参数装饰器的意义
在 Node.js 生态中,常见做法是将自定义属性附加到 req 对象上(如认证后的用户信息 req.user),此时需要在每个路由处理器中手动提取:
const user = req.user;自定义参数装饰器可将这一重复操作封装,提升代码可读性和复用性,可在所有控制器中统一复用。
三、自定义参数装饰器实现
Nest 提供 createParamDecorator()方法,用于创建自定义参数装饰器,该方法接收两个参数:装饰器工厂函数、执行上下文(ExecutionContext)。
3.1 基础实现(提取 req.user)
创建 @User() 装饰器,用于快速提取请求对象中的 user 属性:
// user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
// 切换到 HTTP 上下文,获取请求对象
const request = ctx.switchToHttp().getRequest();
// 返回 req.user 属性
return request.user;
},
);3.2 装饰器使用方式
在控制器路由处理器中,直接使用 @User() 装饰器即可提取用户信息:
@Get()
async findOne(@User() user: UserEntity) {
console.log(user); // 直接获取 req.user
}四、传递数据到装饰器(Passing data)
当装饰器的行为需要根据特定条件调整时,可通过 data 参数(装饰器工厂函数的第一个参数),向装饰器传递自定义参数。
典型场景:创建可根据属性名,提取 req.user 中特定字段的自定义装饰器。
4.1 场景假设
认证层验证请求后,会在 req.user 中附加用户实体,格式如下:
{
"id": 101,
"firstName": "Alan",
"lastName": "Turing",
"email": "alan@email.com",
"roles": ["admin"]
}4.2 带参数的装饰器实现
修改 @User() 装饰器,支持传入属性名,提取对应字段(若未传入字段名,则返回完整 user 对象):
// user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const User = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
// 若传入 data(属性名),则返回 user[data],否则返回完整 user 对象
return data ? user?.[data] : user;
},
);4.3 带参数的装饰器使用
传入属性名,即可提取 req.user 中的特定字段,提升代码可读性:
@Get()
async findOne(@User('firstName') firstName: string) {
console.log(`Hello ${firstName}`); // 输出:Hello Alan
}可通过传入不同属性名,提取 user 对象中的不同字段;若 user 对象结构复杂,该方式可大幅简化路由处理器代码。
提示:TypeScript 用户可利用 createParamDecorator<T>() 泛型确保类型安全。例如 createParamDecorator<string>((data, ctx) => ...),或在工厂函数中指定 data: string;若两者都省略,data 类型将为 any。
五、与管道配合使用(Working with pipes)
Nest 会将自定义参数装饰器与内置参数装饰器(@Body()、@Param()、@Query())同等对待,这意味着管道(Pipes)也会对自定义装饰器标注的参数执行验证/转换逻辑。
此外,可直接将管道应用到自定义参数装饰器上,需注意设置 validateCustomDecorators: true(默认不验证自定义装饰器标注的参数)。
5.1 示例:结合 ValidationPipe
@Get()
async findOne(
// 将 ValidationPipe 应用到 @User() 装饰器,开启自定义装饰器验证
@User(new ValidationPipe({ validateCustomDecorators: true }))
user: UserEntity,
) {
console.log(user);
}提示:必须设置 validateCustomDecorators: true,否则 ValidationPipe 不会验证自定义装饰器标注的参数。
六、装饰器组合(Decorator composition)
Nest 提供 applyDecorators() 辅助方法,用于将多个装饰器组合成一个单一装饰器,适用于将一组相关装饰器(如认证、授权相关)封装复用。
6.1 组合装饰器实现
创建 @Auth() 装饰器,组合元数据设置、Guard 应用、Swagger 文档注解等多个装饰器:
// auth.decorator.ts
import { applyDecorators } from '@nestjs/common';
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles), // 设置角色元数据
UseGuards(AuthGuard, RolesGuard), // 应用认证、角色守卫
ApiBearerAuth(), // Swagger Bearer 认证注解
ApiUnauthorizedResponse({ description: 'Unauthorized' }), // Swagger 未授权响应注解
);
}6.2 组合装饰器使用
使用单一 @Auth() 装饰器,即可应用所有组合的装饰器,简化代码:
@Get('users')
@Auth('admin') // 仅需一行,应用所有组合的装饰器
findAllUsers() {}警告:@nestjs/swagger 包中的 @ApiHideProperty() 装饰器不可组合,与 applyDecorators() 函数配合使用时会失效。
简单记:@ApiHideProperty() 别放进 applyDecorators(),要么单独用,要么换 @ApiProperty({ hidden: true }) 组合。
评论