React + NestJS购物平台练习【2】后端项目框架搭建
搭建基础的 NestJS 项目框架,包括以下内容:
- 初始化 NestJS 项目
- 配置环境变量
- 配置数据库连接
- 配置 Swagger 文档
- 设置基础中间件
- 配置日志系统
1. 初始化 NestJS 项目
1.1. 安装 NestJS CLI 工具
全局安装 NestJS 的 CLI 工具:
1 | yarn global add @nestjs/cli |
确保全局安装的包路径已被添加到环境变量 PATH
中,否则无法在终端中使用 nest
命令。
可以使用以下命令查看 Yarn 的全局包路径:
1 | yarn global bin |
将输出的路径添加到 PATH
中。
1.2. 创建新的 NestJS 项目
执行以下命令,创建一个名为 shopping-nest-server
的新项目:
1 | nest new shopping-nest-server |
运行后,NestJS CLI 会提示选择包管理工具,选择 Yarn
,等待其自动安装所需的依赖。
项目生成后,NestJS CLI 默认会在根目录下生成一个 test
目录和一些单元测试文件(.spec.ts
)。我暂时不需要测试,所以删去了这些内容。
NestJS CLI 创建的 NestJS 项目会自带 ESLint 和 Prettier,我们只需要将上一章配置好的 .prettierrc
复制过来、确保前端后端都遵循相同的代码规范即可:
1 | { |
2. 配置环境变量和配置文件
在实际配置前,我们先来考虑一下,什么是需要配置的:
-
应用基本配置
APP_PORT
:定义应用监听的端口号APP_ENV
:表示当前的应用环境。应用将根据环境做出一些环境特定的设置,比方说日志的详细级别
-
核心数据库配置
数据库是应用的核心部分,用于存储应用的持久化数据。
DB_TYPE
:数据库的类型。我选择使用mysql
或者postgres
DB_HOST
、DB_PORT
、DB_USER
、DB_PASSWORD
、DB_NAME
:这些参数确保应用能连接到正确的数据库实例。不同环境中的数据库配置往往不尽相同,开发环境中可能连接到本地数据库、生产环境中可能连接到远程数据库
-
缓存配置
缓存系统是提升应用性能的重要手段。
Redis 是一个高效的内存缓存数据库,常用于缓存频繁访问的数据,从而减轻数据库负载、提升响应速度。
REDIS_HOST
、REDIS_PORT
:让应用访问正确的 Redis 实例REDIS_PASSWORD
部分 Redis 服务需要密码验证,通过密码可以保障缓存数据的安全REDIS_DB
:指定 Redis 数据库编号,有助于在同一 Redis 实例中隔离不同用途的数据
-
Elasticsearch 配置
Elasticsearch 是一个分布式搜索和分析引擎,适用于海量数据的全文检索和分析。
ELASTICSEARCH_HOST
、ELASTICSEARCH_PORT
:定义了 Elasticsearch 服务的位置和端口ELASTICSEARCH_USERNAME
、ELASTICSEARCH_PASSWORD
:通过用户名和密码的方式实现对 Elasticsearch 集群的访问控制ELASTICSEARCH_INDEX
:定义应用使用的索引。索引类似于数据库中的表,是 Elasticsearch 存储和查询数据的基本单位
-
文件存储配置
对于需要上传文件(如用户头像、商品图片等)的应用来说,文件存储服务是不可或缺的。
很多应用会选择云存储服务(如 Amazon S3、Aliyun OSS)或者本地可用的 MinIO 来满足存储需求。
STORAGE_ENDPOINT
:指定文件存储服务的 API 端点,便于与远程存储服务连接STORAGE_ACCESS_KEY
和STORAGE_SECRET_KEY
:用于认证的密钥对,保障文件存储服务的安全访问STORAGE_BUCKET
、STORAGE_REGION
:定义存储的目标存储桶和区域位置,以便更合理地管理文件资源,减少延迟STORAGE_USE_SSL
:配置是否启用 HTTPS,以增强数据传输的安全性
-
JWT 配置
JWT 用于实现用户身份验证和会话管理,是一种轻量的认证方式。
在过去的 React + NestJS + SocketIO 教程文章中,我们已经讲过了 JWT,感兴趣的可以看看。
JWT_SECRET
:用于签发和验证 JWT 的密钥,确保身份认证的安全性。设计一个足够强度的密钥并保持其私密性至关重要JWT_TOKEN_AUDIENCE
:指定 JWT 的受众,即令牌面向的服务或应用。设置受众可以帮助确保令牌仅被指定应用使用,提高认证的安全性JWT_TOKEN_ISSUER
:用于声明 JWT 的发布者,一般设置为认证服务器的标识,确保 JWT 的来源是可信的JWT_ACCESS_TOKEN_TTL
:JWT 访问令牌的有效时间,单位为秒。合理设置过期时间既能提升安全性(防止会话过长导致会话劫持风险),又可避免用户频繁重新登录带来的不便
2.1. 安装必要依赖
首先安装 @nestjs/config
、dotenv
以及数据库驱动和 TypeORM,以支持加载环境变量、进行数据库连接和配置。
1 | yarn add @nestjs/config |
@nestjs/config
是 NestJS 提供的官方配置模块,专为加载、管理和验证环境变量而设计dotenv
是配置模块的底层依赖,通过.env
文件加载环境变量- TypeORM 是一个 TypeScript 支持的 ORM(对象关系映射),能够与关系型数据库集成
2.2. 创建环境变量文件
在项目根目录下创建 .env
文件:
1 | # 应用 |
这里的环境变量覆盖了不同模块所需的配置项,确保各模块配置的灵活性。
2.3. 引入配置模块和验证
在 AppModule
中配置 @nestjs/config
,使用 Joi
对环境变量进行验证,确保每个变量都满足格式要求:
1 | import { Module } from '@nestjs/common'; |
isGlobal
:将配置模块设为全局模块,避免在其他模块中重复引入validationSchema
:通过Joi
验证环境变量的值,确保值类型与业务需求匹配;例如DB_HOST
需要是字符串,APP_PORT
应为数值,且数据库和JWT密钥都必须存在
Joi
是一个 JavaScript 数据验证库,通常用来确保应用中的数据符合特定的规则或格式。2.3.1. 基本用法
Joi
提供了一个简单的链式 API 来定义验证规则。验证的流程一般是:定义 schema(验证规则) -> 验证数据 -> 获取结果或错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 const Joi = require('joi');
// 定义 schema
const schema = Joi.object({
name: Joi.string().min(3).max(30).required(),
age: Joi.number().integer().min(0).max(120),
email: Joi.string().email()
});
// 验证数据
const result = schema.validate({ name: 'Alice', age: 25, email: '[email protected]' });
// 检查结果
if (result.error) {
console.log(result.error.details);
} else {
console.log(result.value);
}2.3.2. 基本类型验证
Joi
支持的基本类型包括:string
、number
、boolean
、array
、object
等。每种类型可以组合其他规则,如最小/最大值、必填/选填、格式限制等。
- 字符串验证
1
2
3
4
5 Joi.string() // 定义为字符串类型
Joi.string().min(3) // 最小长度
Joi.string().max(30) // 最大长度
Joi.string().email() // 必须是电子邮箱格式
Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/) // 使用正则表达式验证格式
- 数字验证
1
2
3
4
5
6 Joi.number() // 定义为数字类型
Joi.number().integer() // 必须是整数
Joi.number().min(0) // 最小值
Joi.number().max(100) // 最大值
Joi.number().positive() // 必须是正数
Joi.number().negative() // 必须是负数
- 布尔值验证
1 Joi.boolean() // 定义为布尔类型
- 数组验证
1
2
3
4 Joi.array() // 定义为数组类型
Joi.array().items(Joi.number()) // 数组中每项都必须是数字
Joi.array().min(1).max(5) // 数组长度限制
Joi.array().unique() // 数组中的每个元素必须唯一
- 对象验证
1
2
3
4 Joi.object({
username: Joi.string().required(),
password: Joi.string().min(8).required(),
})2.3.3. 条件验证
条件验证允许定义复杂的规则,如基于字段值的条件或逻辑分支。
when
条件验证:
1
2
3
4
5
6
7
8 const schema = Joi.object({
password: Joi.string().min(8).required(),
confirmPassword: Joi.string().valid(Joi.ref('password')).when('password', {
is: Joi.exist(), // 如果 password 存在……
then: Joi.required(), // ……confirmPassword 也是必填
otherwise: Joi.forbidden() // 否则不允许出现 confirmPassword
})
});2.3.4. 嵌套对象和数组验证
Joi
允许定义嵌套结构,如对象嵌套和数组嵌套。
- 嵌套对象
1
2
3
4
5
6 const schema = Joi.object({
user: Joi.object({
name: Joi.string().required(),
age: Joi.number().min(0)
}).required()
});
- 嵌套数组
1
2
3
4
5
6 const schema = Joi.array().items(
Joi.object({
id: Joi.number().required(),
name: Joi.string().required()
})
);2.3.5. 自定义验证器
Joi
支持自定义验证函数,用于更复杂的场景。
1
2
3
4
5
6 const schema = Joi.string().custom((value, helpers) => {
if (!/^[a-zA-Z]+$/.test(value)) {
return helpers.error('any.invalid'); // 返回一个自定义错误
}
return value; // 验证通过
}, 'Custom alphabet validation');2.3.6. 配合 NestJS 使用
在 NestJS 中,可以结合
@nestjs/config
模块来使用Joi
验证配置文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
// 假设有核心数据库的配置
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().default(5432),
})
})
],
})
export class AppModule {}2.3.7. 错误处理与自定义错误消息
Joi
会在验证失败时返回详细的错误信息,可以自定义错误消息。
1
2
3
4
5
6
7
8 const schema = Joi.object({
name: Joi.string().min(3).required().messages({
'string.base': `"name" should be a type of 'text'`,
'string.empty': `"name" cannot be an empty field`,
'string.min': `"name" should have a minimum length of {#limit}`,
'any.required': `"name" is a required field`
})
});
2.4. 使用配置
作为一个使用 ConfigService
的例子,我们将从 .env
中读取 APP_PORT
的值,并将其作为应用的启动端口。
1 | import { NestFactory } from '@nestjs/core'; |
运行 nest start
来查看是否有任何问题。
3. 设置数据库连接
首先,确保自己的本机环境中有安装 MySQL 或者 PostgreSQL。
安装教程请自行在网上搜索,本篇文档将只会使用 8.0.40 MySQL Community
。
挖坑:未来可能会添加支持其他数据库的功能。
3.1. 配置 MySQL
-
打开 MySQL CLI(或者使用
mysql -u root -p
来进行连接) -
创建数据库:
1
CREATE DATABASE 数据库名称;
数据库名称要匹配
.env
中的:1
DB_NAME=数据库名称
-
创建用户:
1
CREATE USER '用户名'@'localhost' IDENTIFIED BY '密码';
这里的内容要匹配
.env
中的这一段内容:1
2DB_USER=用户名
DB_PASSWORD=密码 -
设置权限:
1
2GRANT ALL PRIVILEGES ON 数据库名称.* TO '用户名'@'localhost';
FLUSH PRIVILEGES; -
设置完后可以测试一下:
1
mysql -u 用户名 -p -h localhost -P 3306 数据库名称
3.2. 创建 DatabaseModule
在 NestJS 项目中,集中管理数据库连接的配置非常重要,尤其是在需要支持多种环境(如开发、测试、生产)时。
创建 DatabaseModule
能让我们将数据库的配置代码分离出来,以便在不同的环境中灵活调整配置,比如使用 ConfigService
来获取环境变量。
通过 TypeOrmModule.forRootAsync
方法,我们可以使用异步的方式配置 TypeORM。这样可以确保数据库配置在应用初始化时依赖于环境变量,如 DB_HOST
、DB_USER
、DB_PASSWORD
等,从而增强配置的灵活性和安全性。
1 | import { Module } from '@nestjs/common'; |
ConfigService
:用于从环境变量获取配置,确保DB_TYPE
等参数的灵活性forRootAsync
:动态配置TypeOrmModule
,适用于需要依赖环境变量初始化的模块autoLoadEntities: true
:TypeORM 会自动加载应用中定义的所有实体。这让我们可以在项目中自由地添加新的实体,而不需要每次手动导入synchronize
:将其设置为true
会在开发环境中自动同步数据库表结构,以便在本地开发时快速响应数据结构的修改。但在生产环境中,建议关闭synchronize
,以防止意外数据丢失或表结构破坏
4. 配置 Swagger 文档
在现代 Web 开发中,API 文档对于开发人员和用户来说都是至关重要的,特别是在团队协作中,清晰的 API 文档可以大大提高开发效率。
NestJS 提供了内置的 Swagger 支持,允许我们快速生成符合 OpenAPI 标准的文档,为用户提供更好的接口可视化。
OpenAPI 是一种用于描述 RESTful API 的规范,它提供了一种标准化的格式,用于定义 API 的端点、请求、响应、认证等内容。
它的前身是 Swagger 规范,因此你可能听过 Swagger 和 OpenAPI 这两个词被混用。
OpenAPI 的主要目标是使 API 设计、文档、测试和集成过程更为高效和一致。
4.1. 安装依赖
首先,我们需要安装 @nestjs/swagger
和 swagger-ui-express
两个模块。
@nestjs/swagger
提供了 NestJS 对 Swagger 的支持swagger-ui-express
是 Swagger UI 的依赖包,用于在浏览器中显示 API 文档
在项目根目录下运行以下命令来安装它们:
1 | yarn add @nestjs/swagger swagger-ui-express |
4.2. 配置 Swagger
打开 main.ts
并添加以下代码:
1 | // ... |
在这里,我们使用 DocumentBuilder
来创建 Swagger 文档的基本信息。常见的配置项有:
.setTitle()
: 设置 API 文档的标题.setDescription()
: 提供 API 的描述信息.setVersion()
: 指定 API 的版本号.addBearerAuth()
: 如果 API 需要 JWT 认证(通常用于保护 API),可以添加 Bearer 认证支持
SwaggerModule.setup()
方法将 Swagger UI 绑定到指定的路由路径(这里是/api-docs
),之后,我们可以通过访问 http://localhost:APP_PORT/api-docs
来查看生成的文档。
4.3. 如何使用 Swagger
在我们完成 Swagger 的基础配置后,接下来的步骤将详细介绍如何利用 Swagger 注释来生成清晰的 API 文档。这一部分将涵盖如何为控制器、DTO(数据传输对象)和请求参数添加 Swagger 装饰器,以便 Swagger 能够生成准确且全面的 API 文档。
4.3.1. 为控制器添加注释
在 NestJS 中,控制器负责处理客户端请求并返回响应。我们可以使用 Swagger 提供的装饰器为控制器中的每个方法添加注释,以描述其功能、请求参数和返回结果。
1 | import { Controller, Get, Post, Body } from '@nestjs/common'; |
4.3.2. 为 DTO 添加注释
DTO(数据传输对象)用于定义请求和响应的结构。使用 Swagger 的 @ApiProperty
装饰器,可以清晰地说明每个字段的含义和要求。
1 | import { ApiProperty } from '@nestjs/swagger'; |
在上面的例子中,CreateUserDto
包含了三个属性:name
、age
和 email
。每个属性都使用了 @ApiProperty
装饰器来提供详细描述,并且可以设置字段的其他约束(如是否必填、类型等)。
4.3.3. 为请求参数添加注释
如果你的 API 需要接受路径参数、查询参数或请求体中的数据,Swagger 也提供了相关的装饰器来帮助描述这些参数。
1 | import { Controller, Get, Param } from '@nestjs/common'; |
在这个示例中,@ApiParam
用于描述路径参数id。它帮助用户理解这个参数是必须的,且应该是一个数字。
5. 设置基础中间件
在现代 Web 开发中,处理安全性、请求速率限制、响应压缩以及自定义日志记录是打造可靠、高效应用的基础。
NestJS 提供了简单灵活的中间件配置支持,通过整合 helmet
、@nestjs/throttler
、compression
等库,开发者可以轻松地实现这些功能。
5.1. 添加 CORS 支持
首先,我们需要确保应用支持跨域请求(CORS),特别是在前后端分离的情况下。以下是启用和配置 CORS 的方法:
1 | // main.ts |
在 .env
中添加:
1 | CORS_ORIGIN=http://localhost:3000 |
使用
http://localhost:3000
是因为 React 本地环境默认运行在localhost:3000
。
5.2. 增强安全性
helmet
是一组帮助设置安全 HTTP 头的中间件,能够防范常见的 Web 攻击(例如,XSS 攻击和点击劫持)。
安装 helmet
库:
1 | yarn add helmet |
开启 helmet
保护:
1 | // main.ts |
通过这段简单的代码,helmet
会自动添加一组常用的安全头(以下信息来自于 NPM):
- Content-Security-Policy:一个强大的允许清单,控制页面上可以发生的操作,有助于缓解多种攻击
- Cross-Origin-Opener-Policy:帮助页面实现进程隔离
- Cross-Origin-Resource-Policy:阻止其他网站跨域加载您的资源
- Origin-Agent-Cluster:将进程隔离改为基于源的方式
- Referrer-Policy:控制
Referer
请求头 - Strict-Transport-Security:告知浏览器优先使用 HTTPS
- X-Content-Type-Options:避免 MIME 类型嗅探
- X-DNS-Prefetch-Control:控制 DNS 预取
- X-Download-Options:强制将下载的文件保存到本地(仅适用于 Internet Explorer)
- X-Frame-Options:传统的标头,用于防范点击劫持攻击
- X-Permitted-Cross-Domain-Policies:控制 Adobe 产品(如 Acrobat)的跨域行为
- X-Powered-By:关于 Web 服务器的信息,已移除,以防止简单攻击利用该信息
- X-XSS-Protection:传统的标头,旨在防止 XSS 攻击,但通常效果不佳,因此 Helmet 将其禁用
5.3. 压缩响应
压缩响应能够有效减少传输的数据量,提升页面加载速度。
安装 compression
库:
1 | yarn add compression |
配置压缩的级别和触发条件:
1 | // main.ts |
在 .env
中添加:
1 | # 响应压缩 |
在上面的代码中,level
设置了压缩级别(范围从0-9,数字越大压缩越强,但 CPU 负荷越高),而 threshold
设置了触发压缩的响应体积阈值(单位为字节)。
5.4. 限制请求速率
防止滥用 API 资源是每个 Web 应用的核心需求之一。我们可以使用 @nestjs/throttler
模块对请求速率进行限制,确保服务不会被大量请求淹没。
1 | yarn add @nestjs/throttler |
我们在 AppModule
中通过 ThrottlerModule.forRootAsync
配置速率限制。利用 ConfigService
从 .env
文件中获取 ttl
(时间窗口)和 limit
(最大请求数)参数:
1 | // app.module.ts |
在 .env
中添加:
1 | # 速率限制 |
5.4.1. 使用例子
配置后,系统会自动为所有API路由设置速率限制。也可以在特定控制器或路由中通过 @Throttle
装饰器进行覆盖:
1 | import { Controller, Get } from '@nestjs/common'; |
5.5. 自定义日志记录
为了记录请求信息,我们可以实现一个简单的 LoggerMiddleware
,并在 AppModule
中配置它:
1 | // logger.middleware.ts |
在 AppModule
中使用 configure
方法应用此中间件:
1 | // app.module.ts |
这样,每次请求都会在控制台输出请求方法和 URL 路径,帮助我们跟踪请求流向和响应情况。
启动 NestJS 项目,在浏览器中访问 localhost:APP_PORT
(或者默认的 localhost:4000
),就能在终端中看到 Request... GET /
的字眼。
5.6. 设置全局错误处理
为了统一错误响应格式,可以创建自定义异常过滤器来捕获异常,并返回标准化的错误信息。
我们在项目中定义 HttpExceptionFilter
类,并将其注册为全局过滤器:
1 | // http-exception.filter.ts |
接着在 main.ts
中注册过滤器:
1 | // main.ts |
通过此过滤器,所有未处理的异常都会以标准格式返回。
6. 配置日志系统
在开发和运维中,日志记录是至关重要的一部分。通过详细的日志记录,我们可以更好地了解应用的运行状态、排查错误,甚至帮助团队进行性能优化。
在开发或生产环境中,可能会遇到以下问题:
- 控制台日志输出过于混乱:控制台日志输出没有明显的视觉区分,开发者难以快速找到关键信息
- 文件日志管理不当:日志文件没有分目录管理,日志存储时间不固定,且文件体积容易过大
- 日志轮转:没有对日志文件进行按日期轮换,容易导致单个日志文件过大,不利于维护
6.1. 安装依赖
我们首先需要安装 chalk
、nest-winston
、winston
和 winston-daily-rotate-file
这些依赖。
1 | yarn add chalk@^4 nest-winston winston winston-daily-rotate-file |
注意:由于我的 NestJS 项目使用的是 CommonJS 模块系统,和使用 ESM 的 chalk@5
不兼容,所以我采用的最简单直接的方法就是降级到 chalk@4
。
6.2. 配置 winston
日志文件
在 NestJS 项目中创建一个 winston.logger.ts
文件,用于配置 winston
的日志记录选项,包括日志等级、日志格式、文件轮转等。
6.2.1. 配置日志目录
我们将设置不同的日志目录来分别存储错误日志、警告日志和常规应用日志。
使用 fs
来检查目录是否存在,不存在时自动创建:
1 | import * as fs from 'fs'; |
这段代码创建了 logs/errors
、logs/warnings
和 logs/app
三个目录,用于分别保存错误、警告和常规日志。
6.2.2. 定义日志颜色
接下来,通过 chalk
为不同的日志级别定义颜色。这样在控制台输出时,不同的日志级别会有明显的颜色区分:
1 | import * as chalk from 'chalk'; |
这样做的好处是,可以根据日志等级设置不同颜色,从而在控制台中快速识别重要的日志信息。
6.2.3. 配置 winston
日志选项
接下来,我们配置 winston
的核心功能,包括日志格式、日志文件轮转和控制台输出:
1 | import { createLogger, format, transports } from 'winston'; |
这段配置实现了以下几个功能:
DailyRotateFile
:设置了日志文件的轮转,每个日志类型(错误、警告、应用)都将按日期命名并存储- 控制台输出:控制台输出带有颜色区分,并包含时间戳和日志级别,便于快速读取
- 日志格式:定义了
json
格式日志输出,并包含timestamp
和stack
信息
6.3. 引入日志配置
在 AppModule
中通过 WinstonModule
引入 winstonLogger
,使日志系统能够全局生效:
1 | import { WinstonModule } from 'nest-winston'; |
通过 WinstonModule.forRoot()
配置,我们将之前定义的 winstonLogger
作为全局日志管理器,使 NestJS 自动将应用日志转发到 winston
。
6.4. 应用日志系统
为了启用配置的日志系统,我们需要在 main.ts
中将其应用到应用程序:
1 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; |
这样就将 winston
配置的日志记录器注入到应用中,使日志管理器可以通过 NestJS 的日志 API 来记录日志。
6.5. 修改 LoggerMiddleware
最后,我们来修改一下 LoggerMiddleware
,将其记录下每个请求的详细信息,包括:
- 请求方法
- URL
- IP
- HTTP 版本
- 状态码
- 响应时间等
这在调试和性能分析时尤为有用。
1 | import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; |
- 事件监听:使用
finish
事件来确保所有响应数据都已发送 - 日志格式:每个请求记录的格式包括请求时间、请求方法、URL、IP、状态码、响应耗时等信息
- 自动区分日志级别:根据响应的状态码自动设置日志等级
- 错误状态码(500+)记录为
error
- 客户端错误(400+)记录为
warn
- 其他成功请求则都记录为
log
- 错误状态码(500+)记录为