🚀 NestJS 完全入门指南: Module/Controller/Service讲解 + 实战 CRUD

📌 写在最前面:为什么你要读这篇?

被 Next.js 全栈能力「宠坏」的前端,突然接手 NestJS 项目------看着 @Module()@Controller() 一脸懵?又或者被 ERR_PNPM_IGNORED_BUILDS 卡了一下午,怀疑人生?

这篇 2 万字的保姆级教程,用大量表格 + 餐厅比喻 + 真实踩坑 ,把 NestJS 核心概念拆解得明明白白。全文覆盖:框架定位、环境搭建、装饰器/DI/MVC 三大件、完整 CRUD 实战、面试速查表。一篇顶十篇,建议先收藏。


📚 本文目录

章节 内容概要 重要度
1️⃣ NestJS 是什么 框架定位 + 与 Next.js 对比(小白版) ⭐⭐⭐⭐⭐
2️⃣ 后端开发是做些什么 API、集成、并发、微服务,给前端「喂饭」 ⭐⭐⭐⭐
3️⃣ 安装与环境搭建 脚手架安装 + 新建项目 + 启动 + 踩坑实录 ⭐⭐⭐⭐⭐
4️⃣ 常见报错速查 EADDRINUSECannot GETTS1261 实战解决 ⭐⭐⭐⭐⭐
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 个角色撞车)

  1. pnpm 10+ 当保安 🛡️:出于安全考虑,默认禁止任何第三方包在安装时跑"构建脚本"(防恶意包挖矿/盗密),必须用户明确批准
  2. unrs-resolver 被迫营业 🔧:NestJS 底层依赖(用于解析模块路径),必须跑构建脚本才能编译原生代码,不跑就半残
  3. 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

正确姿势 ✅:

  1. 终端出现一个交互列表
  2. 按空格 勾选 unrs-resolver(一定要勾选!)
  3. 再回车 确认

错误姿势 ❌:直接按回车 → 所有包被默认标记为 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 找不到路由)

含义:路由没注册成功,多半是改了代码但服务没重启。

排查清单(按顺序查):

  1. AppModule 是否 import 了 TodosModule ------ 看 app.module.tsimports 数组里有没有 TodosModule
  2. TodosModule 是否注册了 controller ------ 看 todos.module.tscontrollers: [TodosController]
  3. controller 装饰器是否正确 ------ @Controller('todos') + @Get() 拼起来才等于 /todos
  4. 服务有没有重启 ------ 默认 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.tstodos.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);
//           ↑ 调用者啥都不用管,工厂内部全包了

工厂的优势:

  1. 封装复杂创建过程:用户不用关心初始化顺序、依赖关系
  2. 统一入口 :所有应用都用 NestFactory.create,写法一致
  3. 支持多场景:同一套代码可以创建 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 {}

然后在 UserModuleimports: [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 的责任

  1. 接 HTTP 请求(路由匹配)
  2. 抽参数(@Param@Body 等)
  3. 调 Service 处理
  4. 把 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 的责任

  1. 业务逻辑(校验、计算、转换)
  2. 数据访问(CRUD 假数据 / 调数据库 / 调第三方 API)
  3. 抛业务异常(找不到、不合法等)

🔟 依赖注入(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

类型签名只约束 returnfindOne(): 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' * 11(不推荐)

推荐用 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️⃣ 开发流程 & 错误处理

🚀 开发流程

完整开发一个业务模块的步骤:

  1. 写 Servicexx.service.ts):定义接口、假数据、CRUD 方法
  2. 写 Controllerxx.controller.ts):声明路由、抽参数、转发给 Service
  3. 写 Modulexx.module.ts):装配 controller 和 service
  4. 挂到根模块app.module.ts):在 imports 数组里 import 业务模块
  5. 启动开发服务器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 "Partial、Pick、Omit 区别?什么时候用?" Partial 工具类型 显式 vs 隐式转换 "Number(id) 和 +id 有啥不同?哪个推荐?" 显式 vs 隐式类型转换 后置自增 "i++ 和 ++i 区别?返回啥?" 后置自增 nextId++ 展开运算符 "合并两个对象,相同字段如何处理?" 展开运算符 ... Object.assign "Object.assign 是深拷贝还是浅拷贝?" Object.assign:合并对象 类型约束 "interface 和 type 有啥区别?类型能保证运行时不出错吗?" 类型约束(接口 interface) "#throw-vs-return%E6%A0%B8%E5%BF%83%E5%8C%BA%E5%88%AB")
