《从零搭建NestJS项目》

0 阅读9分钟

从零搭建NestJS项目(附完整代码):连接PostgreSQL+实现基础接口

作为一名后端开发者,在接触过Express、Koa等轻量框架后,第一次用NestJS就被它的模块化架构和依赖注入设计惊艳到了。它基于TypeScript,完美契合企业级开发的“高效、可扩展、易维护”需求,让后端代码不再杂乱无章。

今天就带大家从零搭建一个完整的NestJS基础项目,包含PostgreSQL数据库连接、用户登录、Todo增删查、数据库测试等核心功能,所有代码可直接复制使用,新手也能快速上手~

一、先搞懂:NestJS到底是什么?

很多人会把NestJS和Express对比,其实两者不是一个维度的东西——NestJS是基于Express(或Fastify)封装的企业级框架,它解决了Express缺乏规范、项目越大越难维护的痛点。

核心优势亮点:

  • 基于TypeScript开发,强类型约束,减少运行时错误,代码更规范
  • 采用模块化(Module)架构,拆分Controller(路由)、Service(业务),职责清晰
  • 内置依赖注入(DI),解耦代码,便于测试和维护
  • 支持RESTful API设计,语义化HTTP请求,契合现代后端开发规范

简单类比:如果说Express是“毛坯房”,可以自由装修但需要自己定规则;那NestJS就是“精装房”,自带成熟的设计规范,既能快速入住,也能按需改造。

二、环境准备与项目初始化

1. 安装NestJS脚手架

首先全局安装NestJS CLI,用于快速创建项目和生成各类文件:

npm i -g @nestjs/cli

2. 新建NestJS项目

# 新建项目(项目名可自定义,这里用nest-test-demo)
nest new nest-test-demo

# 进入项目目录
cd nest-test-demo

# 启动开发服务(默认端口3000)
npm run start:dev

启动成功后,访问 http://localhost:3000,能看到NestJS默认的欢迎页面,说明项目初始化成功。

3. 安装所需依赖

本项目需要用到PostgreSQL数据库、环境变量配置等依赖,执行以下命令安装:

# 安装PostgreSQL驱动和连接池
npm install pg

# 安装环境变量配置依赖
npm install dotenv

三、项目核心结构解析(附完整代码)

NestJS的核心是“模块化”,一个标准的Nest项目结构如下(我们只保留核心文件,简化冗余内容):

nest-test-demo/
├── src/
│   ├── database/          # 数据库配置模块
│   │   └── database.module.ts
│   ├── todos/             # Todo功能模块(增删查)
│   │   ├── todos.controller.ts
│   │   ├── todos.service.ts
│   │   └── todos.module.ts
│   ├── app.controller.ts  # 主控制器(路由入口)
│   ├── app.service.ts     # 主服务(业务逻辑)
│   ├── app.module.ts      # 根模块(整合所有子模块)
│   └── main.ts            # 入口文件(启动服务)
└── .env                   # 环境变量配置

1. 入口文件:main.ts

项目的启动入口,负责创建Nest应用实例、加载环境变量、监听端口。

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { config } from 'dotenv';

// 加载.env环境变量
config();

async function bootstrap() {
  // 工厂模式创建Nest应用实例(核心:加载根模块AppModule)
  const app = await NestFactory.create(AppModule);
  
  // 打印端口(从环境变量读取,默认3000)
  console.log(process.env.PORT, '//////')
  
  // 监听端口(空值合并运算符:环境变量不存在则用3000)
  await app.listen(process.env.PORT ?? 3000);
}

// 启动应用
bootstrap();

2. 环境变量配置:.env

集中管理端口、数据库连接信息,避免硬编码,便于后期维护和部署。

# 服务器端口
PORT=1234

# PostgreSQL数据库配置
DB_USER=postgres          # 数据库用户名(默认postgres)
DB_HOST=localhost         # 数据库地址(本地默认localhost)
DB_NAME=XUEBI             # 数据库名称(需提前创建)
DB_PASSWORD=******      # 数据库密码(自己设置的密码)
DB_PORT=5432              # PostgreSQL默认端口

注意:不要写错配置项(比如之前踩坑的DB=PASSWORD,多写一个等号会导致密码读取失败),配置项要和代码中读取的变量名完全一致。

3. 数据库模块:database.module.ts

全局数据库配置模块,使用pg的Pool创建数据库连接池,通过依赖注入供其他模块使用。

import { Module, Global } from '@nestjs/common';
import { Pool } from 'pg';  // 引入PostgreSQL连接池
import * as dotenv from 'dotenv';

// 加载环境变量
dotenv.config();

