🚀 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 大类常考点 + 标准回答思路

🔜 下一篇继续

相关推荐
BingoGo6 小时前
PHP clone 之后,为什么改副本会影响原对象?
后端·php
JaguarJack6 小时前
PHP clone 之后,为什么改副本会影响原对象?
后端·php·服务端
小灰灰搞电子6 小时前
Rust+Slint 实现动态消息提示框源码分享
开发语言·后端·rust
小奏技术7 小时前
10 MB 的 Postman 替代品,启动不到 1 秒
后端
东风破_7 小时前
Text2SQL :用自然语言操作 SQLite 数据库
人工智能·后端
IT_陈寒11 小时前
Python的多线程就是个假把式,我算是体验到了
前端·人工智能·后端
码事漫谈11 小时前
如果 AI 要圈养人类,它可能不需要笼子
后端
newerp12 小时前
Golang 调度循环:Go runtime 如何永不停歇地找人干活
后端·程序员·go
明月_清风12 小时前
AI 时代,为什么架构师又开始谈"本体论"?
人工智能·后端·agent