前端写久了总想把一个接口从头到尾自己做完。Express 上手最快,可写到第三个模块就开始难受,路由、参数校验、数据库连接、异常处理各写各的,没有统一约定,换个人接手得重读一遍代码才敢改。NestJS 给的正是这层约定,控制器收请求,服务干活,模块负责组装,中间还塞了五种切面能力去接管横切逻辑。这篇是我把 NestJS 从建项目一路做到接 MySQL 和 Redis 之后攒下来的完整笔记,配置、代码、踩坑点都在里面,需要哪块直接翻到哪块抄走。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- NestJS 的项目结构,控制器、服务、模块分别在解决什么问题
- 静态资源、模板引擎、Cookie 与 Session 这几个 Express 老熟人在 Nest 里怎么配
- 中间件、守卫、拦截器、管道、异常过滤器的职责边界和真实执行顺序
- 用 DTO 配合 class-validator 做参数校验,把校验规则从业务代码里摘出去
- 配置抽离、多环境变量、文件上传下载、图片验证码、邮件服务、定时任务
- passport + JWT 登录鉴权、密码加密方案、RBAC 角色权限的表设计和守卫实现
- 接入 Swagger 自动生成可调试的接口文档
- MongoDB 与 TypeORM 操作 MySQL,实体设计、五种访问方式、三种增删改查写法
- 事务的三种用法,一对一、一对多、多对多关系怎么设计和增删改查
- 接入 Redis,以及用它实现单点登录
先把 Nest 是个什么东西说清楚。
Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用 JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP (面向对象编程)、FP (函数式编程)和 FRP (函数响应式编程)。
- 在底层,Nest 构建在强大的 HTTP 服务器框架上,例如 Express (默认),并且还可以通过配置从而使用 Fastify !
- Nest 在这些常见的 Node.js 框架 (Express/Fastify) 之上提高了一个抽象级别,但仍然向开发者直接暴露了底层框架的 API。这使得开发者可以自由地使用适用于底层平台的无数的第三方模块。
我一直觉得 Nest 最值钱的地方不是某个 API,而是它把「一个后端项目该怎么分层」这件事变成了框架级别的强制约定。你写了三个月,别人接手也知道去哪找登录逻辑。
本文基于 nest8 演示。这里必须提前说一句,这篇写于 2022 年 5 月,之后 NestJS 又发过大版本,TypeORM 也从 0.2 升到了 0.3,一些 API 的签名有变化,最典型的就是 findOne 从接受裸 id 改成必须传 where 对象、Connection 相关的一批全局函数被重新组织过。下文我把原始写法原样保留了,因为很多人手上的老项目还跑在这套 API 上,但如果你是新起项目,装完包之后请以官方文档的当前版本为准,不要照抄我这里的旧签名。
# 一、基础篇 项目结构与请求生命周期
这一部分解决的是「怎么把一个 Nest 项目跑起来并且组织好」。控制器、服务、模块是三个必须先分清的角色,之后的静态资源、模板引擎、Cookie 和 Session 都是把 Express 的能力接进 Nest 的写法,最后那一大块中间件守卫管道过滤器拦截器,是 Nest 区别于裸 Express 最核心的东西。
# 创建项目
Nest 的 CLI 不是可选项,是这个框架的一部分。手写目录结构当然也行,但控制器、服务、模块、DTO 之间有一套固定的文件命名和注册关系,用 CLI 生成能省掉一堆「忘了在 module 里注册」的低级错误。所以第一步先全局装它。
$ npm i -g @nestjs/cli
nest new project-name 创建一个项目
$ tree
.
├── README.md
├── nest-cli.json
├── package.json
├── src
│ ├── app.controller.spec.ts
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ └── main.ts
├── test
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json
2 directories, 12 files
目录很干净,真正要看的就三个文件。
以下是这些核心文件的简要概述:
app.controller.ts带有单个路由的基本控制器示例。app.module.ts应用程序的根模块。main.ts应用程序入口文件。它使用 NestFactory 用来创建 Nest 应用实例。
main.ts包含一个异步函数,它负责引导我们的应用程序:
import { NestFactory } from '@nestjs/core';
import { ApplicationModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
await app.listen(3000);
}
bootstrap();
NestFactory暴露了一些静态方法用于创建应用实例create()方法返回一个实现INestApplication接口的对象, 并提供一组可用的方法
main.ts 这个 app 实例后面会被反复用到。全局管道、全局守卫、全局过滤器、静态资源目录、Swagger 挂载,全都是在这里往 app 上挂,所以看一个 Nest 项目,先翻 main.ts 基本就知道它开了哪些全局能力。
底层跑的是谁也可以换。
nest有两个支持开箱即用的 HTTP 平台:express和fastify。 您可以选择最适合您需求的产品
platform-expressExpress 是一个众所周知的 node.js 简约 Web 框架。 这是一个经过实战考验,适用于生产的库,拥有大量社区资源。 默认情况下使用@nestjs/platform-express包。 许多用户都可以使用Express,并且无需采取任何操作即可启用它。platform-fastifyFastify是一个高性能,低开销的框架,专注于提供最高的效率和速度。
这里有个坑要注意。选 Express 还是 Fastify,会影响到后面 Cookie、Session、静态资源、文件上传这些能力的写法,因为它们说到底就是在调底层平台的 API。下文所有例子都是 Express 平台的写法,如果你换了 Fastify,对应的中间件包和调用方式要跟着换。我自己只在 Express 平台上完整跑过,Fastify 那条线没验证过。
# Nest控制器
Nest中的控制器层负责处理传入的请求, 并返回对客户端的响应。
下面这张图是 Nest 官方对控制器位置的示意,客户端请求先落到控制器,控制器再决定交给谁处理。

