【NestJS】18 项目架构全景图:从零散知识到完整体系
把学到的所有知识点串起来,建立完整的认知体系。不讲新东西,只帮你理清思路。
前言 ✨
学完了 NestJS 的各个知识点,你可能会有这样的感觉:
- 🤔 每个概念都懂,但不知道怎么组合在一起
- 🤔 写代码时不知道该在哪个文件里写
- 🤔 看别人的项目,感觉很复杂,不知道从哪里看起
这很正常。学习技术就像拼图,你需要先看到完整的画面,才知道每块拼图应该放在哪里。
这篇文章不讲新东西,只做一件事:把零散的知识点串成一条线,帮你建立完整的知识体系。
💡 版本说明:NestJS 11.0.1 + Node.js 20+
让我们开始吧!
一、项目的骨架:从入口到模块 🏗️
1.1 应用的起点:main.ts
想象你在建一栋楼,main.ts 就是地基。所有东西都从这里开始。
它做了什么?
1. 创建应用实例(NestFactory.create) ↓2. 配置全局功能 - ValidationPipe:验证所有输入数据 - CORS:允许跨域请求 - 全局拦截器:统一响应格式 - 全局过滤器:统一错误处理 ↓3. 启动服务器(listen(3000))关键理解:
main.ts是整个应用的”总开关”- 在这里配置的东西,对所有接口都生效
- 你不需要在每个接口里重复配置
💡 类比:就像你家的总电闸,控制整个房子的电。
1.2 应用的大脑:AppModule
如果 main.ts 是地基,那 AppModule 就是建筑的主体结构。
它的三个核心职责:
1. 全局配置(imports)
ConfigModule → 管理环境变量(API Key、数据库地址)MongooseModule → 连接数据库JwtModule → 配置 JWT 认证PassportModule → 配置认证策略2. 功能模块(imports)
UserModule → 用户管理InterviewModule → 面试功能PaymentModule → 支付功能ResumeModule → 简历管理...3. 全局组件(providers)
ResponseInterceptor → 统一响应格式AllExceptionsFilter → 统一错误处理关键理解:
AppModule是项目的”目录”,所有功能都在这里注册- 全局配置在这里,功能模块也在这里
- 想知道项目有什么功能?看
AppModule就够了
💡 类比:就像一本书的目录,告诉你每一章讲什么。
1.3 模块化的本质
为什么要分模块?
想象你在整理房间:
- ❌ 不分模块:所有东西堆在一起,找不到
- ✅ 分模块:衣服放衣柜,书放书架,工具放工具箱
模块的四个部分:
@Module({ imports: [...] // 我需要用到的其他模块 providers: [...] // 我提供的服务(Service) controllers: [...] // 我提供的接口(Controller) exports: [...] // 我允许别人用的服务})关键理解:
- 每个模块是一个独立的功能单元
- 模块内部的东西,外部默认看不到
- 想让别人用你的 Service?必须
exports
💡 类比:模块就像乐高积木,每块积木独立,但可以组合。
二、请求的旅程:从进入到返回 🚀
2.1 完整的请求流程
假设用户发起一个请求:POST /interview/mock/start
它会经历这些步骤:
1. 请求进入应用(main.ts) ↓2. ValidationPipe 验证数据 - 检查参数格式是否正确 - 不符合?直接拒绝,返回 400 错误 ↓3. JwtAuthGuard 验证身份 - 检查 Token 是否有效 - 无效?返回 401 错误(未认证) ↓4. RolesGuard 验证权限 - 检查用户是否有权限 - 没权限?返回 403 错误(禁止访问) ↓5. 到达 Controller - 找到对应的方法 - 提取参数 ↓6. Controller 调用 Service - Service 处理业务逻辑 - 可能调用其他 Service - 可能查询数据库 ↓7. 返回数据 ↓8. ResponseInterceptor 格式化响应 - 统一包装成 { code, message, data } ↓9. 返回给客户端如果出错了呢?
任何一步出错 ↓抛出异常 ↓AllExceptionsFilter 捕获 ↓格式化成统一的错误响应 ↓返回给客户端关键理解:
- 请求像流水线,每一步都有明确的职责
- 验证、认证、授权都在 Controller 之前完成
- 出错了不用担心,Filter 会统一处理
💡 类比:就像机场安检,先验票、再安检、再登机,每一步都有人负责。
2.2 各层的职责
ValidationPipe:数据守门员
- 职责:确保数据格式正确
- 时机:最早,在所有逻辑之前
- 例子:检查邮箱格式、密码长度
Guard:身份守门员
- 职责:确认”你是谁”和”你能做什么”
- 时机:在 Controller 之前
- 例子:JWT 认证、角色权限
Controller:接待员
- 职责:接收请求,调用 Service
- 原则:只做”接待”,不做”业务”
- 例子:提取参数,调用 Service
Service:业务专家
- 职责:处理所有业务逻辑
- 原则:不关心 HTTP,只关心业务
- 例子:创建用户、生成面试题
Interceptor:包装员
- 职责:统一处理请求和响应
- 时机:在 Controller 前后
- 例子:统一响应格式、记录日志
Filter:救火员
- 职责:捕获所有错误
- 时机:任何地方出错都会来这里
- 例子:统一错误格式、记录错误日志
关键理解:
- 每一层只做自己的事,不越界
- 这叫”关注点分离”(Separation of Concerns)
- 好处:代码清晰、易于维护、易于测试
三、模块设计:从简单到复杂 📦
3.1 简单模块:UserModule
UserModule 做什么?
- 管理用户信息
- 提供用户查询和修改功能
它的结构:
UserModule├── User Schema → 用户数据结构├── UserService → 用户业务逻辑├── UserController → 用户接口└── exports: [UserService] → 导出给其他模块用为什么要导出 Service?
因为其他模块需要用户数据:
InterviewModule需要检查用户配额PaymentModule需要更新用户余额ResumeModule需要关联用户简历
关键理解:
- 简单模块只负责一个功能领域
- 通过
exports让其他模块可以使用 - 这是模块间协作的基础
3.2 复杂模块:InterviewModule
InterviewModule 做什么?
- 简历押题:用户上传简历,AI 生成问题
- 模拟面试:AI 作为面试官,与用户对话
它为什么复杂?
因为它需要协调多个功能:
InterviewModule 需要:├── 访问用户数据 → 导入 UserModule├── 访问简历数据 → 导入 ResumeModule├── 调用 AI 服务 → 使用 InterviewAIService├── 解析文档 → 使用 DocumentParserService├── 访问配置 → 导入 ConfigModule└── 访问多个数据表 → 导入多个 Schema三个 Service 的分工:
InterviewService:指挥官
- 协调整个流程
- 调用其他 Service
- 处理业务逻辑
InterviewAIService:AI 代理
- 专门负责调用 AI API
- 构造 Prompt
- 处理 AI 响应
DocumentParserService:解析器
- 专门负责解析文档
- 提取文本内容
- 支持多种格式
关键理解:
- 复杂模块需要多个 Service 协作
- 每个 Service 只负责一个领域
- 通过依赖注入组合在一起
💡 类比:就像一个公司,CEO(InterviewService)指挥,技术总监(AIService)负责技术,行政(ParserService)负责文档。
3.3 模块间的依赖关系
整个项目的模块关系:
AppModule(根模块) ↓ ├── UserModule(核心) │ └── 导出 UserService │ ├── InterviewModule(中心) │ ├── 导入 UserModule │ ├── 导入 ResumeModule │ └── 导入 ConfigModule │ ├── PaymentModule │ └── 导入 UserModule │ ├── ResumeModule │ └── 导入 UserModule │ └── WechatModule └── 导入 UserModule关键理解:
UserModule是核心,几乎所有模块都依赖它InterviewModule是中心,它协调多个模块- 模块间通过
imports和exports建立依赖
四、核心概念深度理解 💡
4.1 为什么要分模块?
三个核心原因:
1. 代码清晰
不分模块:所有代码在一起 → 找不到 → 改不动
分模块:用户相关在 UserModule → 一眼就找到2. 易于维护
不分模块:改一个功能,影响其他功能 → 容易出 bug
分模块:改 UserModule,不影响 InterviewModule3. 易于扩展
不分模块:加新功能,不知道往哪加 → 代码越来越乱
分模块:加新功能,创建新模块 → 结构清晰4.2 为什么 InterviewModule 要访问多个 Schema?
因为一个完整的业务流程涉及多个数据:
用户发起面试:1. 检查用户配额 → User Schema2. 获取用户简历 → Resume Schema3. 生成面试问题 → Interview Schema4. 记录消费记录 → UserTransaction Schema5. 保存面试结果 → InterviewResult Schema关键理解:
- 真实项目的功能都是”跨数据表”的
- 一个功能完整实现,需要多个数据支撑
- 这在实际开发中非常常见
4.3 为什么要用全局拦截器和过滤器?
问题:如果不用全局组件,会怎样?
❌ 每个 Controller 都要写:@UseInterceptors(ResponseInterceptor)@UseFilters(AllExceptionsFilter)
结果:- 50 个接口,写 50 遍- 改一次,改 50 个地方- 容易漏掉,不统一解决:用全局组件
✅ 在 main.ts 注册一次:app.useGlobalInterceptors(new ResponseInterceptor())app.useGlobalFilters(new AllExceptionsFilter())
结果:- 写一次,处处生效- 改一个地方,全部更新- 不会漏,完全统一关键理解:
- 全局组件遵循 DRY 原则(Don’t Repeat Yourself)
- 减少重复代码,降低维护成本
- 保证所有接口行为一致
4.4 为什么要用 Swagger?
对比:手写文档 vs Swagger
手写文档(如 Postman):
❌ 改了接口,忘记更新文档❌ 前端看的文档是旧的❌ 前后端理解不一致❌ 浪费时间对接Swagger 自动文档:
✅ 改了代码,文档自动更新✅ 前端看的永远是最新的✅ 前后端理解完全一致✅ 可以直接在网页测试关键理解:
- Swagger 让文档和代码同步
- 降低前后端沟通成本
- 提高开发效率
五、开发实战:如何添加新功能 🛠️
5.1 开发流程
假设你要开发一个新功能:“用户收藏面试题”
完整流程:
1. 确定模块 → 属于 InterviewModule
2. 设计数据结构(Schema) → 创建 FavoriteQuestion Schema → 字段:userId, questionId, createdAt
3. 定义接口参数(DTO) → CreateFavoriteDto: { questionId } → 返回值:{ id, questionId, createdAt }
4. 编写业务逻辑(Service) → FavoriteService.create() → 检查是否已收藏 → 保存到数据库
5. 暴露接口(Controller) → POST /interview/favorite → @UseGuards(JwtAuthGuard)
6. 添加认证 → 需要登录才能收藏
7. 编写文档(Swagger) → @ApiOperation({ summary: '收藏面试题' }) → @ApiResponse({ status: 201, description: '收藏成功' })
8. 测试 → 打开 Swagger UI → 测试接口关键理解:
- 开发有固定的流程
- 从数据到逻辑,从逻辑到接口
- 每一步都有明确的目标
5.2 代码应该写在哪里?
常见困惑:
❓ 验证用户配额,写在哪里?✅ 写在 Service 里(业务逻辑)
❓ 检查 Token,写在哪里?✅ 用 Guard(认证逻辑)
❓ 统一响应格式,写在哪里?✅ 用 Interceptor(横切关注点)
❓ 验证参数格式,写在哪里?✅ 用 DTO + ValidationPipe(数据验证)
❓ 捕获错误,写在哪里?✅ 用 Filter(错误处理)关键理解:
- 每一层有明确的职责
- 不要把业务逻辑写在 Controller
- 不要把 HTTP 相关的东西写在 Service
六、核心收获总结 🎯
6.1 项目组织
main.ts → 应用入口,全局配置AppModule → 根模块,组织所有功能功能模块 → 按业务领域划分6.2 请求流程
请求进入 ↓验证数据(Pipe) ↓验证身份(Guard) ↓处理请求(Controller → Service) ↓格式化响应(Interceptor) ↓返回客户端6.3 模块协作
通过 imports 导入其他模块通过 exports 导出 Service通过构造函数注入使用 Service6.4 设计原则
关注点分离 → 每一层只做自己的事单一职责 → 每个类只有一个改变的理由DRY 原则 → 不要重复代码依赖注入 → 让框架管理依赖结语 🎉
恭喜你! 你已经建立了完整的 NestJS 知识体系。
现在的你:
- ✅ 理解了项目的整体架构
- ✅ 知道了各个部分如何协作
- ✅ 掌握了开发的完整流程
- ✅ 能够独立开发新功能
记住这三句话:
- 模块化让代码清晰 - 相关的东西放在一起
- 分层让职责明确 - 每一层只做自己的事
- 全局组件减少重复 - 写一次,处处生效
下一步:
- 🚀 动手开发一个完整功能
- 🚀 阅读优秀的开源项目
- 🚀 在实践中不断优化
技术的学习没有终点,但你已经掌握了最重要的基础。 💪
继续加油!(。・ω・。)ノ♡
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!