📌 写在最前面:为什么你要读这篇?
被 Next.js 全栈能力「宠坏」的前端,突然接手 NestJS 项目------看着 @Module()、@Controller() 一脸懵?又或者被 ERR_PNPM_IGNORED_BUILDS 卡了一下午,怀疑人生?
这篇 2 万字的保姆级教程,用大量表格 + 餐厅比喻 + 真实踩坑 ,把 NestJS 核心概念拆解得明明白白。全文覆盖:框架定位、环境搭建、装饰器/DI/MVC 三大件、完整 CRUD 实战、面试速查表。一篇顶十篇,建议先收藏。
📚 本文目录
| 章节 | 内容概要 | 重要度 |
|---|---|---|
| 1️⃣ NestJS 是什么 | 框架定位 + 与 Next.js 对比(小白版) | ⭐⭐⭐⭐⭐ |
| 2️⃣ 后端开发是做些什么 | API、集成、并发、微服务,给前端「喂饭」 | ⭐⭐⭐⭐ |
| 3️⃣ 安装与环境搭建 | 脚手架安装 + 新建项目 + 启动 + 踩坑实录 | ⭐⭐⭐⭐⭐ |
| 4️⃣ 常见报错速查 | EADDRINUSE、Cannot GET、TS1261 实战解决 |
⭐⭐⭐⭐⭐ |
| 5️⃣ 目录架构 | 文件结构 + 命名规范 | ⭐⭐⭐⭐ |
| 6️⃣ 工厂模式 | 为什么不用 new?餐厅比喻秒懂 |
⭐⭐⭐⭐ |
| 7️⃣ 高度模块化 | Module 是啥?怎么装配? | ⭐⭐⭐⭐⭐ |
| 8️⃣ 装饰器模式 | 三层装饰器 + Express 对比 + 参数速查表 | ⭐⭐⭐⭐⭐ |
| 9️⃣ MVC 三大件 | Module / Controller / Service 各司其职 | ⭐⭐⭐⭐⭐ |
| 🔟 依赖注入(DI) | 三个角色 + 语法糖 + 手动 new 对比 | ⭐⭐⭐⭐⭐ |
| 1️⃣1️⃣ 完整 CRUD 实战 | Todos 项目 7 个文件全解析 + 数据流图 | ⭐⭐⭐⭐⭐ |
| 1️⃣2️⃣ TypeScript 关键概念 | interface、throw vs return、Partial、展开运算符等 | ⭐⭐⭐⭐ |
| 1️⃣3️⃣ 数组方法速查 | find / findIndex / splice / Object.assign | ⭐⭐⭐⭐ |
| 1️⃣4️⃣ 开发流程 & 错误处理 | 5 步走 + 内置异常类 + 面试常考 | ⭐⭐⭐⭐ |
| 1️⃣5️⃣ 面试常考速查表 | 框架 / RESTful / TS 基础 / 综合实战 4 大类 | ⭐⭐⭐⭐⭐ |
1️⃣ NestJS 是什么?
相比 Next.js 全栈,NestJS 就是 Node 的纯后端企业级开发框架。
默认使用 TypeScript,全面模块化思想,适合构建企业级服务。相比自由松散的 Express,NestJS 强约束(按 Module / Controller / Service 组织)、默认 TS、适合大型项目。
🆚 与 Next.js 的区别(小白版)
| 维度 | Next.js | NestJS |
|---|---|---|
| 定位 | 全栈框架(前后端都管) | 纯后端框架(只管服务端) |
| 主要产物 | 网页(HTML + 交互) | API 接口(JSON 数据) |
| 路由方式 | app/ 目录即路由 |
装饰器 @Controller() 声明路由 |
| 渲染方式 | SSR/SSG/CSR 都支持 | 不渲染页面,只返回数据 |
| 用在啥场景 | 用户直接访问的网站 | 给前端/手机/小程序提供数据 |
| 类比 | 🍽️ 餐厅(提供完整用餐体验) | 🏭 中央厨房(只做菜送出去) |
📌 一句话记 :Next.js 是给「用户看」的,NestJS 是给「程序调用」的。
2️⃣ 后端开发是做些什么?
2.1 提供 API 接口(Web 开发)
- 干啥:给前端提供数据接口
- 例子 :
GET /api/users------ 获取用户列表POST /api/login------ 处理登录请求GET /api/products/:id------ 获取某商品详情
- 用户能感知到吗:看不到,但每次刷新页面看到的数据都来自后端
2.2 系统集成、并发、底层服务、AI Infra
- 干啥:处理重活累活,承接前端的复杂需求
- 例子 :
- 系统集成:把多个第三方服务打通(支付 + 物流 + 短信)
- 并发:同时处理大量用户请求(抢购、秒杀)
- 底层服务:消息队列、定时任务、日志收集
- AI Infra(AI 基础设施):把 AI 模型封装成 API、对接云服务、做模型推理调度
- 用户能感知到吗:感知到的是"流畅",看不到的是后端扛下的压力
2.3 微服务框架
- 干啥:把一个大后端拆成多个小服务,每个独立部署
- 例子 :
- 用户服务(专门管注册登录)
- 订单服务(专门管下单)
- 商品服务(专门管商品)
- 好处:单个服务挂了不影响整体,可以独立扩容
- NestJS 的支持 :内置
@nestjs/microservices模块,原生支持微服务架构
📌 一句话总结 :后端开发就是「给前端喂饭」------前端要数据后端给数据,前端要算啥后端帮算,前端要存啥后端帮存。
3️⃣ 安装与环境搭建
整个安装流程分 3 步:装脚手架 → 新建项目 → 启动项目。
第 1 步:全局安装 NestJS CLI 脚手架
bash
npm i -g @nestjs/cli
命令拆解:
| 部分 | 含义 |
|---|---|
npm |
Node.js 的包管理工具(装 Node 时自带) |
i |
install 的缩写,意思是"安装" |
-g |
--global 的缩写,"全局安装"------装完之后电脑任何目录都能用 |
@nestjs/cli |
要安装的包名(@nestjs 是组织名,cli 是命令行工具包) |
装完能干啥 :能在终端里直接敲 nest 开头的命令,比如 nest new(新建项目)、nest g controller(生成控制器)、nest start(启动项目)等。
💡 这条命令只需在电脑上执行一次,以后所有 NestJS 项目都能复用。
📌 小白比喻:
-g就像装桌面版微信,全局都能双击打开;不加-g就像临时用网页版微信,只在当前目录可用。
第 2 步:创建新的 NestJS 项目
bash
nest new hello
| 部分 | 含义 |
|---|---|
nest |
第 1 步全局安装的 CLI 工具 |
new |
创建新项目的子命令 |
hello |
项目名,也是项目文件夹名(可自定义) |
执行后选 pnpm(推荐,更快更省空间),之后会自动执行 pnpm install 装依赖。
⚠️ 这一步可能踩到
ERR_PNPM_IGNORED_BUILDS这个坑(pnpm 10+ 安全策略导致)。如果踩了别慌,跳到下面的「安装踩坑记录」按步骤解决。
第 3 步:启动项目
bash
cd hello # 进入项目目录
pnpm run start # 启动
启动成功的标志:终端出现最后一行日志:
css
[Nest] LOG [NestApplication] Nest application successfully started
验证 :浏览器访问 http://localhost:3000,能看到 Hello World! 就完事了。
📌 端口默认 3000,如果被占用会报
EADDRINUSE :3000,详见「常见报错速查」。
🕳️ 安装踩坑记录(新手必看)
主坑:ERR_PNPM_IGNORED_BUILDS(pnpm 拒绝执行构建脚本)
触发场景 :用 nest new hello + pnpm 装依赖时
报错现象:
vbnet
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: unrs-resolver@1.12.2
Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.
根本原因(3 个角色撞车):
- pnpm 10+ 当保安 🛡️:出于安全考虑,默认禁止任何第三方包在安装时跑"构建脚本"(防恶意包挖矿/盗密),必须用户明确批准
- unrs-resolver 被迫营业 🔧:NestJS 底层依赖(用于解析模块路径),必须跑构建脚本才能编译原生代码,不跑就半残
- nest CLI 偷懒了 😅:生成项目时,往
pnpm-workspace.yaml写的是占位符set this to true or false,没帮你写死成true
解决方案(改一个字符就过) ✅:
打开项目根目录的 pnpm-workspace.yaml,把里面的:
yaml
allowBuilds:
unrs-resolver: set this to true or false
改成:
yaml
allowBuilds:
unrs-resolver: true
保存后重跑:
bash
pnpm install
pnpm run start
⚠️ 这个坑还藏着两个小陷阱(坑中坑)
陷阱一:nest new 把真实错误吞了,让你看不到真相
现象 :nest new 自动调 pnpm 装依赖时,只显示一行模模糊糊的:
ini
Failed to execute command: pnpm install --strict-peer-dependencies=false --reporter=silent
🙀 Packages installation failed!
原因 :注意命令里的 --reporter=silent ------ 它是 pnpm 的"静默模式",会把详细输出屏蔽掉,所以你看不到真正的错误。
应对:手动跑一遍去掉 silent 的命令,错误就现身了:
bash
pnpm install --strict-peer-dependencies=false
看到真实报错后(多半就是上面那个 ERR_PNPM_IGNORED_BUILDS)再对症下药。
陷阱二:pnpm approve-builds 交互界面别直接回车
场景 :你按主坑的报错提示去跑 pnpm approve-builds
正确姿势 ✅:
- 终端出现一个交互列表
- 按空格 勾选
unrs-resolver(一定要勾选!) - 再回车 确认
错误姿势 ❌:直接按回车 → 所有包被默认标记为 false(不允许构建)→ 下次再跑 pnpm approve-builds 时直接告诉你"无包待批准",连勾选机会都不给了。
误操作后的修复 :手动打开 pnpm-workspace.yaml,把对应包值从 false 改成 true,再 pnpm install。
📌 一句话总结
pnpm 10+ 想保护你 → nest CLI 没帮你签字 → unrs-resolver 不能跑脚本 → install 失败。改
pnpm-workspace.yaml里一个true就过。
4️⃣ 常见报错速查
报错 1:EADDRINUSE: address already in use :::3000
含义:3000 端口被占用了(多半是上一次启动的 NestJS 还没退干净)。
排查 + 解决(PowerShell):
powershell
# 1. 看谁占了 3000 端口
netstat -ano | findstr :3000
# 2. 拿到最后一列的 PID(一串数字),强杀进程
taskkill /PID <PID号> /F
如果不想杀进程 :改端口跑,编辑 src/main.ts:
typescript
await app.listen(3001); // 换个端口
或者命令行临时指定:
bash
# Windows PowerShell
$env:PORT=3001; pnpm run start
📌 一句话记:EADDRINUSE 不是代码错,是端口被占。改端口或杀进程二选一。
报错 2:Cannot GET /todos(404 找不到路由)
含义:路由没注册成功,多半是改了代码但服务没重启。
排查清单(按顺序查):
- AppModule 是否 import 了 TodosModule ------ 看
app.module.ts的imports数组里有没有TodosModule - TodosModule 是否注册了 controller ------ 看
todos.module.ts的controllers: [TodosController] - controller 装饰器是否正确 ------
@Controller('todos')+@Get()拼起来才等于/todos - 服务有没有重启 ------ 默认
pnpm run start不热重载,改完代码必须重启
强烈建议:开发时用 watch 模式,自动重启:
bash
pnpm run start:dev
启动成功的标志日志:
css
[Nest] LOG [RoutesResolver] TodosController { /todos }:
看到 [RoutesResolver] TodosController { /todos } 才说明路由真的注册上了。
报错 3:TS1261: ... resolved to X while ... resolved to Y
含义 :文件名大小写和 import 路径大小写不一致。TypeScript 大小写敏感,Windows 文件系统不敏感,撞车就报错。
典型场景 :文件叫 todos.controller.ts(小写 t),但 import 写成 from './Todos.controller'(大写 T)。
解决方案(推荐方案) :把文件名统一改成全小写,符合 NestJS CLI 命名规范,且 import 路径必须与文件名完全一致。
| 错误命名 | 正确命名(全小写) |
|---|---|
Todos.controller.ts / Todos.Controller.ts |
todos.controller.ts |
Todos.service.ts / Todos.Service.ts |
todos.service.ts |
Todos.module.ts / Todos.Module.ts |
todos.module.ts |
辅助手段 :改完后在 IDE 里执行 TypeScript: Restart TS Server,清掉缓存的大小写不一致。这步很关键------即使文件已经改对了,TS Server 还可能缓存着旧路径。
📌 一句话记:NestJS 文件名一律全小写,跨平台兼容,且和 nest CLI 生成的文件保持一致。import 路径必须与文件名完全一致(大小写敏感)。
5️⃣ 目录架构
ruby
hello/
├── src/
│ ├── main.ts # 入口文件:启动 Nest 应用
│ ├── app.module.ts # 根模块:组装所有子模块
│ ├── app.controller.ts # 根控制器:处理 / 路由
│ ├── app.service.ts # 根服务:返回 Hello World
│ └── todos/ # 业务模块(手写)
│ ├── todos.module.ts # 模块定义:组装 controller + service
│ ├── todos.controller.ts # 控制器:接收 HTTP 请求
│ └── todos.service.ts # 服务:业务逻辑 + 数据
├── test/
├── package.json
├── pnpm-workspace.yaml
├── tsconfig.json # TS 配置(含 forceConsistentCasingInFileNames: true)
└── nest-cli.json
命名规范 :业务文件全部小写 ,例如 todos.controller.ts、todos.service.ts。文件名大小写敏感问题参考「报错 3:TS1261」。
6️⃣ 工厂模式
什么是工厂模式?
一句话 :不直接 new,把"创建对象"这件事交给一个专门的工厂去做,你只管要、不管怎么造。
为什么实例化不用 new?
对比看:
typescript
// 写法 A:直接 new(传统)
const app = new NestApplication();
app.init();
app.listen(3000);
// ↑ 一堆初始化细节,调用者得知道
// 写法 B:工厂模式(NestJS 实际用法)
const app = await NestFactory.create(AppModule);
await app.listen(3000);
// ↑ 调用者啥都不用管,工厂内部全包了
工厂的优势:
- 封装复杂创建过程:用户不用关心初始化顺序、依赖关系
- 统一入口 :所有应用都用
NestFactory.create,写法一致 - 支持多场景:同一套代码可以创建 HTTP / WebSocket / 微服务 / GraphQL 等不同类型应用
跟 OOP(面向对象)什么关系?
工厂模式是 OOP 设计模式的经典成员,体现了 OOP 三大特性:
| OOP 特性 | 工厂模式如何体现 |
|---|---|
| 封装 | 把复杂的"new + 初始化"塞进工厂内部,调用者只看到 create(...) |
| 继承 | 不同子类工厂可以创建不同类型的应用 |
| 多态 | 同一个 NestFactory.create 入口,能创建多种应用 |
📌 比喻:直接 new = 自己造蛋糕 (要准备原料、控温、烤多久),工厂 = 蛋糕店(你说"要一个巧克力蛋糕",店员后台搞定所有事)。
NestJS 里的工厂入口
typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule); // 工厂模式核心:通过工厂创建应用,不直接 new
await app.listen(3000);
}
bootstrap();
NestFactory.create(AppModule) 内部会解析依赖树、实例化所有 Module / Controller / Service、注入依赖、启动 HTTP 服务------这一套复杂流程用户完全不用知道。
7️⃣ 高度模块化
Module 是什么?
NestJS 把"一组功能"打包成一个 Module(模块),每个模块独立、可复用、可组合。约定如下:
ini
AppModule(根模块)
└── imports: [TodosModule, UserModule, ...] ← 装配子模块
一个 Module 文件包括:
ruby
xx.module.ts # 定义 + 装配(controller + service 在此登记)
xx.controller.ts # 控制器:接 HTTP 请求
xx.service.ts # 服务:业务逻辑、数据访问
@nestjs/common 提供 Module 类,用 @Module({...}) 装饰一个类,告诉框架这个类是个模块:
typescript
@Module({
imports: [...], // 依赖的其他模块
controllers: [XxxController], // 注册控制器(框架会扫描它的路由方法)
providers: [XxxService], // 注册服务(供依赖注入使用)
exports: [XxxService], // 导出服务,供其他模块使用(可选)
})
export class XxxModule {}
关于 exports :如果其他模块需要用到当前模块的 Service,必须在 exports 数组中声明。例如,如果 UserModule 需要使用 TodosService,则 TodosModule 需要:
typescript
@Module({
controllers: [TodosController],
providers: [TodosService],
exports: [TodosService], // ← 允许其他模块使用 TodosService
})
export class TodosModule {}
然后在 UserModule 中 imports: [TodosModule] 即可。这是 NestJS 模块封装性的体现------默认不暴露内部实现,只有显式 exports 的内容才对外可见。
模块化的好处
| 好处 | 例子 |
|---|---|
| 独立 | todos 模块挂了不影响 users 模块 |
| 复用 | 把 todos 模块整体搬到别的项目就能用 |
| 团队协作 | 不同人负责不同模块,互不干扰 |
8️⃣ 装饰器模式
什么是装饰器模式?
一句话 :不修改原有对象 ,动态给对象/方法/参数添加功能 的设计模式。需要 TypeScript 支持(experimentalDecorators: true),原生 JS 不支持。
📌 比喻:给物体贴标签 ------
@Controller()贴个标签,NestJS 就认出这是控制器;@Get()贴个标签,NestJS 就知道这是个 GET 路由。原类没动一行代码,只是多了个"标签"。
装饰器三个层级(NestJS 核心)
NestJS 几乎所有功能都靠装饰器,分为三个层级:
| 层级 | 装饰器例子 | 贴在哪 | 干啥 |
|---|---|---|---|
| 类级别 | @Module() @Controller() @Injectable() |
类的上方 | 标识这个类的身份 |
| 方法级别 | @Get() @Post() @Put() @Delete() @Patch() |
方法上方 | 标识这是处理什么请求的路由 |
| 参数级别 | @Param() @Body() @Query() |
方法参数前 | 从请求里抽取参数 |
三层装饰器完整示例
typescript
@Controller('todos') // ← 类级别:这个类是 todos 控制器
export class TodosController {
@Get() // ← 方法级别:处理 GET /todos
findAll() { ... }
@Get(':id') // ← 方法级别:处理 GET /todos/:id
findOne(@Param('id') id: string) { ... }
// ↑ 参数级别:从 URL 取 id
@Post() // ← 方法级别:处理 POST /todos
create(@Body('title') title: string) { ... }
// ↑ 参数级别:从请求体取 title
@Delete(':id') // ← 方法级别:处理 DELETE /todos/:id
remove(@Param('id') id: string) { ... }
@Patch(':id') // ← 方法级别:处理 PATCH /todos/:id
update(
@Param('id') id: string, // ← 参数级别
@Body() patch: Partial<Todo>, // ← 参数级别:取整个 body
) { ... }
}
Express vs NestJS 装饰器对比
Express 写法(没有装饰器,手动注册路由):
javascript
const express = require('express');
const app = express();
app.get('/todos', (req, res) => { /* 查全部 */ });
app.post('/todos', (req, res) => { /* 新增 */ });
app.delete('/todos/:id', (req, res) => { /* 删除 */ });
NestJS 写法(装饰器声明式):
typescript
@Controller('todos')
class TodosController {
@Get() findAll() { ... }
@Post() create() { ... }
@Delete(':id') remove() { ... }
}
| 项 | Express | NestJS |
|---|---|---|
| 路由注册 | 命令式(手动 app.get) | 声明式(装饰器贴标签) |
| 组织方式 | 函数+回调 | 类 + 方法 |
| 类型安全 | 弱(运行时才报错) | 强(编译期就报错) |
| 可读性 | 路由散落各处 | 同一 controller 集中管理 |
装饰器速查表(参数装饰器详解)
| 装饰器 | 用在 | 从哪取 | 例子 | 后端拿到的值 |
|---|---|---|---|---|
@Module() |
类 | --- | 标识模块 | --- |
@Controller('todos') |
类 | --- | 标识控制器(路径前缀) | --- |
@Injectable() |
类 | --- | 标识可注入服务 | --- |
@Get() @Post() @Put() @Patch() @Delete() |
方法 | --- | 声明路由 + HTTP 方法 | --- |
@Param('id') |
参数 | URL 路径 | /todos/1 |
"1"(string) |
@Body() |
参数 | 请求体 | {title:'a'} |
{title:'a'}(对象) |
@Body('title') |
参数 | 请求体某字段 | {title:'a'} |
"a"(string) |
@Query('page') |
参数 | URL 查询串 | /todos?page=2 |
"2"(string) |
@Headers('auth') |
参数 | 请求头 | Authorization: xxx |
"xxx"(string) |
@HttpCode(204) |
方法 | --- | 自定义 HTTP 状态码 | --- |
参数装饰器关键点:
- 路由里写
:id是占位符 ,@Param('id')的'id'必须和:id同名 - URL 取出来的所有值都是 string (
/todos/1里的1是文本"1",不是数字 1),需要Number(id)转换 @Body()不带 key = 取整个 body 对象;带 key(如@Body('title'))= 只取该字段
前端调用对比:
javascript
// @Param:URL 直接带
fetch('/todos/1');
// @Body:放在请求体里
fetch('/todos', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ title: '吃饭' })
});
9️⃣ MVC 三大件详解
Module、Controller、Service 各自干啥
NestJS 遵循经典 MVC 思想(在框架里被重新组织成 Module/Controller/Service 三件套):
| 角色 | 干啥 | 比喻 |
|---|---|---|
| Module | 容器,登记 controller 和 service,让框架认识 | 📄 营业执照 |
| Controller | 接 HTTP 请求、抽参数、调 Service、返结果 | 🧑💼 服务员 |
| Service | 业务逻辑、数据 CRUD | 👨🍳 后厨 |
📌 餐厅比喻一句话记牢:客人(前端请求)→ 服务员(Controller)→ 后厨(Service)→ 菜(数据)。服务员不做饭、后厨不见客。
AppModule(根模块)
typescript
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodosModule } from './todos/todos.module';
@Module({
imports: [TodosModule], // 依赖的其他模块(把业务模块挂进来)
controllers: [AppController], // 注册根控制器
providers: [AppService], // 注册根服务
})
export class AppModule {}
关键点:
imports数组装的是其他 Module------你在哪写业务模块,就要在这里 import 进来- 业务模块(TodosModule)一旦 import,它的所有 controller 路由才会生效
- 没在
imports里登记的模块 = 没接入系统,访问对应路由会 404
Controller(控制器)
typescript
@Controller('todos') // 类级别装饰器:路径前缀 /todos
export class TodosController {
constructor(private readonly todosService: TodosService) {}
// ↑ 依赖注入:声明要 TodosService,NestJS 自动实例化并塞进来
@Get()
findAll(): Todo[] {
return this.todosService.findAll(); // 转发给 Service
}
}
Controller 的责任:
- 接 HTTP 请求(路由匹配)
- 抽参数(
@Param、@Body等) - 调 Service 处理
- 把 Service 返回的数据转给前端
不该干的:写业务逻辑、直接操作数据库------这些归 Service。
Service(服务)
typescript
@Injectable() // ← 标签:可以被 NestJS 创建实例并注入到别处
export class TodosService {
findAll(): Todo[] {
return todos; // 直接返假数据
}
findOne(id: number): Todo {
const todo = todos.find(t => t.id === id);
if (!todo) throw new Error(`Todo${id}不存在`);
return todo;
}
}
Service 的责任:
- 业务逻辑(校验、计算、转换)
- 数据访问(CRUD 假数据 / 调数据库 / 调第三方 API)
- 抛业务异常(找不到、不合法等)
🔟 依赖注入(DI)
什么是依赖注入?
一句话 :你不用 new,NestJS 自动帮你创建 Service 实例并塞进 Controller。
三个角色
| 角色 | 干啥 | 例子 |
|---|---|---|
| 提供者 | 声明"我可以被注入" | @Injectable() 标记的 Service |
| 注册者 | 把 Provider 登记到系统 | @Module({ providers: [TodosService] }) |
| 消费者 | 在构造函数里"要"依赖 | constructor(private readonly todosService: TodosService) |
注入语法糖
typescript
constructor(private readonly todosService: TodosService) {}
这是 TypeScript 的语法糖,等价于:
typescript
private readonly todosService: TodosService;
constructor(todosService: TodosService) {
this.todosService = todosService; // 手动赋值
}
| 关键字 | 作用 |
|---|---|
private |
让参数变成类的私有属性 |
readonly |
防止后续代码误改这个属性 |
TodosService(参数类型) |
告诉 NestJS "我要一个 TodosService 实例" |
NestJS 在运行时会看构造函数的参数类型,自动从已注册的 providers 里找到对应实例,new 出来塞进去。
DI vs 手动 new
| 写法 | 优劣 |
|---|---|
const service = new TodosService() |
紧耦合,难替换、难测试 |
| DI 自动注入 | 松耦合,易替换(比如换成 MockService 测试) |
📌 比喻:手动 new = 自己买菜做饭 (得知道去哪买、买啥),DI = 让 NestJS 当采购员(你只要在菜单上写"我要鸡蛋",它就把鸡蛋送来)。
1️⃣1️⃣ 完整 CRUD 实战(Todos 项目)
RESTful 风格
REST 是前后端约定的一套"动词规范":用同一个 URL /todos,搭配不同 HTTP 方法做不同的事。
| HTTP 方法 | 装饰器 | 语义 | 路由示例 | 成功状态码 |
|---|---|---|---|---|
| GET | @Get() |
查 | GET /todos 查全部 |
200 OK |
| GET | @Get(':id') |
查一个 | GET /todos/1 |
200 OK / 404 没找到 |
| POST | @Post() |
增 | POST /todos 新增 |
201 Created |
| PUT | @Put(':id') |
全量替换 | PUT /todos/1 |
200 OK |
| PATCH | @Patch(':id') |
局部修改 | PATCH /todos/1 |
200 OK |
| DELETE | @Delete(':id') |
删 | DELETE /todos/1 |
200 OK / 204 |
核心思想 :URL 标识资源(/todos/1 表示"那一条 todo"),HTTP 方法标识动作(GET 看、POST 增、DELETE 删)。
路由拼接规则
控制器类装饰器 @Controller('todos') 给所有方法加一个路径前缀 ,方法装饰器 @Get(':id') 定义子路径,最终路由 = 前缀 + 子路径。
| 类装饰器 | 方法装饰器 | 最终路由 |
|---|---|---|
@Controller('todos') |
@Get() |
GET /todos |
@Controller('todos') |
@Get(':id') |
GET /todos/:id |
@Controller('todos') |
@Post() |
POST /todos |
@Controller('todos') |
@Delete(':id') |
DELETE /todos/:id |
@Controller('todos') |
@Patch(':id') |
PATCH /todos/:id |
项目文件结构(7 个核心文件)
ruby
src/
├── main.ts # ① 入口:启动应用
├── app.module.ts # ② 根模块:挂载子模块
├── app.controller.ts # ③ 根控制器:处理 / 路由(返 Hello World)
├── app.service.ts # ④ 根服务:Hello World 业务逻辑
└── todos/
├── todos.module.ts # ⑤ 业务模块:装配 controller + service
├── todos.service.ts # ⑥ 业务服务:CRUD + 数据
└── todos.controller.ts # ⑦ 业务控制器:接 HTTP 请求
执行顺序 :main.ts 启动 → 加载 AppModule → 加载 TodosModule → 注册 AppController/AppService + TodosController/TodosService → 路由生效。
① main.ts(入口文件)
typescript
// 入口文件:NestJS 应用从这里启动
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
// NestFactory.create 就是工厂模式(详见「工厂模式」章节)
// 传入根模块 AppModule,工厂内部解析依赖、实例化所有 Module/Controller/Service、注入依赖
const app = await NestFactory.create(AppModule);
// 启动 HTTP 服务,端口优先取环境变量 PORT,没有就用 3000
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
关键点:
- 工厂模式入口:
NestFactory.create(AppModule) - 端口可被环境变量覆盖:
process.env.PORT ?? 3000 - 启动后浏览器访问
http://localhost:3000
② app.module.ts(根模块)
typescript
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodosModule } from './todos/todos.module';
@Module({
imports: [TodosModule], // ← 把业务模块挂进来,路由才会生效
controllers: [AppController], // 根控制器(处理 / 路由,返 Hello World)
providers: [AppService], // 根服务
})
export class AppModule {}
关键点:
imports数组里必须挂上TodosModule,否则 todos 的所有路由都访问不到(404)- 根模块本身也有自己的 controller 和 service(默认的 Hello World 接口)
- 没在
imports里登记的模块 = 没接入系统
③ app.controller.ts(根控制器)
typescript
// 【控制器】NestJS 的"接待员",专门负责接收前端 HTTP 请求
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller() // 类级别装饰器:声明这是个控制器(不带参数 → 路径前缀为空)
export class AppController {
// 依赖注入:NestJS 自动创建 AppService 实例并塞进来
// private → 变成类私有属性;readonly → 防止误改
constructor(private readonly appService: AppService) {}
@Get() // 方法级别装饰器:处理 GET / 请求(根路径)
getHello(): string {
console.log('/ 的控制器'); // 调试日志,方便排查
// this.appService → 访问注入的 Service 实例
return this.appService.getHello(); // 转发给 Service
}
}
关键点:
@Controller()不带参数 = 路径前缀为空,匹配根路径/@Get()不带参数 = 匹配根路径,所以拼起来就是GET /- 访问
http://localhost:3000/就会执行getHello(),返回Hello World! - 这是 NestJS 自动生成的"演示控制器",让你看到框架跑起来了
④ app.service.ts(根服务)
typescript
import { Injectable } from '@nestjs/common';
@Injectable() // 标签:声明这个类可以被 NestJS 创建实例并注入到别处
export class AppService {
getHello(): string { // 类型约束:返回值必须是 string
// 返回数据给 Controller,Controller 再返回给前端
return 'Hello World!';
// return 1; // 会报错:number 不能赋给 string(演示类型约束的作用)
}
}
关键点:
@Injectable()让这个类成为可注入服务,才能被 Controller 通过 DI 拿到: string类型约束:承诺返回 string,如果写成return 1会在编译期报错(演示类型约束的作用)- 真正"干活"的地方------返回
'Hello World!'这个字符串 - 跟 TodosService 是同一套机制,只是业务更简单(啥也不查,直接返字符串)
AppService vs TodosService 对比:
| 对比项 | AppService(根服务) | TodosService(业务服务) |
|---|---|---|
| 职责 | 返回简单的 Hello World 字符串 | 完整的 Todo CRUD 业务逻辑 |
| 数据 | 硬编码字符串 | 数组模拟数据库 + 自增 id |
| 复杂度 | ⭐ 极简(演示用) | ⭐⭐⭐⭐⭐ 完整业务 |
| 依赖注入 | 被 AppController 注入 | 被 TodosController 注入 |
| 异常处理 | 无(不会出错) | 有(找不到抛 NotFoundException) |
共同点 :都使用 @Injectable() 装饰器,都是可注入的服务,都遵循"Controller 调 Service"的 MVC 分层思想。
⑤ todos.module.ts(业务模块装配)
typescript
// Todos Module 的定义文件
import { Module } from '@nestjs/common';
import { TodosController } from './todos.controller';
import { TodosService } from './todos.service';
@Module({
controllers: [TodosController], // 注册控制器,NestJS 会扫描其路由方法
providers: [TodosService], // 注册服务,供依赖注入(DI)使用
// exports: [TodosService], // 如其他模块需要,可取消注释
})
export class TodosModule {}
关键点:
- 一个 Module 文件干的事就是"装配":登记 controller 和 service
controllers数组:告诉 NestJS "这个模块有哪些控制器"providers数组:告诉 NestJS "这个模块提供哪些可注入的服务"exports数组:告诉 NestJS "哪些 Provider 可以暴露给其他模块使用"(默认不暴露)
⑥ todos.service.ts(业务核心 + 数据)
typescript
import { Injectable } from '@nestjs/common';
// 接口:定义一个待办事项的属性(前后端共享的类型约定)
export interface Todo {
id: number;
title: string;
completed: boolean;
}
// 模拟数据库(学习用,生产环境由数据库替代)
let todos: Todo[] = [
{id: 1, title: '学习 NestJS', completed: false},
{id: 2, title: '完成项目', completed: true},
{id: 3, title: '与客户沟通', completed: false},
{id: 4, title: '测试代码', completed: true},
{id: 5, title: '部署应用', completed: false},
];
let nextId = 6; // 自增 id 计数器(重启会重置,生产环境由数据库自增)
@Injectable() // 标签:声明这个类可以被 NestJS 创建实例并注入到别处
export class TodosService {
// ① 查全部
findAll(): Todo[] {
return todos;
}
// ② 查一个
findOne(id: number): Todo {
const todo = todos.find(t => t.id === id);
if (!todo) throw new Error(`Todo${id}不存在`); // 容错:找不到抛错
return todo;
}
// ③ 新增
create(title: string): Todo {
// id 由后端生成(前端不能传),completed 默认 false
const todo: Todo = { id: nextId++, title, completed: false };
todos.push(todo); // 加到数组末尾
return todo; // 返回完整对象(含后端生成的 id)
}
// ④ 删除
remove(id: number): void { // void 表示不返回值
const index = todos.findIndex(t => t.id === id);
if (index === -1) throw new Error(`Todo${id}不存在`); // 必须用 === -1
todos.splice(index, 1); // 从 index 位置删 1 个
}
// ⑤ 局部修改(PATCH)
update(id: number, patch: Partial<Todo>): Todo {
const todo = this.findOne(id); // 复用 findOne 找原对象(含容错)
Object.assign(todo, patch); // 合并:patch 里有字段覆盖 todo
return todo;
}
}
5 个 CRUD 方法核心思路表:
| 方法 | 干啥 | 关键代码 | 注意点 |
|---|---|---|---|
findAll() |
查全部 | return todos |
直接返整个数组 |
findOne(id) |
查一个 | todos.find(t => t.id === id) |
找不到用 if (!todo) throw |
create(title) |
新增 | {id: nextId++, title, completed: false} |
id 由后端生成 (前端不能传 id),completed 默认 false |
remove(id) |
删除 | findIndex + splice(index, 1) |
必须用 index === -1 判断找不到 ,不能用 !index |
update(id, patch) |
局部修改 | findOne(id) + Object.assign(todo, patch) |
复用 findOne 找原对象,原地合并 patch 字段 |
⑦ todos.controller.ts(路由 + 转发)
typescript
import {
Controller, // 类装饰器:声明控制器身份 + 路径前缀
Get, // 方法装饰器:声明 GET 路由
Post, // 方法装饰器:声明 POST 路由
Patch, // 方法装饰器:声明 PATCH 路由
Delete, // 方法装饰器:声明 DELETE 路由
Param, // 参数装饰器:从 URL 路径取参数
Body, // 参数装饰器:从请求体取参数
} from '@nestjs/common';
import { TodosService } from './todos.service';
import { type Todo } from './todos.service'; // 只导入类型,不导入值
@Controller('todos') // 类级别装饰器:路径前缀 /todos
export class TodosController {
// 依赖注入:在构造函数声明要 TodosService,NestJS 自动实例化并塞进来
// private → 变成类私有属性;readonly → 防止误改
constructor(private readonly todosService: TodosService) {}
// ① 查全部 GET /todos
@Get()
findAll(): Todo[] {
return this.todosService.findAll(); // 转发给 Service
}
// ② 查一个 GET /todos/:id
@Get(':id')
findOne(@Param('id') id: string): Todo {
// ↑ @Param('id') 从 URL 取路径参数(取出来都是 string)
return this.todosService.findOne(Number(id)); // Number() 显式转 number
}
// ③ 新增 POST /todos
@Post()
create(@Body('title') title: string): Todo {
// ↑ @Body('title') 只取请求体里的 title 字段(id/completed 由后端生成)
return this.todosService.create(title);
}
// ④ 删除 DELETE /todos/:id
@Delete(':id')
remove(@Param('id') id: string): { message: string } {
this.todosService.remove(Number(id));
return { message: `Todo${id}删除成功` }; // 返确认信息,方便前端提示
}
// ⑤ 局部修改 PATCH /todos/:id
// 修改有两个:Put(全量替换)和 Patch(局部修改),此处用 patch 更合适
@Patch(':id')
update(
@Param('id') id: string, // 从 URL 取 id
@Body() patch: Partial<Todo>, // 从请求体取要改的字段(部分字段,用 Partial)
): Todo {
return this.todosService.update(Number(id), patch);
}
}
Controller 的核心职责:
- 接 HTTP 请求 → 抽参数 → 调 Service → 返结果
- 不写业务逻辑,只做"接请求、转格式、调 Service、返结果"
- URL 取出的都是 string,调 Service 前要用
Number(id)转成数字 @Body('title')只取一个字段;@Body()取整个 body 对象- Controller 不需要
try/catch:NestJS 会捕获 Service 层抛出的异常,自动转成对应的 HTTP 错误响应(如NotFoundException→ 404)
关于 HTTP 状态码的补充:
在 RESTful 规范中,不同操作应该返回不同的状态码:
| 操作 | 成功状态码 | NestJS 默认 | 建议 |
|---|---|---|---|
| GET(查全部) | 200 OK | ✅ 200 | 保持默认 |
| GET(查一个) | 200 OK / 404 | ✅ 200(找不到需手动抛 404) | 用 NotFoundException |
| POST(新增) | 201 Created | ❌ 默认 200 | 需手动改 |
| PUT/PATCH(改) | 200 OK | ✅ 200 | 保持默认 |
| DELETE(删) | 204 No Content / 200 OK | ✅ 200 | 可保持 200 并返回消息 |
如何让 POST 返回 201:
typescript
import { Controller, Post, Body, HttpCode } from '@nestjs/common';
// ↑ 需要额外引入 HttpCode
@Controller('todos')
export class TodosController {
@Post()
@HttpCode(201) // ← 加这一行,状态码变 201
create(@Body('title') title: string): Todo {
return this.todosService.create(title);
}
}
📌 虽然默认 200 也能用,但严格 RESTful 规范要求 POST 返回 201,面试时可能会被问到。
完整数据流示例(前端访问 GET /todos/1)
kotlin
浏览器请求 GET /todos/1
↓
NestJS 路由匹配:@Controller('todos') + @Get(':id')
↓
Controller.findOne(@Param('id') id = "1")
↓ Number("1") = 1
Service.findOne(1)
↓ todos.find(t => t.id === 1)
↓ 找到 {id:1, title:'学习 NestJS', completed:false}
↓ return todo
Controller 拿到 todo
↓ return 给 NestJS
NestJS 序列化成 JSON
↓
HTTP 200 OK + {"id":1,"title":"学习 NestJS","completed":false}
↓
浏览器收到响应 ✅
1️⃣2️⃣ TypeScript 关键概念(小白版)
📐 类型约束(接口 interface)
typescript
interface Todo {
id: number; // 必须是 number
title: string; // 必须是 string
completed: boolean; // 必须是 boolean
}
作用 :给编译器看,编译期 检查你的数据对不对。运行时(浏览器收到的)类型已经丢了,类型不能保证运行时不出错。
typescript
const todo: Todo = { id: 1, title: 'a', completed: false }; // ✅
const bad: Todo = { id: '1', title: 'a' }; // ❌ TS 报错
📌 类型管"代码不出错",运行时容错还得靠代码(如 if + throw)。
🎯 throw vs return(核心区别)
| 关键字 | 行为 | HTTP 响应 | 比喻 |
|---|---|---|---|
return |
正常返回值,方法结束 | 200 OK | 🍽️ 服务员端菜 |
throw |
异常中断,逃出整个调用链 | 500/404/... | 🚨 喊"着火了!" |
关键差异 :return 走"成功通道",throw 走"异常通道"。
typescript
findOne(id: number): Todo {
const todo = todos.find(t => t.id === id);
if (!todo) throw new Error('不存在'); // ← 失败通道:HTTP 500
return todo; // ← 成功通道:HTTP 200
}
口诀 :成功 return,失败 throw。
类型签名只约束 return :findOne(): Todo 承诺返回 Todo,但 throw new Error(...) 不违反签名。
📦 void 返回类型
typescript
remove(id: number): void {
// ↑ void 表示"不打算返回任何值"
todos.splice(index, 1);
}
含义:方法只干副作用(删数据),不返东西给调用者。
注意 :返回 undefined 也算 void,但 NestJS 会把空响应转成 HTTP 200 + 空 body,前端可能解析失败。建议改成返回确认信息:
typescript
remove(id: number): { message: string } {
todos.splice(index, 1);
return { message: `已删除 ${id}` }; // ← 前端能拿到 JSON
}
🔧 Partial 工具类型
作用 :把一个类型的所有字段变成可选。
typescript
interface Todo {
id: number;
title: string;
completed: boolean;
}
type PartialTodo = Partial<Todo>;
// 等价于:
// {
// id?: number; // ← 都加了 ?
// title?: string;
// completed?: boolean;
// }
PATCH 场景必用:前端只传要改的字段,其他字段可以缺。
typescript
update(id: number, patch: Partial<Todo>): Todo { ... }
// ↑ 接收 {completed:true} 或 {title:'a'} 或空对象都合法
🔄 显式 vs 隐式类型转换
URL 里的所有值都是字符串,需要转 number 给 Service:
| 写法 | 类型 | 例子 |
|---|---|---|
Number(id) |
✅ 显式转换 | Number('1') → 1 |
parseInt(id) |
✅ 显式转换 | parseInt('1') → 1 |
+id |
隐式转换 | +'1' → 1(不推荐,可读性差) |
id * 1 |
隐式转换 | '1' * 1 → 1(不推荐) |
推荐用 Number(id):意图明确,不易出 bug。
✍️ 对象简写语法
typescript
const title = '吃饭';
const todo = { title }; // ← 简写
// 等价于
const todo = { title: title }; // ← 完整写法
规则 :当对象的 key 名跟变量名一样时,可以省略 : 变量名,只写一个名字。这是 ES6 简写语法。
🌊 展开运算符 ...
typescript
const old = { a: 1, b: 2 };
const patch = { b: 99, c: 3 };
const merged = { ...old, ...patch };
// 结果:{ a: 1, b: 99, c: 3 }
// ↑ 没动 ↑ 被覆盖 ↑ 新增
规则 :后面覆盖前面,同名 key 取后面的值。
➕ 后置自增 nextId++
typescript
let nextId = 6;
const todo = { id: nextId++ };
// ↑ 取当前值 6 给 id,然后 nextId 变 7
| 写法 | 行为 | 返回 |
|---|---|---|
nextId++(后置) |
先用再加 1 | 旧值(6) |
++nextId(前置) |
先加 1再用 | 新值(7) |
1️⃣3️⃣ 数组方法速查
🔍 find vs findIndex(关键区别)
| 方法 | 找到时返回 | 找不到时返回 | 适合干啥 |
|---|---|---|---|
find(cb) |
元素本身 | undefined(falsy) |
取值 |
findIndex(cb) |
索引(0,1,2...) | -1 |
定位(为删改做准备) |
判断找不到的写法对比:
typescript
// find(用 ! 判断)
const todo = todos.find(t => t.id === id);
if (!todo) throw ...; // ← undefined 是 falsy,!undefined === true
// findIndex(必须用 === -1)
const index = todos.findIndex(t => t.id === id);
if (index === -1) throw ...; // ← 必须显式比较!
// ❌ 不能用 !index,因为 index=0 时 !0===true,会把"找到了第一条"误判为"找不到"
✂️ splice:删除/插入/替换
typescript
array.splice(起始位置, 删除个数, ...插入的元素)
// ↑ 可选 ↑ 可选
| 写法 | 作用 |
|---|---|
todos.splice(1, 1) |
从位置 1 删 1 个 |
todos.splice(1, 2) |
从位置 1 删 2 个 |
todos.splice(1, 0, item) |
在位置 1 插入 item(删 0 个) |
todos.splice(1, 1, item) |
在位置 1 替换(删 1 插 1) |
📋 Object.assign:合并对象
typescript
Object.assign(目标, 源1, 源2, ...);
// 把所有源的属性复制到目标,后面覆盖前面
typescript
const todo = { id:1, title:'a', completed:false };
const patch = { completed:true };
Object.assign(todo, patch);
// 结果:todo = { id:1, title:'a', completed:true }
// ↑ 被覆盖 ↑ 没动
注意:
Object.assign是浅拷贝,嵌套对象只覆盖引用- 原地修改目标对象,不创建新对象(适合 PATCH 直接改原数据)
1️⃣4️⃣ 开发流程 & 错误处理
🚀 开发流程
完整开发一个业务模块的步骤:
- 写 Service (
xx.service.ts):定义接口、假数据、CRUD 方法 - 写 Controller (
xx.controller.ts):声明路由、抽参数、转发给 Service - 写 Module (
xx.module.ts):装配 controller 和 service - 挂到根模块 (
app.module.ts):在imports数组里 import 业务模块 - 启动开发服务器 :
pnpm run start:dev(自动重启)
⚙️ 开发模式 vs 生产模式启动
| 命令 | 模式 | 特点 | 适用场景 |
|---|---|---|---|
pnpm run start |
普通 | 一次编译,不热重载 | 生产环境(配合 PM2) |
pnpm run start:dev |
开发 | 热重载(改代码自动重启) | 开发阶段(强烈推荐) |
pnpm run start:debug |
调试 | 热重载 + 开启调试端口 | 需要打断点调试时 |
pnpm run start:prod |
生产 | 先 build 再启动编译后代码 | 生产部署(更规范) |
开发时的最佳实践:
bash
# ✅ 开发时用这个,改完代码自动重启,不用手动 Ctrl+C
pnpm run start:dev
# ❌ 开发时别用这个,改代码不重启,访问会 404
pnpm run start
🛑 错误处理
NotFoundException(NestJS 内置错误类)
typescript
import { NotFoundException } from '@nestjs/common';
findOne(id: number): Todo {
const todo = todos.find(t => t.id === id);
if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
// ↑ 抛这个会让 HTTP 返回 404,语义更准
return todo;
}
对比 throw new Error() 和 throw new NotFoundException():
| 写法 | HTTP 状态码 | 语义 |
|---|---|---|
throw new Error(...) |
500 Internal Server Error | "服务器出问题了"(不合适,找不到数据不是服务器崩了) |
throw new NotFoundException(...) |
404 Not Found | "没找到"(语义正确) |
NestJS 异常处理机制说明 :Controller 层不需要手动 try/catch Service 层抛出的异常。NestJS 框架层会自动捕获所有异常,并根据异常类型(如 NotFoundException)转换成对应的 HTTP 状态码和错误响应体。这体现了「让框架处理横切关注点」的设计理念。
NestJS 内置了一系列 HTTP 异常类:
| 异常类 | 状态码 | 场景 |
|---|---|---|
BadRequestException |
400 | 参数不合法 |
UnauthorizedException |
401 | 没登录 |
ForbiddenException |
403 | 没权限 |
NotFoundException |
404 | 资源不存在 |
ConflictException |
409 | 冲突(如重复创建) |
InternalServerErrorException |
500 | 服务器内部错误 |
🔧 tsconfig.json 中的关键配置
json
{
"compilerOptions": {
"experimentalDecorators": true, // ← 必须开启,否则装饰器不生效
"emitDecoratorMetadata": true, // ← 必须开启,否则依赖注入不工作
"forceConsistentCasingInFileNames": true // ← 强制文件名大小写一致(防 TS1261)
}
}
| 配置项 | 作用 | 不开启的后果 |
|---|---|---|
experimentalDecorators |
支持装饰器语法 | @Controller() 等全部报错 |
emitDecoratorMetadata |
生成装饰器元数据(类型信息) | 依赖注入失效(NestJS 不知道要注入啥类型) |
forceConsistentCasingInFileNames |
强制文件名大小写一致 | Windows 上容易出现 TS1261 错误 |
📌 用
nest new生成的项目默认都配置好了,不需要手动改。但如果你从零手写 tsconfig,这三个配置必须加上。
🎯 面试常考:你是如何处理后端报错的?
- TypeScript 是单线程,不处理异常会挂掉整个进程
- 使用
try / catch / finally兜底 - NestJS 提供标准化错误类(如
NotFoundException),抛出后框架自动转成对应 HTTP 状态码 - 标准化错误输出包含:
statusCode(状态码)message(错误消息)- 可选
error(错误名称)
1️⃣5️⃣ 🎯 面试常考知识点速查表
下面这些点都常出现在面试题里,对应章节已经讲过。复习时按这张表查漏补缺。
NestJS 框架相关
| 知识点 | 面试常见问法 | 所在章节 |
|---|---|---|
| 工厂模式 | "为什么 NestJS 实例化不用 new?" |
[工厂模式 → 为什么实例化不用 new](#知识点 面试常见问法 所在章节 工厂模式 "为什么 NestJS 实例化不用 new?" 工厂模式 → 为什么实例化不用 new 装饰器模式 "装饰器分哪几层?跟 OOP 什么关系?" 装饰器模式 → 装饰器三个层级 MVC 三大件 "Controller 能直接操作数据库吗?为什么?" MVC 三大件详解 依赖注入(DI) "什么是依赖注入?跟手动 new 有啥区别?" 依赖注入(DI) 异常处理 "你是如何处理后端报错的?" 错误处理 → 面试常考 "#%E4%B8%BA%E4%BB%80%E4%B9%88%E5%AE%9E%E4%BE%8B%E5%8C%96%E4%B8%8D%E7%94%A8-new") |
| 装饰器模式 | "装饰器分哪几层?跟 OOP 什么关系?" | [装饰器模式 → 装饰器三个层级](#知识点 面试常见问法 所在章节 工厂模式 "为什么 NestJS 实例化不用 new?" 工厂模式 → 为什么实例化不用 new 装饰器模式 "装饰器分哪几层?跟 OOP 什么关系?" 装饰器模式 → 装饰器三个层级 MVC 三大件 "Controller 能直接操作数据库吗?为什么?" MVC 三大件详解 依赖注入(DI) "什么是依赖注入?跟手动 new 有啥区别?" 依赖注入(DI) 异常处理 "你是如何处理后端报错的?" 错误处理 → 面试常考 "#%E8%A3%85%E9%A5%B0%E5%99%A8%E4%B8%89%E4%B8%AA%E5%B1%82%E7%BA%A7nestjs-%E6%A0%B8%E5%BF%83") |
| MVC 三大件 | "Controller 能直接操作数据库吗?为什么?" | [MVC 三大件详解](#知识点 面试常见问法 所在章节 工厂模式 "为什么 NestJS 实例化不用 new?" 工厂模式 → 为什么实例化不用 new 装饰器模式 "装饰器分哪几层?跟 OOP 什么关系?" 装饰器模式 → 装饰器三个层级 MVC 三大件 "Controller 能直接操作数据库吗?为什么?" MVC 三大件详解 依赖注入(DI) "什么是依赖注入?跟手动 new 有啥区别?" 依赖注入(DI) 异常处理 "你是如何处理后端报错的?" 错误处理 → 面试常考 "#mvc-%E4%B8%89%E5%A4%A7%E4%BB%B6%E8%AF%A6%E8%A7%A3") |
| 依赖注入(DI) | "什么是依赖注入?跟手动 new 有啥区别?" | [依赖注入(DI)](#知识点 面试常见问法 所在章节 工厂模式 "为什么 NestJS 实例化不用 new?" 工厂模式 → 为什么实例化不用 new 装饰器模式 "装饰器分哪几层?跟 OOP 什么关系?" 装饰器模式 → 装饰器三个层级 MVC 三大件 "Controller 能直接操作数据库吗?为什么?" MVC 三大件详解 依赖注入(DI) "什么是依赖注入?跟手动 new 有啥区别?" 依赖注入(DI) 异常处理 "你是如何处理后端报错的?" 错误处理 → 面试常考 "#%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5di") |
| 异常处理 | "你是如何处理后端报错的?" | [错误处理 → 面试常考](#知识点 面试常见问法 所在章节 工厂模式 "为什么 NestJS 实例化不用 new?" 工厂模式 → 为什么实例化不用 new 装饰器模式 "装饰器分哪几层?跟 OOP 什么关系?" 装饰器模式 → 装饰器三个层级 MVC 三大件 "Controller 能直接操作数据库吗?为什么?" MVC 三大件详解 依赖注入(DI) "什么是依赖注入?跟手动 new 有啥区别?" 依赖注入(DI) 异常处理 "你是如何处理后端报错的?" 错误处理 → 面试常考 "#%F0%9F%8E%AF-%E9%9D%A2%E8%AF%95%E5%B8%B8%E8%80%83%E4%BD%A0%E6%98%AF%E5%A6%82%E4%BD%95%E5%A4%84%E7%90%86%E5%90%8E%E7%AB%AF%E6%8A%A5%E9%94%99%E7%9A%84") |
RESTful & API 设计
| 知识点 | 面试常见问法 | 所在章节 |
|---|---|---|
| RESTful 风格 | "GET/POST/PUT/PATCH/DELETE 各对应什么操作?" | [完整 CRUD 实战 → RESTful 风格](#知识点 面试常见问法 所在章节 RESTful 风格 "GET/POST/PUT/PATCH/DELETE 各对应什么操作?" 完整 CRUD 实战 → RESTful 风格 路由拼接规则 "@Controller + @Get 怎么拼出最终路由?" 路由拼接规则 PUT vs PATCH "PUT 和 PATCH 有啥区别?什么时候用 PATCH?" 完整 CRUD 实战 → RESTful 风格表 "#restful-%E9%A3%8E%E6%A0%BC") |
| 路由拼接规则 | "@Controller + @Get 怎么拼出最终路由?" | [路由拼接规则](#知识点 面试常见问法 所在章节 RESTful 风格 "GET/POST/PUT/PATCH/DELETE 各对应什么操作?" 完整 CRUD 实战 → RESTful 风格 路由拼接规则 "@Controller + @Get 怎么拼出最终路由?" 路由拼接规则 PUT vs PATCH "PUT 和 PATCH 有啥区别?什么时候用 PATCH?" 完整 CRUD 实战 → RESTful 风格表 "#%E8%B7%AF%E7%94%B1%E6%8B%BC%E6%8E%A5%E8%A7%84%E5%88%99") |
| PUT vs PATCH | "PUT 和 PATCH 有啥区别?什么时候用 PATCH?" | [完整 CRUD 实战 → RESTful 风格表](#知识点 面试常见问法 所在章节 RESTful 风格 "GET/POST/PUT/PATCH/DELETE 各对应什么操作?" 完整 CRUD 实战 → RESTful 风格 路由拼接规则 "@Controller + @Get 怎么拼出最终路由?" 路由拼接规则 PUT vs PATCH "PUT 和 PATCH 有啥区别?什么时候用 PATCH?" 完整 CRUD 实战 → RESTful 风格表 "#restful-%E9%A3%8E%E6%A0%BC") |
TypeScript / JavaScript 基础
| 知识点 | 面试常见问法 | 所在章节 |
|---|---|---|
| throw vs return | "为什么找不到数据要 throw 而不是 return undefined?" |
[throw vs return](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial |
| find vs findIndex | "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" |
[find vs findIndex](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial |
| Partial<T> | "Partial、Pick、Omit 区别?什么时候用?" |
[Partial<T> 工具类型](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial |
| 显式 vs 隐式转换 | "Number(id) 和 +id 有啥不同?哪个推荐?" |
[显式 vs 隐式类型转换](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial |
| 后置自增 | "i++ 和 ++i 区别?返回啥?" |
[后置自增 nextId++](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial |
| 展开运算符 | "合并两个对象,相同字段如何处理?" | [展开运算符 ...](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial |
| Object.assign | "Object.assign 是深拷贝还是浅拷贝?" |
[Object.assign:合并对象](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial |
| 类型约束 | "interface 和 type 有啥区别?类型能保证运行时不出错吗?" | [类型约束(接口 interface)](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial |
综合实战题
| 知识点 | 面试常见问法 | 所在章节 |
|---|---|---|
| 完整 CRUD 设计 | "让你设计一个 Todo API,路由怎么定?" | [完整 CRUD 实战](#知识点 面试常见问法 所在章节 完整 CRUD 设计 "让你设计一个 Todo API,路由怎么定?" 完整 CRUD 实战 参数装饰器选择 "什么场景用 @Param、@Body、@Query?" 装饰器速查表 启动报错排查 "NestJS 跑不起来 / 404,怎么排查?" 常见报错速查 "#%E5%AE%8C%E6%95%B4-crud-%E5%AE%9E%E6%88%98todos-%E9%A1%B9%E7%9B%AE") |
| 参数装饰器选择 | "什么场景用 @Param、@Body、@Query?" |
[装饰器速查表](#知识点 面试常见问法 所在章节 完整 CRUD 设计 "让你设计一个 Todo API,路由怎么定?" 完整 CRUD 实战 参数装饰器选择 "什么场景用 @Param、@Body、@Query?" 装饰器速查表 启动报错排查 "NestJS 跑不起来 / 404,怎么排查?" 常见报错速查 "#%E8%A3%85%E9%A5%B0%E5%99%A8%E9%80%9F%E6%9F%A5%E8%A1%A8%E5%8F%82%E6%95%B0%E8%A3%85%E9%A5%B0%E5%99%A8%E8%AF%A6%E8%A7%A3") |
| 启动报错排查 | "NestJS 跑不起来 / 404,怎么排查?" | [常见报错速查](#知识点 面试常见问法 所在章节 完整 CRUD 设计 "让你设计一个 Todo API,路由怎么定?" 完整 CRUD 实战 参数装饰器选择 "什么场景用 @Param、@Body、@Query?" 装饰器速查表 启动报错排查 "NestJS 跑不起来 / 404,怎么排查?" 常见报错速查 "#%E5%B8%B8%E8%A7%81%E6%8A%A5%E9%94%99%E9%80%9F%E6%9F%A5") |
💡 复习建议:先把每章对应知识点看一遍,再合上文档用自己的话讲一遍------讲不出来就是没懂。
🎓 最终总结(本篇回顾)
如果你读到了这里,恭喜你 🎉------你已经系统地走完了 NestJS 入门的第一阶段!
这篇你学到了什么?
| 知识板块 | 核心收获 |
|---|---|
| 🧭 框架定位 | NestJS 是纯后端框架,和 Next.js 各司其职 |
| 🔧 环境搭建 | CLI 安装 + 项目创建 + 3 个常见报错实战解决 |
| 🏗️ 架构模式 | 工厂模式、装饰器模式、模块化设计 |
| 🧩 核心三件套 | Module(登记)+ Controller(接客)+ Service(干活) |
| 💉 依赖注入 | 不用 new,NestJS 自动送实例上门 |
| 📡 RESTful API | 7 个文件手写完整 CRUD,含数据流图解 |
| 📘 TS 基础 | interface、throw/return、Partial、数组方法等 |
| 🎯 面试准备 | 4 大类常考点 + 标准回答思路 |