
如果有人问我:"谈谈你用的框架。" 如果我只回答"NestJS 是个好用的 Node 后端框架",那就只是一个会用工具的搬运工。 真正有价值的是:我能说清楚它为什么这么设计,以及这种设计如何服务于 AI 应用后端。
这篇文章不写教程,不罗列 API。我用一条主线索------从工厂模式、依赖注入、装饰器、模块化、AOP 这五个设计哲学点出发,复盘我写的 NestJS 项目,把它讲成一个"软件工程思想"的故事。
一、从蜜雪冰城的冰淇淋工厂说起
我随手写过一个例子(factory_demo/1.mjs):蜜雪冰城有很多产品------冰淇淋、柠檬水、奶茶。如果开发者要自己 new 每一个产品类,他得记住十几个类的构造函数细节,累死,而且换个产品就得改调用方代码。

于是有了工厂:
js
const drink = MixueFactory.create('ice'); // 我只说"我要冰淇淋",别的都不用管
工厂模式的本质就一句话:把"如何创建对象"这件事,从使用者手里抽象走。 使用者不需要知道类是怎么实现的,只要给一个类型,工厂返回一个合格的产品。将来企业加一款"芋泥啵啵",调用方一行都不用改。
再看 NestJS 的入口(src/main.ts):

ts
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3005);
NestFactory.create(AppModule) 就是 Nest 最大的那只"工厂"。它做的远比"new 一个 app"多:它要扫描模块图、分析依赖、把每个 provider 实例化并注入到位、装配好整个应用再交给你。你只给了它一个根模块,它还给你的是一整套跑起来的后端系统。
把它迁移到 AI 后端想想 :我们的 LLM 客户端、向量数据库、embedding 模型、缓存、鉴权服务,本质都是"产品"。靠工厂统一生产,哪天要把 OpenAI 换成本地部署的模型,只改工厂这一处配置,所有调用方不动。工厂模式换来的不是少写两行代码,而是"变化隔离"。 这正好是我理解后端最重要的价值之一------让系统能演进。
二、依赖注入:把"控制权"反转过来
工厂解决"谁来创建",依赖注入解决"创建完往哪儿放"。
传统写法里,一个 Controller 要干活,得自己 new 一个 Service:
ts
const service = new TodosService(); // 耦合在这里
而 NestJS 里我是这样写的(src/todos/todos.controller.ts):
ts
constructor(private readonly todosService: TodosService) {}
我只"声明我需要一个 TodosService",至于它是谁创建的、什么时候创建的、是不是同一个实例,全部交给容器。这就是控制反转(IoC)------创建和装配的控制权,从"我"反转给了"框架"。
这一反转带来三件我面试时一定会讲的好处:
- 可测试性。单元测试里我可以注入一个 mock 的 Service,不碰数据库、不碰网络,快且稳。
- 可替换性。Service 的实现可以换(内存数组 → MySQL → Redis),Controller 完全无感。
- 关注点分离。对象不再关心"怎么被别人造出来",只关心"我提供什么能力"。
AI 应用视角:一条 RAG 检索链路------embedding 模型、向量检索器、LLM、重排器------每条都是可注入的依赖。我要做"换模型 A/B 对比",或者"测试时注入假模型",依赖注入让这一切变成配置级别的操作,而不是改业务代码。
三、装饰器:从"命令式"走向"声明式"
装饰器模式的核心是:在不修改原对象的前提下,动态地给对象叠加额外能力。
NestJS 把装饰器用到了极致,我写接口时完全换了一种表达方式:
ts
@Controller('todos') // 声明:这个类负责 /todos 路由
export class TodosController {
@Get(':id') // 声明:这个方法处理 GET /todos/:id
findOne(@Param('id') id: string): Todo {
return this.todosService.findOne(Number(id));
}
}
注意对比。命令式写法是:
ts
if (req.method === 'GET' && req.url.startsWith('/todos/')) {
const id = req.url.split('/').pop();
// ...
}
装饰器写法则是:我不描述"怎么判断这个请求属于谁",我只标注"这个请求属于谁"。 判断逻辑由框架去完成。
这是一次思维模式的升级,而且我发现它和前端、和 LLM 是相通的:
- React 用 JSX 声明式地描述 UI:"页面长这样",而不是"一步步操作 DOM";
- 我们给 LLM 写 prompt 也是在声明意图:"请做这件事",而不是逐行给指令;
- NestJS 用装饰器声明式地描述接口契约:"这个端点做什么、参数是什么"。
声明式思想的本质是:把"怎么做到"交给框架/模型,把"要什么"留给人类。 这在 AI 时代尤其重要------我们正在把越来越多"如何"的部分交给机器。
四、模块化与 MVC:企业的组织哲学
NestJS 最外显的设计是模块化 (src/app.module.ts):
ts
@Module({
imports: [TodosModule], // 依赖其他模块
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
AppModule 是根模块,像乐高积木的底座;每个业务一块积木(TodosModule),里面自洽地包含三件套:
- Controller:接收请求、校验参数、返回响应------薄的一层,只做"对外接口";
- Service:真正的业务逻辑、数据访问------厚的一层;
- Module:声明"这个积木由哪些零件组装而成"。
这就是 MVC。注释里我写了一句很关键的话:"视图层不可以直接去数据库查数据。" 前端拿到的永远是 Service 层处理过的、稳定、安全的数据,中间隔着 controller 和 service 两层。
为什么企业级应用一定强调这种分层和模块化?因为企业级应用不是一个人的项目,而是几十个人协作、演进十年的系统。模块化让每个人只操心自己那一块,让每个团队可以独立迭代,让错误被边界挡住。企业级 ≠ 复杂,而是"复杂可控"。
AI 应用后端同样需要分层:
- API 层:鉴权、限流、参数校验;
- 编排层:agent 的流程控制、多模型调度、工具调用;
- 基础设施层:向量库、模型服务、缓存、消息队列。
不分层,一条 prompt 从 API 一直糊到数据库,出一个 bug 全网崩溃,AI 时代数据更敏感,更要守好边界。
五、AOP:NestJS 最被低估的武器
这是我最想展开讲的一点,也是面试里最能体现深度的点。
后端有一类需求------日志、鉴权、参数校验、统一异常、超时、限流------它们不属于任何具体业务,却散落在每一个业务方法里。如果每个方法都写一遍 try/catch + 日志 + 鉴权判断,代码会腐化得很快。
AOP(面向切面编程)把这些横切关注点单独抽出来,统一处理。NestJS 对应四类武器:
| 机制 | 作用 | AI 后端场景 |
|---|---|---|
| Pipe 管道 | 参数校验与转换 | 校验用户输入、截断超长文本 |
| Guard 守卫 | 鉴权 | 只有登录用户才能调 agent 接口 |
| Interceptor 拦截器 | 日志/超时/缓存/计费 | LLM 调用超时、token 计费、响应缓存 |
| ExceptionFilter 过滤器 | 统一异常输出 | 模型超时/限流 → 统一错误码 |
我在 todos 里就尝到了甜头(src/todos/todos.service.ts):
ts
if (!todo) throw new NotFoundException(`Todo ${id} 不存在`);
注意,我不是在业务里手写 res.status(404).json(...),而是抛出一个 Nest 内置的异常类,由全局过滤器统一翻译成标准 JSON 响应。这就是从"过程式错误处理"到"体系化错误处理"的转变。
为什么 AI 后端特别需要 AOP? 因为 LLM 调用是"不稳定且昂贵"的:会超时、会 rate limit、会上下文超长、按 token 花钱。这些横切问题,恰恰最需要拦截器统一兜底------超时重试、熔断、计费日志,全部切面化,而不是散落在每个 agent 函数里。
面试官问"你怎么处理后端报错",别只答 try/catch/finally------那是过程。答**"标准化异常类 + 全局过滤器,把错误变成稳定可预期的 API 契约"**,才是体系。
六、我的面试回答范式
把上面串起来,我在面试时会这样回答"你为什么选择 / 怎么理解 NestJS":
NestJS 对 AI 应用后端的价值,不在于它是 Node 还是 TS,而在于它的设计哲学正好命中 AI 后端的痛点:
- 模块化 + MVC,让鉴权、计费、用户、AI 编排各成模块,几十人协作不乱;
- 依赖注入,让 LLM、向量库、模型都可以替换和 mock,天然支持 A/B 与测试;
- 装饰器带来声明式编程,接口契约清晰可读;
- AOP(管道/守卫/拦截器/过滤器),把 LLM 调用的超时、限流、计费、错误统一切面化处理;
- 工厂模式(NestFactory)+ IoC 容器,把对象创建和装配的控制权交给框架,实现变化隔离。
最终它服务的目标是:让 AI 应用的"传统后端部分"(用户、鉴权、数据、计费)达到企业级稳定,从而让我可以把精力集中在"AI 编排"这一真正核心的部分。
写在最后
从蜜雪冰城的冰淇淋工厂,到 NestFactory.create(AppModule);从 new TodosService() 的耦合,到声明一句"我需要一个 Service";从手写 if 判断路由,到 @Get(':id') 一声标注------这些不是语法糖,而是一整套软件工程思想的落点:
把变化隔离在背后,把意图写在明面上。
面试真正考察的,从来不是"你用过什么",而是**"你有没有透过代码看到背后的思想"**。工具会过时,设计哲学不会------今天它是 NestJS 的模块化和依赖注入,明天它就是 AI 编排里的切面和协议。
这也是我从"会写接口"走向"会设计系统"的第一课。