// @Global() 装饰器:让该模块成为全局模块,其他模块无需import即可使用
@Global()
@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 || '5432', 10), // 端口转数字
        ssl: false,              // 本地连接关闭SSL(生产环境可开启)
        client_encoding: 'UTF8'  // 解决中文乱码问题
      })
    }
  ],
  exports: ['PG_CONNECTION']  // 导出连接,供其他模块使用
})
export class DatabaseModule {}

4. 主服务:app.service.ts

处理核心业务逻辑(比如登录验证、欢迎语),被Controller依赖注入,职责是“做什么”。

import { Injectable } from '@nestjs/common'

// @Injectable() 装饰器:标记该类为可注入的服务,供Controller使用
@Injectable()  
export class AppService {
  // 测试接口:返回简单字符串
  getHello(): string {
    return '你好yeah!!!'
  }

  // 欢迎接口:返回欢迎语
  getWelcome(): string {
    return "欢迎来到nest测试项目"
  }

  // 登录业务逻辑:简单的用户名密码校验(实际开发需加密)
  handleLogin(username: string, password: string) {
    // 模拟管理员账号密码(实际开发需从数据库查询)
    if (username === "admin" && password === "123456") {
      return {
        status: 200,
        message: "登录成功"
      }
    } else {
      return {
        status: 400,
        message: "登录失败"
      }
    }
  }
}

5. 主控制器:app.controller.ts

处理HTTP请求,定义路由,接收前端参数,调用Service处理业务,职责是“接收请求、返回响应”。

import { Controller, Get, Post, Body, Inject } from '@nestjs/common'
import { AppService } from './app.service'

// @Controller() 装饰器:标记该类为控制器,处理根路径相关请求
@Controller()
export class AppController {
  // 依赖注入:注入数据库连接和AppService
  constructor(
    @Inject('PG_CONNECTION') private readonly db: any, // 注入PostgreSQL连接
    private readonly appService: AppService           // 注入AppService
  ) {}

  // GET请求:根路径(http://localhost:1234)
  @Get()
  getHello(): string {
    return this.appService.getHello()
  }

  // GET请求:/welcome(http://localhost:1234/welcome)
  @Get('welcome')
  getWelcome(): string {
    return this.appService.getWelcome()
  }

  // POST请求:/login(http://localhost:1234/login),处理登录
  @Post('login')
  login(@Body() body: { username: string, password: string }) {
    const { username, password } = body;
    console.log(username, password); // 打印前端传递的参数
    
    // 参数校验(非空、密码长度)
    if (!username || !password) {
      return { code: 400, message: "用户名或密码不能为空" }
    }
    if (password.length< 6) {
      return { code: 400, message: "密码不能少于6位" }
    }
    
    // 调用Service的登录方法处理业务
    return this.appService.handleLogin(username, password)
  }

  // GET请求:/db-test(http://localhost:1234/db-test),测试数据库连接
  @Get('db-test')
  async testConnection() {
    try {
      // 查询users表(需提前创建)
      const res = await this.db.query('SELECT * from users');
      return {
        status: '连接成功',
        data: res.rows
      }
    } catch(error) {
      return {
        status: '连接失败',
        error: error.message
      }
    }
  }
}

6. Todo模块:实现基础增删查

Todo模块是一个独立的功能模块,包含Controller(路由)、Service(业务)、Module(模块),体现NestJS的模块化思想。

(1)Todo服务:todos.service.ts
import { Injectable } from '@nestjs/common'

// 定义Todo接口,强类型约束
export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

@Injectable()
export class TodosService {
  // 模拟数据库(实际开发需操作PostgreSQL)
  private todos: Todo[] = [
    { id: 1, title: '周五狂欢', completed: false },
    { id: 2, title: '三角洲首胜', completed: true }
  ]

  // 查询所有Todo
  findAll() {
    return this.todos
  }

  // 添加Todo
  addTodo(title: string) {
    const todo: Todo = {
      id: +Date.now(), // 用时间戳作为临时ID
      title,
      completed: false
    }
    this.todos.push(todo);
    return todo;
  }

  // 删除Todo
  deleteTodo(id: number) {
    this.todos = this.todos.filter(todo => todo.id !== id);
    return { message: 'Todo deleted', code: 200 }
  }
}
(2)Todo控制器:todos.controller.ts
import {
  Controller,
  Get,
  Post,
  Body,
  Delete,
  Param,
  ParseIntPipe // 解析参数为整数(避免字符串ID)
} from '@nestjs/common';
import { TodosService } from './todos.service';

// @Controller('todos'):所有路由前缀为 /todos
@Controller('todos')
export class TodosController{
  constructor(private readonly todosService: TodosService){}

  // GET /todos:查询所有Todo
  @Get()
  getTodos() {
    return this.todosService.findAll();
  }

