写在前面:前几节课还在用 Next.js 搭博客搞全栈,今天画风一转------NestJS 。老师说 Next.js 是全栈框架,NestJS 则是 Node 的纯后端企业级开发框架,默认 TypeScript,全面模块化。今天从
nest new开始,用工厂模式启动应用、用模块拆分业务、用装饰器给类贴标签、用依赖注入让 Service 自动到岗,最后手搓了一个完整的 Todos CRUD。最妙的是,课堂用蜜雪冰城来类比工厂模式------NestFactory 就是 MixueFactory。以下所有代码均来自课堂真实文件。
一、NestJS 是什么:Next.js 的"后端表哥"
一句话区分:
| 框架 | 定位 | 擅长 |
|---|---|---|
| Next.js | 全栈框架 | 前端 SSR/SSG + API 路由 |
| NestJS | 纯后端框架 | 企业级 API、微服务、AI Infra |
readme 开篇就定了调子:
"Next.js 全栈,NestJS 就是 Node 的纯后端企业级开发框架。默认使用 TypeScript,全面模块化思想,适合构建企业级服务。"
后端开发到底做什么?readme 列了三个方向:
- 提供 API 接口(Web 开发)
- 系统集成(并发、底层服务、AI Infra)
- 微服务
安装也极其简单:
bash
npm i -g @nestjs/cli
nest new hello
装完 CLI,一条命令创建项目。目录结构精简到令人发指:
ruby
src/
├── main.ts # 入口文件
└── app.module.ts # 根模块
就两个文件,一个管启动,一个管组织。NestJS 的哲学已经藏在目录结构里了------入口归入口,业务归业务。
二、工厂模式:想喝奶茶,不用自己泡
NestJS 的第一个门槛不是路由,不是中间件,而是------工厂模式。
readme 用了一个绝妙的比喻:
"你想喝奶茶,不用自己做(流程代码 NO),找蜜雪冰城。"
蜜雪冰城有什么?柠檬水、圣代、芋圆葡萄、冰激凌、珍珠奶茶......
你不需要知道每种饮品怎么做的,你只需要跟柜台说一句"要一杯柠檬水",工厂负责生产。这就是工厂模式的本质------调用者和生产者解耦。
课堂的 1.mjs 写了一个完整的蜜雪冰城工厂:
javascript
// 蜜雪冰城产品之一:冰激凌
class IceCream {
constructor() {
this.name = '冰激凌';
this.price = 3;
}
show() {
console.log(`${this.name} 价格${this.price} 元`)
}
}
class LemonTea {
constructor() {
this.name = '柠檬水';
this.price = 4;
}
show() {
console.log(`${this.name} 价格${this.price} 元`)
}
}
class MilkTea {
constructor() {
this.name = '珍珠奶茶';
this.price = 8;
}
show() {
console.log(`${this.name} 价格${this.price} 元`)
}
}
// 工厂类
class MixueFactory {
static create(type) {
switch(type) {
case 'ice': return new IceCream();
case 'lemon': return new LemonTea();
case 'milk': return new MilkTea();
}
}
}
const drink1 = MixueFactory.create('ice');
drink1.show(); // 冰激凌 价格3 元
const drink2 = MixueFactory.create('lemon');
drink2.show(); // 柠檬水 价格4 元
注意一个关键设计:每个产品类都实现了相同的 show() 接口。 冰激凌 3 块、柠檬水 4 块、珍珠奶茶 8 块------价格不同,但都叫 show()。这意味着不管工厂返回哪个类,调用者都可以放心地直接调 show(),不用关心具体是哪种产品。
readme 的总结:
"由于工厂里的每个类都实现了相同的 show 接口,由工厂类生产出的类,可以放心的直接调用。"
NestJS 里这个工厂叫 NestFactory。main.ts 入口文件就一行核心代码:
typescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
// 工厂模式:NestFactory 就是蜜雪冰城
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
NestFactory.create(AppModule)------你跟 Nest 工厂说"我要一个应用",工厂就给你造一个。你不需要知道应用内部怎么实例化的,工厂负责生产,你负责消费。
三、模块化:每个 Module 就是一个"独立档口"
蜜雪冰城不是一个档口卖所有东西,而是分了冷饮档、冰品档、奶茶档。NestJS 也一样------每个业务一个 Module,互不干扰。
根模块:AppModule
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], // 控制器:校验、简单逻辑
providers: [AppService], // 服务:复杂业务、数据
})
export class AppModule {}
readme 对 @Module 的注释一针见血:
"复杂,说明书,照着做。"
@Module 就是一份"部门组织架构说明书"------谁管前台(controllers),谁管后厨(providers),需要对接哪些外部部门(imports),写清楚就行。
业务模块:TodosModule
todos.module.ts 是一个独立的业务部门:
typescript
import { Module } from '@nestjs/common';
import { TodosController } from './todos.controller';
import { TodosService } from './todos.service';
// 大型后端框架,MVC 视图层 不可以直接去数据库查数据
@Module({
controllers: [TodosController],
providers: [TodosService],
})
export class TodosModule {}
注释里有一句关键提醒:
"大型后端框架,MVC 视图层不可以直接去数据库查数据。"
这行注释道出了 NestJS 的铁律------Controller 不碰数据库,Service 不管路由。 各司其职,这就是 MVC 的核心。
四、装饰器模式:给类贴标签的艺术
NestJS 的代码里到处都是 @ 符号------@Module、@Controller、@Get、@Post、@Injectable。这些就是装饰器。
readme 的解释:
"装饰器模式在不修改原有对象的前提下,动态给对象叠加额外功能。"
通俗点说------你的类是一个员工,装饰器就是工牌上贴的标签。贴上 @Controller,它就是前台接单员;贴上 @Injectable,它就是后厨师傅;贴上 @Get,它就知道这个方法要响应 GET 请求。
NestJS 把装饰器用到了极致。看看 app.controller.ts:
typescript
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
console.log('/ 的控制器');
return this.appService.getHello();
}
}
@Controller() 告诉 NestJS:这个类是控制器。@Get() 告诉 NestJS:这个方法响应 GET 请求。不需要写路由配置文件,不需要注册路由表------标签一贴,路由生效。
五、MVC 分层:前台接单,后厨干活
NestJS 的核心架构就是经典 MVC,但拆得极其干净:
| 角色 | 文件 | 职责 | 蜜雪冰城类比 |
|---|---|---|---|
| Controller | xx.controller.ts |
接收请求、参数校验、简单逻辑 | 前台接单员 |
| Service | xx.service.ts |
业务逻辑、数据操作(CRUD) | 后厨师傅 |
| Module | xx.module.ts |
组装 Controller 和 Service | 店长 |
前台:Controller
Controller 的职责很明确------接单,然后交给后厨。
app.controller.ts 的 getHello() 方法就干了一件事:调用 Service。
typescript
@Get()
getHello(): string {
console.log('/ 的控制器');
// 响应什么内容?交给 service 层
return this.appService.getHello();
}
注释说得很直白:
"响应什么内容?交给 service 层。"
前台不做饭。前台只负责:接单 → 转交后厨 → 把成品端给顾客。
后厨:Service
app.service.ts 更简单:
typescript
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
// 给 controller 层一个交代的
getHello(): string {
return 'Hello World!';
}
}
注释里的"给 controller 层一个交代的"------后厨做完菜,给前台一个交代。分工明确,各管一摊。
六、依赖注入:员工自动到岗,不用你 new
传统开发里,Controller 需要用 Service 时得自己 import 然后 new 一个实例。NestJS 不需要。
看 app.controller.ts 的构造函数:
typescript
constructor(private readonly appService: AppService) {}
没有 new,没有 import 后手动实例化。你只要在构造函数里声明"我需要 AppService",NestJS 就自动帮你注入。
魔法在哪?在 @Injectable() 装饰器。
app.service.ts 头上贴了 @Injectable():
typescript
@Injectable()
export class AppService { ... }
这个标签告诉 NestJS:这个类可以被自动注入到任何需要它的地方。 Controller 需要它?注入。其他 Service 需要它?注入。不需要手动 new,不需要管生命周期------NestJS 全包了。
readme 的原话:
"@Injectable() 自动依赖注入,自动注入 controller 或任何用它的地方,controller 里的一个属性。"
七、CRUD 实战:Todos 的完整业务闭环
课堂用一个 Todos(待办事项)模块把 CRUD 跑了一遍。这是 NestJS 最经典的入门场景。
数据层:Todo 接口
todos.service.ts 定义了数据结构:
typescript
export interface Todo {
id: number;
title: string;
completed: boolean;
}
let todos: Todo[] = [
{ id: 1, title: '学习nestjs', completed: false },
{ id: 2, title: '学习 CRUD', completed: true },
];
两条初始数据,一明一暗:一个还没学完 NestJS,一个已经学完 CRUD。
Controller:五种 HTTP 方法的完整映射
todos.controller.ts 是今天最重磅的文件,把 RESTful API 的五种操作全用上了:
typescript
@Controller('todos')
export class TodosController {
constructor(private readonly todosService: TodosService) {}
// GET /todos ------ 查全部
@Get()
findAll(): Todo[] {
return this.todosService.findAll();
}
// GET /todos/:id ------ 查一条
@Get(':id')
findOne(@Param('id') id: string): Todo {
return this.todosService.findOne(Number(id));
}
// POST /todos ------ 新增
@Post()
create(@Body('title') title: string): Todo {
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} removed` };
}
// PUT /todos/:id ------ 修改
@Put(':id')
update(
@Param('id') id: string,
@Body() patch: Partial<Todo>,
): Todo {
return this.todosService.update(Number(id), patch);
}
}
逐个拆解:
| 装饰器 | HTTP 方法 | 路由 | 参数提取 | 操作 |
|---|---|---|---|---|
@Get() |
GET | /todos | 无 | 查全部 |
@Get(':id') |
GET | /todos/1 | @Param('id') |
查一条 |
@Post() |
POST | /todos | @Body('title') |
新增 |
@Delete(':id') |
DELETE | /todos/1 | @Param('id') |
删除 |
@Put(':id') |
PUT | /todos/1 | @Param + @Body |
修改 |
注意 @Put 的参数:@Body() patch: Partial<Todo>。Partial<Todo> 是 TypeScript 的工具类型------把 Todo 的所有字段变成可选的。你可以只传 { completed: true },也可以传 { title: '新标题', completed: false },灵活得很。
Service:后厨的五大菜谱
todos.service.ts 对应五个方法:
typescript
@Injectable()
export class TodosService {
findAll(): Todo[] {
return todos;
}
findOne(id: number): Todo {
const todo = todos.find(t => t.id === id);
if (!todo) throw new NotFoundException(`Todo ${id} not found`);
return todo;
}
create(title: string): Todo {
const todo: Todo = { id: nextId++, title, completed: false };
todos.push(todo);
return todo;
}
remove(id: number): void {
const index = todos.findIndex(t => t.id === id);
if (index === -1) throw new NotFoundException(`Todo ${id} not found`);
todos.splice(index, 1);
}
update(id: number, patch: Partial<Todo>): Todo {
const todo = this.findOne(id);
Object.assign(todo, patch);
return todo;
}
}
Controller 负责接单和参数校验,Service 负责干活。Controller 里一行业务逻辑都没有------这就是 readme 说的"controller 参数校验,简单逻辑,最后 return response"。
错误处理:NotFoundException
todos.service.ts 里有两处错误处理,都用的是 NestJS 内置的 NotFoundException:
typescript
if (!todo) throw new NotFoundException(`Todo ${id} not found`);
readme 特别强调了错误处理的重要性:
"nestjs 提供了各种错误类,标准化错误输出。statusCode 状态码,message 消息。"
不用自己拼 JSON 错误响应,NotFoundException 一抛,NestJS 自动返回标准的 404 响应,带状态码和消息。省心。
对比一下传统方式------try catch finally,TypeScript 独苗,一个线程挂了全崩。NestJS 的内置错误类把异常处理标准化了,不用每个接口都写 try-catch。
八、测试:NestJS 的出厂质检
NestJS 脚手架自带测试文件。app.controller.spec.ts:
typescript
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});
NestJS 用 Test.createTestingModule() 创建了一个测试专用的模块------相当于蜜雪冰城开业前的内部试吃。Controller 和 Service 都在测试模块里实例化好,直接调用验证结果。
expect(appController.getHello()).toBe('Hello World!')------简单粗暴,调一下看返回对不对。出厂质检,合格才能上柜。
九、一张图看懂 NestJS 的请求流程
一个 HTTP 请求从到达到返回,在 NestJS 里经历了什么:
scss
HTTP 请求
↓
NestFactory.create(AppModule) ← 工厂启动应用
↓
AppModule (根模块) ← 总调度
↓
TodosModule (业务模块) ← 路由匹配
↓
TodosController ← 前台接单:@Get / @Post / @Delete / @Put
↓ (自动依赖注入 TodosService)
TodosService ← 后厨干活:CRUD 业务逻辑
↓
NotFoundException (找不到时) ← 标准化错误
↓
HTTP 响应 (JSON)
工厂造应用 → 模块分业务 → 控制器接请求 → 服务干逻辑 → 错误类兜底。每一层职责单一,层层递进。
十、设计模式不是玄学
今天最深的感受是------NestJS 把设计模式用成了基础设施。
- 工厂模式 :
NestFactory.create()造应用,MixueFactory.create('ice')造冰激凌。调用者不需要知道内部细节,工厂负责生产。 - 装饰器模式 :
@Controller、@Get、@Injectable给类贴标签,不修改原有代码就叠加功能。 - 依赖注入 :
@Injectable()声明可注入,构造函数声明需求,NestJS 自动装配。 - 模块化 :
@Module把 Controller 和 Service 打包,业务隔离,按需加载。
readme 里说设计模式是"面向接口的编程,抽象的,23 种"。今天用了两种(工厂 + 装饰器),NestJS 里处处都是它们的影子。
设计模式不是面试时背的八股文,是工程化开发的肌肉记忆。
PS:前端有 Next.js,后端有 NestJS。一个是全栈瑞士军刀,一个是企业级蜜雪冰城。搭配使用,风味更佳。