引言
在 Node.js 生态中,Express 虽灵活但易“烂尾”——随着项目膨胀,代码逐渐变成“意大利面条”。而 NestJS 的出现,正是为了解决这一痛点。
它基于 TypeScript 构建,融合了 Angular 的依赖注入与模块化思想,提供了一套企业级的开发范式:结构清晰、类型安全、易于维护和扩展。本文将带你:
✅ 从零初始化项目
✅ 深入理解核心三要素(Module/Controller/Service)
✅ 快速集成 PostgreSQL 实现持久化
✅ 封装数据库连接并保障安全性
✅ 掌握关键语法糖与工程化技巧
全程高能,无废话,助你快速写出“别人看了都说专业”的后端代码!
一、环境准备 & 初始化项目
确保已安装 Node.js(建议 v18+ LTS),然后全局安装 NestJS CLI:
npm install -g @nestjs/cli
创建项目:
nest new nestjs-pg-demo
选择 npm 或你喜欢的包管理器。CLI 自动生成标准结构:
src/
├── main.ts # 入口文件
├── app.module.ts # 根模块
├── app.controller.ts
└── app.service.ts
启动开发服务器:
npm run start:dev
访问 http://localhost:3000 看到 “Hello World” 表示成功!
💡 小知识:Nest 默认使用 Express 作为底层 HTTP 引擎,性能足够;若追求极致性能可切换为 Fastify(通过 @nestjs/platform-fastify)。
二、NestJS 三大核心概念详解(必懂!)
NestJS 遵循“关注点分离”,三大组件各司其职:
| 组件 | 职责 | 类比前端 |
|---|---|---|
| Module | 功能模块容器,组织代码单元 | Vue 中的模块划分 |
| Controller | 处理 HTTP 请求,定义路由 | 路由控制器 |
| Service | 封装业务逻辑,不处理请求 | Vuex Store / Service 层 |
✅ 使用 CLI 快速生成 Todo 模块
我们来做一个简单的 todos CRUD 功能:
nest g module todos # 生成模块
nest g controller todos --no-spec # 控制器(跳过测试)
nest g service todos --no-spec # 服务
自动生成的 todos.module.ts 如下:
// src/todos/todos.module.ts
import { Module } from '@nestjs/common';
import { TodosController } from './todos.controller';
import { TodosService } from './todos.service';
@Module({
controllers: [TodosController],
providers: [TodosService],
})
export class TodosModule {}
记得在 AppModule 中导入它:
// src/app.module.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],
})
export class AppModule {}
📌 重点理解:
@Module()装饰器是整个应用的“组装器”imports: 引入其他模块controllers: 当前模块暴露的路由providers: 可被注入的服务(支持 DI)
三、先做内存版 CRUD —— 快速验证流程
为了先跑通逻辑,我们先用数组模拟数据存储。
1. 定义接口 & 创建 Service
// src/todos/todos.service.ts
import { Injectable } from '@nestjs/common';
export interface Todo {
id: number;
title: string;
completed: boolean;
}
@Injectable()
export class TodosService {
private todos: Todo[] = [{ id: 1, title: '学习 NestJS', completed: false }];
private idCounter = 2;
findAll(): Todo[] {
return this.todos;
}
create(title: string): Todo {
const todo = { id: this.idCounter++, title, completed: false };
this.todos.push(todo);
return todo;
}
delete(id: number): boolean {
const index = this.todos.findIndex(t => t.id === id);
if (index === -1) return false;
this.todos.splice(index, 1);
return true;
}
}
📌 @Injectable() 是什么?
它标记这个类可以被 Nest 的依赖注入系统管理。即使没显式写
@Injectable(),只要被模块引用也能工作,但加上更规范,尤其当你需要用到注入其他服务时。
2. 编写 Controller 接收请求
// src/todos/todos.controller.ts
import { Controller, Get, Post, Body, Delete, Param, ParseIntPipe } from '@nestjs/common';
import { TodosService, Todo } from './todos.service';
@Controller('todos')
export class TodosController {
constructor(private readonly todosService: TodosService) {}
@Get()
getAll(): Todo[] {
return this.todosService.findAll();
}
@Post()
add(@Body('title') title: string): Todo {
if (!title?.trim()) throw new Error('标题不能为空');
return this.todosService.create(title);
}
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return { success: this.todosService.delete(id) };
}
}
🎯 关键语法解析:
| 装饰器 | 作用 |
|---|---|
@Controller('todos') | 定义基础路径 /todos |
@Get() → GET /todos | 获取列表 |
@Post() → POST /todos | 添加任务 |
@Body('title') | 提取 JSON 请求体中的字段 |
@Param('id', ParseIntPipe) | 自动将 URL 参数转成整数,失败抛错 |
🧠 ParseIntPipe 干了啥?
原生 Node 中
req.params.id是字符串'1',你需要手动parseInt。而ParseIntPipe会自动转换,并在校验失败时返回 400 错误,提升健壮性!
四、接入 PostgreSQL:告别内存,走向生产
现在我们将数据持久化到 PostgreSQL。
1. 安装依赖
npm install pg dotenv
npm install -D @types/pg
2. 配置环境变量 .env
DB_HOST=localhost
DB_PORT=5432
DB_USER=your_user
DB_PASSWORD=your_password
DB_NAME=nest_demo
PORT=3000
在 main.ts 开头加载配置:
// src/main.ts
import { config } from 'dotenv';
config(); // 加载 .env 文件
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT || 3000);
}
bootstrap();
3. 封装 DatabaseModule(推荐做法)
我们要把数据库连接做成一个全局共享的连接池,避免重复创建。
// src/database/database.module.ts
import { Global, Module } from '@nestjs/common';
import { Pool } from 'pg';
@Global() // 🌍 标记为全局模块,其他模块无需再 import
@Module({
providers: [
{
provide: 'PG_CONNECTION', // 自定义令牌名
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, 10),
}),
},
],
exports: ['PG_CONNECTION'], // ⚠️ 让外部能拿到这个连接
})
export class DatabaseModule {}
📌 为什么用 @Global()?
如果你不加,每个需要数据库的模块都得手动导入
DatabaseModule,很麻烦。加上后只需一次注册,处处可用。
别忘了在 AppModule 中引入一次:
// src/app.module.ts
import { DatabaseModule } from './database/database.module';
@Module({
imports: [DatabaseModule, TodosModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
4. 改造 TodosService 使用 PG 查询
先在数据库中建表:
CREATE TABLE todos (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
completed BOOLEAN DEFAULT false
);
然后改造服务层:
// src/todos/todos.service.ts
import { Injectable, Inject } from '@nestjs/common';
@Injectable()
export class TodosService {
constructor(@Inject('PG_CONNECTION') private readonly db: Pool) {}
async findAll() {
const res = await this.db.query('SELECT * FROM todos ORDER BY id');
return res.rows; // 返回结果数组
}
async create(title: string) {
const res = await this.db.query(
'INSERT INTO todos (title) VALUES ($1) RETURNING *',
[title]
);
return res.rows[0];
}
async delete(id: number) {
const res = await this.db.query('DELETE FROM todos WHERE id = $1', [id]);
return res.rowCount > 0; // 是否有行被删除
}
}
🔑 重点来了:SQL 注入防护!
❌ 错误写法(拼接 SQL):
db.query(`DELETE FROM todos WHERE id = ${id}`); // 危险!可能被注入
✅ 正确做法(参数化查询):
db.query('DELETE FROM todos WHERE id = $1', [id]); // 安全 ✔️
$1,$2是 PostgreSQL 占位符,配合参数数组使用,从根本上防止 SQL 注入攻击。
5. 添加数据库健康检查接口
方便调试连接是否正常:
// src/app.controller.ts
import { Controller, Get, Inject } from '@nestjs/common';
@Controller()
export class AppController {
constructor(@Inject('PG_CONNECTION') private readonly db: any) {}
@Get('db-test')
async testDb() {
try {
await this.db.query('SELECT 1');
return { status: '✅ 数据库连接成功!' };
} catch (err) {
return { status: '❌ 连接失败', error: err.message };
}
}
}
访问 /db-test 即可看到连接状态。
五、总结 & 下一步进阶建议(冲榜加分项!)
你已经完成了以下关键能力构建:
✅ 搭建 NestJS 工程骨架
✅ 理解模块化架构设计
✅ 实现依赖注入与全局模块封装
✅ 安全地操作 PostgreSQL 数据库
✅ 掌握装饰器、管道、参数化查询等核心语法
🚀 掘金读者关心的“下一步怎么做”?
| 目标 | 推荐方案 |
|---|---|
| 更高效写数据库 | 使用 TypeORM 或 Prisma,告别手写 SQL |
| 请求校验自动化 | 引入 class-validator + ValidationPipe,自动拦截非法输入 |
| 统一错误响应 | 使用 @UseFilters() + HttpExceptionFilter 全局捕获异常 |
| 自动生成文档 | 集成 @nestjs/swagger,一键生成 API 文档页面 |
| 提升开发体验 | 使用 DTO 分离数据传输对象,增强类型提示 |
💬 最后说一句真心话
NestJS 不只是一个框架,更是一种编程思维的升级。
当你开始用“模块”组织功能、“服务”封装逻辑、“注入”管理依赖时,你就已经脱离了“脚本小子”的阶段,迈向真正的工程化开发。