一、NestJS 是什么
NestJS 是目前最流行的 Node.js 大型后端框架之一。它的核心思想:用面向对象 + 装饰器,把后端代码组织得规整、可扩展。
为什么需要它?看一个对比:
- 原生 Node/Express 写后端:一个文件几千行,路由、校验、业务、数据库操作全混在一起,项目一大人就懵;
- NestJS 写后端:强制分层------模块(Module)、控制器(Controller)、服务(Service)各司其职,像搭积木一样清晰。
本质上,NestJS 是经典的 MVC 模式:
- M(Model):数据模型,数据库抽象
- C(Controller):控制器,接收前端请求
- V(View):视图层,返回给前端的响应
前后端分离时代,V 交给了前端,后端专注 M 和 C,中间由 Service 承担业务逻辑。
二、项目骨架:从 main.ts 开始
先看入口文件 main.ts:
ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
// 工厂模式:实例化一个 Nest 后端应用
const app = await NestFactory.create(AppModule);
// 启动 Web 服务,默认 3000 端口
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
要点:
- 工厂模式 :
NestFactory.create(AppModule)不是new一个对象,而是由"工厂"根据模块配置帮你组装出整个应用------你不需要关心内部细节; - 按需加载 :NestJS 是模块化架构,
AppModule是根模块,应用启动时由它开始装配所有功能; - 用 JS 类比:
NestFactory.create就像一个"App 组装车间",你给它一张说明书(Module),它按说明书把控制器、服务、路由全部接好线。
三、Module:整个应用的"说明书"
app.module.ts:
ts
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], // 服务:复杂业务 CRUD
})
export class AppModule {}
Module 是 NestJS 的最小组织单元,它声明了三类东西:
| 配置项 | 含义 | JS 类比 |
|---|---|---|
imports |
引入其他模块 | 引入子模块组件 |
controllers |
路由入口,校验、简短逻辑 | 前台接待员 |
providers |
可注入的服务,复杂业务 | 后台干活的员工 |
理解模块化的价值:一个大型项目 = 很多个 Module 拼起来。比如你的项目有 TodosModule(待办)、UserModule(用户)、OrderModule(订单),每个模块独立开发、独立维护,最后在根模块里组装。
四、Controller:请求的"前台接待"
Todos.controller.ts(省略装饰器 import 部分):
ts
@Controller('todos') // 路由前缀:所有 /todos 开头的请求
export class TodosController {
constructor(private readonly todosService: TodosService) {}
@Get() // GET /todos
findAll(): Todo[] {
return this.todosService.findAll();
}
@Get(':id') // GET /todos/1
findOne(@Param('id') id: string): Todo {
return this.todosService.findOne(Number(id));
}
@Post() // POST /todos
create(@Body('title') title: string): Todo {
return this.todosService.create(title);
}
@Put(':id') // PUT /todos/1(更新)
update(@Param('id') id: string, @Body() patch: Partial<Todo>): Todo {
return this.todosService.update(Number(id), patch);
}
@Delete(':id') // DELETE /todos/1
remove(@Param('id') id: string): { message: string } {
this.todosService.remove(Number(id));
return { message: '删除成功' };
}
}
Controller 的关键认知:
- 装饰器即路由 :
@Controller('todos')定前缀,@Get/@Post/@Put/@Delete定方法------不用手写app.get('/todos', ...)了; - 参数装饰器 :
@Param('id')取路径参数,@Body()取请求体------类似 Express 的req.params.id、req.body,但更声明式; - Controller 不干重活:它只做"接请求、传参数、返回结果",真正的业务逻辑委托给 Service。
用 JS 类比装饰器:装饰器就像给函数"贴标签"。
@Get('todos')相当于在普通函数上贴了一张"我是 GET /todos 的处理函数"的标签,框架看到标签就把路由绑定好。这比手写router.get()更直观、更模块化。
五、Service:业务逻辑的"员工"
Todos.service.ts:
ts
@Injectable() // 可被依赖注入
export class TodosService {
// 模拟内存数据库
private todos: Todo[] = [
{ id: 1, weight: 1, completed: false },
{ id: 2, weight: 2, completed: false },
];
findAll(): Todo[] {
return this.todos;
}
findOne(id: number): Todo {
const todo = this.todos.find((t) => t.id === id);
// 容错:找不到就抛异常,而不是返回 undefined
if (!todo) throw new NotFoundException(`todo ${id} not found`);
return todo;
}
create(title: string): Todo {
const todo: Todo = { id: this.nextId(), weight: 1, completed: false };
this.todos.push(todo);
return todo;
}
remove(id: number): void {
const index = this.todos.findIndex((t) => t.id === id);
if (index === -1) throw new NotFoundException(`todo ${id} 不存在`);
this.todos.splice(index, 1);
}
update(id: number, patch: Partial<Todo>): Todo {
const todo = this.findOne(id);
Object.assign(todo, patch); // 把更新字段合并进原对象
return todo;
}
}
Service 的职责:处理真正的业务(查数据、改数据、校验存在性),并对 Controller 负责。三个细节值得学习:
@Injectable()装饰器:标记这个类可以被 NestJS 的依赖注入(DI)容器自动创建并注入到需要它的地方;NotFoundException:业务严谨稳定靠的是容错模块------找不到资源时抛明确异常,而不是返回 undefined 让上层摸不着头脑;Partial<Todo>:TS 内置工具类型,表示"Todo 的部分字段",更新时只传需要改的字段。用 JS 类比就是:{...todo, ...patch}合并对象。
六、依赖注入(DI):Controller 怎么拿到 Service
你可能好奇:TodosController 里根本没有 new TodosService(),它为什么能用?
ts
constructor(private readonly todosService: TodosService) {}
这是 依赖注入(Dependency Injection) :NestJS 的容器看到 Controller 的构造函数需要 TodosService,就会自动创建实例并"塞"进来。
用 JS 类比:
js
// 不使用 DI:手动创建依赖(耦合)
class TodosController {
constructor() {
this.todosService = new TodosService(); // 自己 new,换实现要改代码
}
}
// 使用 DI:声明"我需要什么",框架给什么(解耦)
class TodosController {
constructor(todosService) {
this.todosService = todosService; // 谁传的?NestJS 容器
}
}
DI 的好处:
- 解耦:Controller 不关心 Service 怎么创建,只关心它有什么能力;
- 易测试:测试时传一个 mock 的 service 进去即可(后面测试小节会看到);
- 易替换:换数据库实现,只改 Service,Controller 一行不动。
七、数据流:一次请求的完整旅程
把整个 Todos 模块串起来,一次 GET /todos 的旅程:
前端只跟 Controller 打交道,Controller 只跟 Service 打交道,Service 管数据。 这就是分层架构的威力------每一层职责单一,改动互不牵连。
八、测试:NestJS 内置的测试体系
app.controller.spec.ts:
ts
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!');
});
});
});
测试的要点:
Test.createTestingModule:不用启动整个服务器,只组装被测的 Controller + Service,跑得快;app.get<AppController>(AppController):从测试容器里取出被测对象;describe/it/expect:Jest 测试框架的三大件,描述测试分组、用例、断言。
这就是 DI 带来的测试红利:想测 Controller,就只给它配好 Service,其余一律不加载。
九、完整项目结构回顾
把上传的 8 个文件归位,就是 NestJS 的标准项目骨架:
| 文件 | 角色 | 一句话职责 |
|---|---|---|
main.ts |
入口 | 工厂模式创建应用并启动 |
app.module.ts |
根模块 | 组装所有模块/控制器/服务 |
app.controller.ts |
根控制器 | 处理 / 请求 |
app.service.ts |
根服务 | 返回首页文案 |
Todos.module.ts |
功能模块 | 声明 Todos 的控制器和服务 |
Todos.controller.ts |
功能控制器 | /todos 的 CRUD 路由 |
Todos.service.ts |
功能服务 | 内存数据 + CRUD 业务 |
app.controller.spec.ts |
测试 | 验证 Controller 行为 |
十、结语
NestJS 学起来看似装饰器满天飞,但底层逻辑非常朴素:
- 模块化:Module 组织一切,大项目 = 模块拼装;
- 分层:Controller 接请求 → Service 做业务,各司其职;
- 装饰器:用声明式语法替代手写路由;
- 依赖注入:框架自动组装依赖,解耦且易测。
如果你熟悉 Express,会惊讶于"同样的功能,代码怎么突然这么规整";这正是 NestJS 的价值------用结构化的框架约束,让后端项目从"能跑"走向"能长期维护"。