控制器的目的是接收应用的特定请求。路由机制控制哪个控制器接收哪些请求。通常,每个控制器有多个路由,不同的路由可以执行不同的操作
有一条纪律建议一开始就守住,控制器里不写业务逻辑。它只干三件事,声明路由、从请求里取参数、把结果 return 出去。查库、算数据、调第三方全部丢给 service。这条守住了,后面写单元测试和换数据源的时候会轻松很多。
通过NestCLi创建控制器:
nest -h 可以看到nest支持的命令
常用命令:
- 创建控制器:
nest g co user module - 创建服务:
nest g s user module - 创建模块:
nest g mo user module - 默认以src为根路径生成
执行 nest -h 之后能看到完整的命令列表,长这样。

实际用起来,比如要建一个文章模块的控制器。
nest g controller posts
表示创建posts的控制器,这个时候会在src目录下面生成一个posts的文件夹,这个里面就是posts的控制器,代码如下
import { Controller } from '@nestjs/common';
@Controller('posts')
export class PostsController {
}
创建好控制器后,nestjs会自动的在 app.module.ts 中引入PostsController,代码如下
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostsController } from './posts/posts.controller'
@Module({
imports: [],
controllers: [AppController, PostsController],
providers: [AppService],
})
export class AppModule {}
CLI 自动改 app.module.ts 这个行为很关键。Nest 的依赖注入是靠模块元数据串起来的,控制器不写进 controllers 数组就不会被扫描到,路由自然也不存在。手写文件最容易漏的就是这一步。
# nest配置路由请求数据
路由声明完,下一个问题就是怎么把请求里的数据拿出来。Express 里是从 req.query、req.body、req.params 上手动抠,Nest 把这套包成了参数装饰器,写在形参上,框架帮你注进来。
Nestjs提供了其他HTTP请求方法的装饰器
@Get()@Post()@Put()、@Delete()、@Patch()、@Options()、@Head()和@All()
在Nestjs中获取Get传值或者Post提交的数据的话我们可以使用Nestjs中的装饰器来获取。
左边是 Nest 的装饰器,右边是它对应到 Express 原生对象上的哪个字段,对照着看就很清楚了。
@Request() req
@Response() res
@Next() next
@Session() req.session
@Param(key?: string) req.params / req.params[key]
@Body(key?: string) req.body / req.body[key]
@Query(key?: string) req.query / req.query[key]
@Headers(name?: string) req.headers / req.headers[name]
示例