find vs findIndex "findfindIndex 返回值有啥区别?判断找不到怎么写?" [find vs findIndex](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial "Partial、Pick、Omit 区别?什么时候用?" Partial 工具类型 显式 vs 隐式转换 "Number(id) 和 +id 有啥不同?哪个推荐?" 显式 vs 隐式类型转换 后置自增 "i++ 和 ++i 区别?返回啥?" 后置自增 nextId++ 展开运算符 "合并两个对象,相同字段如何处理?" 展开运算符 ... Object.assign "Object.assign 是深拷贝还是浅拷贝?" Object.assign:合并对象 类型约束 "interface 和 type 有啥区别?类型能保证运行时不出错吗?" 类型约束(接口 interface) "#find-vs-findindex%E5%85%B3%E9%94%AE%E5%8C%BA%E5%88%AB")
Partial<T> "PartialPickOmit 区别?什么时候用?" [Partial<T> 工具类型](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial "Partial、Pick、Omit 区别?什么时候用?" Partial 工具类型 显式 vs 隐式转换 "Number(id) 和 +id 有啥不同?哪个推荐?" 显式 vs 隐式类型转换 后置自增 "i++ 和 ++i 区别?返回啥?" 后置自增 nextId++ 展开运算符 "合并两个对象,相同字段如何处理?" 展开运算符 ... Object.assign "Object.assign 是深拷贝还是浅拷贝?" Object.assign:合并对象 类型约束 "interface 和 type 有啥区别?类型能保证运行时不出错吗?" 类型约束(接口 interface) "#partialt-%E5%B7%A5%E5%85%B7%E7%B1%BB%E5%9E%8B")
显式 vs 隐式转换 "Number(id)+id 有啥不同?哪个推荐?" [显式 vs 隐式类型转换](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial "Partial、Pick、Omit 区别?什么时候用?" Partial 工具类型 显式 vs 隐式转换 "Number(id) 和 +id 有啥不同?哪个推荐?" 显式 vs 隐式类型转换 后置自增 "i++ 和 ++i 区别?返回啥?" 后置自增 nextId++ 展开运算符 "合并两个对象,相同字段如何处理?" 展开运算符 ... Object.assign "Object.assign 是深拷贝还是浅拷贝?" Object.assign:合并对象 类型约束 "interface 和 type 有啥区别?类型能保证运行时不出错吗?" 类型约束(接口 interface) "#%E6%98%BE%E5%BC%8F-vs-%E9%9A%90%E5%BC%8F%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2")
后置自增 "i++++i 区别?返回啥?" [后置自增 nextId++](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial "Partial、Pick、Omit 区别?什么时候用?" Partial 工具类型 显式 vs 隐式转换 "Number(id) 和 +id 有啥不同?哪个推荐?" 显式 vs 隐式类型转换 后置自增 "i++ 和 ++i 区别?返回啥?" 后置自增 nextId++ 展开运算符 "合并两个对象,相同字段如何处理?" 展开运算符 ... Object.assign "Object.assign 是深拷贝还是浅拷贝?" Object.assign:合并对象 类型约束 "interface 和 type 有啥区别?类型能保证运行时不出错吗?" 类型约束(接口 interface) "#%E5%90%8E%E7%BD%AE%E8%87%AA%E5%A2%9E-nextid")
展开运算符 "合并两个对象,相同字段如何处理?" [展开运算符 ...](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial "Partial、Pick、Omit 区别?什么时候用?" Partial 工具类型 显式 vs 隐式转换 "Number(id) 和 +id 有啥不同?哪个推荐?" 显式 vs 隐式类型转换 后置自增 "i++ 和 ++i 区别?返回啥?" 后置自增 nextId++ 展开运算符 "合并两个对象,相同字段如何处理?" 展开运算符 ... Object.assign "Object.assign 是深拷贝还是浅拷贝?" Object.assign:合并对象 类型约束 "interface 和 type 有啥区别?类型能保证运行时不出错吗?" 类型约束(接口 interface) "#%E5%B1%95%E5%BC%80%E8%BF%90%E7%AE%97%E7%AC%A6-")
Object.assign "Object.assign 是深拷贝还是浅拷贝?" [Object.assign:合并对象](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial "Partial、Pick、Omit 区别?什么时候用?" Partial 工具类型 显式 vs 隐式转换 "Number(id) 和 +id 有啥不同?哪个推荐?" 显式 vs 隐式类型转换 后置自增 "i++ 和 ++i 区别?返回啥?" 后置自增 nextId++ 展开运算符 "合并两个对象,相同字段如何处理?" 展开运算符 ... Object.assign "Object.assign 是深拷贝还是浅拷贝?" Object.assign:合并对象 类型约束 "interface 和 type 有啥区别?类型能保证运行时不出错吗?" 类型约束(接口 interface) "#objectassign%E5%90%88%E5%B9%B6%E5%AF%B9%E8%B1%A1")
类型约束 "interface 和 type 有啥区别?类型能保证运行时不出错吗?" [类型约束(接口 interface)](#知识点 面试常见问法 所在章节 throw vs return "为什么找不到数据要 throw 而不是 return undefined?" throw vs return find vs findIndex "find 和 findIndex 返回值有啥区别?判断找不到怎么写?" find vs findIndex Partial "Partial、Pick、Omit 区别?什么时候用?" Partial 工具类型 显式 vs 隐式转换 "Number(id) 和 +id 有啥不同?哪个推荐?" 显式 vs 隐式类型转换 后置自增 "i++ 和 ++i 区别?返回啥?" 后置自增 nextId++ 展开运算符 "合并两个对象,相同字段如何处理?" 展开运算符 ... Object.assign "Object.assign 是深拷贝还是浅拷贝?" Object.assign:合并对象 类型约束 "interface 和 type 有啥区别?类型能保证运行时不出错吗?" 类型约束(接口 interface) "#%E7%B1%BB%E5%9E%8B%E7%BA%A6%E6%9D%9F%E6%8E%A5%E5%8F%A3-interface")

综合实战题

知识点 面试常见问法 所在章节
完整 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 大类常考点 + 标准回答思路

🔜 下一篇继续

相关推荐
开心就好20251 小时前
appuploader-cli 使用教程:在 Windows 上用命令行把 IPA 上传到 App Store
后端·ios
倾颜1 小时前
Node.js 为什么会慢?从 Event Loop、CPU、内存到并发控制聊性能问题
后端
用户921080262861 小时前
AI 应用平台为什么要拆分 Java 后端和 Python AI 服务
后端
星火10241 小时前
【LangChain4j系列08】Agentic AI 多智能体协作
人工智能·后端
凤山老林1 小时前
零停机数据库演进:Spring Boot 集成 Flyway 与平滑DDL变更策略
数据库·spring boot·后端
步行cgn1 小时前
MyBatis <sql> 标签详解:SQL 片段的定义与复用
后端
想要成为糕糕手1 小时前
🏭 设计模式之工厂模式:从蜜雪冰城到 NestJS,把「new」外包出去
后端·nestjs
抓哇小菜鸡1 小时前
Spring Boot + 本地大模型(Ollama/DeepSeek) + MyBatis-Plus 企业级智能体数据分析系统从零到一源码全解析
spring boot·后端·mybatis
星火10241 小时前
【LangChain4j系列07】结构化输出与类型安全
人工智能·后端