在 Node.js 的后端开发生态中,Express 长期以来以其极简主义占据统治地位。然而,随着项目规模的扩大,缺乏约束的“自由”往往会导致代码结构混乱,也就是我们常说的“意大利面条式代码”。
为了解决这个问题,NestJS 应运而生。NestJS 是一个用于构建高效、可扩展且易于维护的企业级后端应用的框架。它基于 TypeScript 构建,深受 Angular 架构的影响,引入了模块化、依赖注入(DI)和装饰器等先进概念。
本文将结合一个包含待办事项(Todos)管理和 PostgreSQL 数据库连接的实战 Demo,带你深入理解 NestJS 的核心架构。
一、 为什么选择 NestJS?
在开始写代码之前,我们需要理解 NestJS 试图解决什么问题。
- 架构标准化:Express 让你自己决定文件放哪,而 NestJS 强制使用 模块(Module)、控制器(Controller)、服务(Service) 的分层架构。这被称为 MVC(模型-视图-控制器)设计模式。
- TypeScript优先:虽然 JS 也能写,但 NestJS 充分利用了 TS 的静态类型检查,让代码更健壮。
- 依赖注入(Dependency Injection) :这是 NestJS 的灵魂,实现了低耦合、高内聚的代码组织方式。
环境准备
启动一个 NestJS 项目非常简单:
npm i -g @nestjs/cli
nest new nest-test-demo
cd nest-test-demo
pnpm run start:dev
二、 核心架构解析:从入口到模块
1. 入口文件:Main.ts
任何 Node.js 应用都有一个入口。在 NestJS 中,这个文件通常是 main.ts。让我们看看代码:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { config } from 'dotenv';
config(); // 加载 .env 文件
async function bootstrap() {
// 工厂模式,创建应用实例
const app = await NestFactory.create(AppModule);
// 监听端口,优先使用环境变量,默认为 3000
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
这里体现了 NestJS 的工厂模式。NestFactory.create(AppModule) 就像一个复杂的机器启动按钮,它接收根模块 AppModule,并根据其中定义的规则组装整个应用。同时,代码中使用了空值合并运算符 ?? 来优雅地处理端口配置,并引入了 dotenv 来管理环境变量。
2. 这里的“大脑”:Root Module
AppModule 是整个应用的根节点。它负责把所有的“积木”拼装在一起。
@Module({
imports: [
TodosModule,
DatabaseModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule{}
通过 @Module 装饰器,我们清晰地看到了应用的结构:
- Imports:导入了
TodosModule(业务功能)和DatabaseModule(基础设施)。 - Controllers:注册了
AppController,用于处理根路径的路由。 - Providers:注册了
AppService,处理具体的业务逻辑。
这种结构保证了一个文件对应一个类,职责分明。
三、 业务开发实战:Todo 模块
接下来,我们通过 TodosModule 来演示标准的 NestJS 开发流程:Controller 接收请求 -> Service 处理逻辑。
1. 控制器(Controller):路由的守门员
Controller 的职责非常单一:接收 HTTP 请求,调用 Service,然后返回响应。它不应该包含复杂的业务逻辑。
查看 todos.controller.ts:
@Controller('todos')
export class TodosController {
// 依赖注入 Service
constructor(private readonly todosService: TodosService) {}
@Get()
getTodos() {
return this.todosService.findAll();
}
@Post()
addTodo(@Body('title') title: string) {
return this.todosService.addTodo(title);
}
@Delete(':id')
deleteTodo(@Param('id', ParseIntPipe) id: number) {
return this.todosService.deleteTodo(id);
}
}
关键技术点解析:
-
RESTful 语义:使用了
@Get(获取)、@Post(创建)、@Delete(删除),符合 RESTful API 设计规范。 -
参数解析:
@Body('title'):直接从 POST 请求体中提取title字段。@Param('id', ParseIntPipe):这是 NestJS 管道(Pipe)的强大之处。它自动截取 URL 中的id参数,并强制转换为整数。如果用户传了非数字(如 "abc"),NestJS 会自动抛出 400 错误,无需在业务逻辑中手动校验类型。
2. 服务(Service):业务逻辑的掌勺人
Service 包含实际的业务逻辑。在 todos.service.ts 中,虽然目前使用内存数组模拟数据库,但逻辑是清晰的:
@Injectable()
export class TodosService {
private todos: Todo[] = [
{ id: 1, title: '周五狂欢', completed: false },
{ id: 2, title: '疯狂星期四', completed: true }
]
addTodo(title: string) {
const todo: Todo = {
id: Date.now(),
title: title.trim(),
completed: false,
}
this.todos.push(todo);
return todo;
}
// ... deleteTodo 逻辑
}
这里使用了 @Injectable() 装饰器,这意味着 TodosService 可以被 NestJS 的 IoC(控制反转)容器管理,并注入到任何需要它的 Controller 中。
四、 进阶功能:数据库模块封装
在企业级应用中,数据库连接通常被封装为一个独立的模块。这在代码中的 database.module.ts 得到了完美体现。
1. 自定义 Provider 与数据库连接池
@Global() // 全局模块,一次导入,到处使用
@Module({
providers: [
{
provide: 'PG_CONNECTION',
// 使用 pg 库创建连接池
useValue: new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: parseInt(process.env.DB_PORT || '5432', 10),
})
}
],
exports: ['PG_CONNECTION'] // 导出连接,供其他模块注入
})
export class DatabaseModule {}
深度解析:
- @Global() :将此模块标记为全局。这意味着你不需要在每个 Feature Module(如 TodosModule)中重复导入
DatabaseModule,只需要在AppModule导入一次即可。 - 自定义 Provider:这里没有使用简写的类注入,而是定义了一个 token
'PG_CONNECTION'。 - useValue:直接使用
pg库的Pool实例作为值。这允许我们在 NestJS 中使用原生的 SQL 查询能力。 - 环境变量:配置信息全部来自
.env,避免了敏感信息硬编码。
2. 在 Controller 中使用数据库连接
在 app.controller.ts 中,演示了如何注入这个自定义的数据库连接:
export class AppController {
constructor(
// 使用 @Inject 装饰器通过 token 注入数据库连接
@Inject('PG_CONNECTION') private readonly db: any,
private readonly appService: AppService
) {}
@Get('db-test')
async testConnection() {
try {
// 直接执行 SQL 查询
const res = await this.db.query('select * from users');
return { status: '连接成功', data: res.rows }
} catch(error) {
return { status: '连接失败', error: error.message }
}
}
}
这段代码展示了 NestJS 极高的灵活性:既可以使用 ORM(如 TypeORM, Prisma),也可以通过自定义 Provider 直接操作原生数据库驱动。
五、 综合业务逻辑与手动校验
除了自动化的管道校验,代码中的 AppController 处理登录逻辑时展示了如何在业务层进行精细控制:
@Post('login')
login(@Body() body: { username: string, password: string }) {
const { username, password } = body;
// 手动参数校验
if(!username || !password) {
return { code: 400, message: '用户名或密码不能为空' }
}
if(password.length < 6) {
return { code: 400, message: '密码长度不能小于6位' }
}
// 调用 Service 处理登录
return this.appService.handleLogin(username, password);
}
配合 AppService 中的逻辑:
handleLogin(username: string, password: string) {
if(username === 'admin' && password === '123456') {
return { status: 200, message: '登录成功' }
} else {
return { status: 400, message: '用户名或密码错误' }
}
}
虽然这是一个模拟的登录(硬编码了 admin/123456),但它完整展示了 Controller 负责参数校验和响应格式化,Service 负责核心判断 的职责分离原则。
六、 总结与展望
通过对这几个文件的剖析,我们可以清晰地看到 NestJS 相比于纯 Express 的优势:
- 结构清晰:
Module把功能按块划分(Database, Todos, App),避免了代码堆砌。 - 关注点分离:Controller 只管 HTTP 交互,Service 只管业务,Global Module 只管基础设施。
- 类型安全:TypeScript 接口(如
Todointerface)贯穿始终,减少了运行时错误。 - 易于扩展:如果未来需要连接 Redis 或 MongoDB,只需按照
DatabaseModule的模式再写一个模块即可。