  // POST /todos:添加Todo(接收前端传递的title参数)
  @Post()
  addTodo(@Body('title') title:string) {
    return this.todosService.addTodo(title);
  }

  // DELETE /todos/:id:删除指定ID的Todo
  @Delete(':id')
  deleteTodo(@Param('id', ParseIntPipe) id: number) {
    console.log(typeof id, '//////'); // 验证ID是否为数字
    return this.todosService.deleteTodo(id);
  }
}
(3)Todo模块:todos.module.ts
import { Module } from '@nestjs/common';
import { TodosController } from './todos.controller'
import { TodosService } from './todos.service'

//  Todo模块:整合该功能的Controller和Service
@Module({
  controllers: [TodosController], // 注册该模块的控制器
  providers: [TodosService]       // 注册该模块的服务
})
export class TodosModule{}

7. 根模块:app.module.ts

NestJS的根模块,负责整合所有子模块(DatabaseModule、TodosModule),是整个项目的“入口模块”。

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TodosModule } from './todos/todos.module';
import { DatabaseModule } from './database/database.module';

// @Module() 装饰器:标记该类为根模块
@Module({
  imports: [
    TodosModule,    // 引入Todo模块
    DatabaseModule  // 引入数据库模块(全局模块也可引入,更清晰)
  ],
  controllers: [AppController], // 注册主控制器
  providers: [AppService],      // 注册主服务
})
export class AppModule{}

四、常见踩坑点(必看)

结合我自己搭建过程中遇到的问题,整理了4个高频踩坑点,帮大家避坑:

1. 数据库连接失败:SASL认证错误

错误提示:{"status":"连接失败","error":"SASL: SCRAM-SERVER-FIRST-MESSAGE: client password must be a string"}

原因:.env文件中密码配置项写错(比如多写等号:DB=PASSWORD=******),导致读取到的密码是undefined(非字符串)。

解决:将.env中的DB=PASSWORD=******改为DB_PASSWORD=******

2. 数据库不存在:关系“XUEBI”不存在

错误提示:{"status":"连接失败","error":"���ݿ� "XUEBI" ������"}(中文乱码,实际是“数据库XUEBI不存在”)。

原因:.env中指定的DB_NAME=XUEBI,但PostgreSQL中没有创建该数据库。

解决:登录PostgreSQL,执行CREATE DATABASE "XUEBI";创建数据库。

3. 表不存在:关系“users”不存在

错误提示:{"status":"连接失败","error":"关系 "users" 不存在"}

原因:db-test接口执行了SELECT * from users,但数据库中没有创建users表。

解决:登录XUEBI数据库,创建users表(示例SQL):

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  username VARCHAR(50) NOT NULL,
  password VARCHAR(100) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

4. psql命令找不到:bash: psql: command not found

原因:Linux系统中PostgreSQL的bin目录未添加到环境变量PATH中。

解决:将PostgreSQL的bin目录(如/usr/lib/postgresql/16/bin)添加到环境变量,永久生效:

# 编辑环境变量配置文件
nano ~/.bashrc

# 添加以下内容(替换为自己的路径)
export PATH=$PATH:/usr/lib/postgresql/16/bin

# 生效配置
source ~/.bashrc

五、接口测试(Postman)

项目启动后(npm run start:dev),用Postman测试所有接口,确保功能正常:

  1. GET http://localhost:1234 → 返回“你好yeah!!!”
  2. GET http://localhost:1234/welcome → 返回“欢迎来到nest测试项目”
  3. POST http://localhost:1234/login(Body传{"username":"admin","password":"123456"})→ 返回登录成功
  4. GET http://localhost:1234/db-test → 返回数据库连接成功和users表数据
  5. GET http://localhost:1234/todos → 返回所有Todo
  6. POST http://localhost:1234/todos(Body传{"title":"学习NestJS"})→ 添加新Todo
  7. DELETE http://localhost:1234/todos/1 → 删除ID为1的Todo

六、总结与后续扩展

通过这个项目,我们快速掌握了NestJS的核心用法:

  • 模块化架构:拆分Module、Controller、Service,职责清晰
  • 依赖注入:通过@Inject、@Injectable实现代码解耦
  • 数据库连接:使用pg连接PostgreSQL,配置全局数据库模块
  • RESTful API:实现GET、POST、DELETE等语义化请求

后续可以基于这个基础项目扩展更多功能:

  • 用户密码加密(使用bcrypt)
  • 添加JWT身份验证
  • 完善Todo的修改、分页查询功能
  • 添加异常过滤器,统一错误响应

NestJS的学习曲线虽然比Express稍陡,但一旦掌握,就能极大提升后端开发效率,尤其适合中大型项目。本文所有代码均可直接复制使用,新手可以跟着步骤一步步搭建,遇到问题可以参考踩坑点解决