从零上手 NestJS + PostgreSQL:打造类型安全、模块化的企业级后端服务

0 阅读6分钟

引言

在 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 数据库
✅ 掌握装饰器、管道、参数化查询等核心语法


🚀 掘金读者关心的“下一步怎么做”?

目标推荐方案
更高效写数据库使用 TypeORMPrisma,告别手写 SQL
请求校验自动化引入 class-validator + ValidationPipe,自动拦截非法输入
统一错误响应使用 @UseFilters() + HttpExceptionFilter 全局捕获异常
自动生成文档集成 @nestjs/swagger,一键生成 API 文档页面
提升开发体验使用 DTO 分离数据传输对象,增强类型提示

💬 最后说一句真心话

NestJS 不只是一个框架,更是一种编程思维的升级。

当你开始用“模块”组织功能、“服务”封装逻辑、“注入”管理依赖时,你就已经脱离了“脚本小子”的阶段,迈向真正的工程化开发