本章主要介绍 NestJS 实战后端开发的全局配置:基础栈介绍、项目基础搭建、Apifox接入、navicat数据库连接、Swagger API文档生成、数据响应全局封装、全局异常处理、全局拦截器、全局日志监听、服务监控、JWT权限配置、登录登出、公共服务等。
对于Nest基础学习,请看NodeJS-NestJS基础。
技术栈介绍
- 主框架 :
NestJS、TypeScript、Rxjs - 身份验证 :
JWT - API接口文档 :
Swagger - 接口调试工具 :
Apifox - 单元测试 :
jest - 数据库 :
MySQL、typeorm、mysql2 - 数据库可视化 :
Navicat或者VScode插件Database Client
NestJS安装
css
npm i -g @nestjs/cli
nest new server-backend
选择你喜欢的包管理模块
perl
? Which package manager would you ❤️ to use?
❯ npm
yarn
pnpm
项目运行
应用程序运行后,打开浏览器并访问 http://localhost:3000/ 地址,将看到类似 Hello World! 的信息。
我 3000 端口有其他项目占用,我就修改了下main.ts的代码,把端口改成 8004 了
arduino
npm run start
安装业务必要依赖包
日期时间处理包 moment:
npm install moment
密码加密包 bcrypt:
npm install bcrypt
excel 文件生成工具 xlxs:
npm install xlsx
新建数据库配置文件:
在src目录下新建一个 ormconfig.ts 文件:
javascript
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
// 这里用户名密码请换成自己的,database也可以换成自己的
export const typeOrmConfig: TypeOrmModuleOptions = {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'Initial0!',
database: 'crm-database',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
retryDelay: 500, // 重试连接数据库间隔
retryAttempts: 10, // 重试连接数据库的次数
autoLoadEntities: true,
};
在app.module.ts添加数据库连接:
数据库建好后,需要在 app.module.ts 里建立连接
python
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { typeOrmConfig } from './ormconfig';
@Module({
imports: [
TypeOrmModule.forRoot(typeOrmConfig),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
接入 Swagger API 文档
安装 swagger 相关的 npm 依赖包
sql
npm install @nestjs/swagger swagger-ui-express --save
修改 main.ts 集成创建 swagger-ui 相关代码:
javascript
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 注册全局接口参数验证
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
stopAtFirstError: true,
skipMissingProperties: false, // 确保所有的属性都被验证
}),
);
const swaggerOptions = new DocumentBuilder()
.setTitle('cms-project api')
.setDescription('内容管理系统描述')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, swaggerOptions);
SwaggerModule.setup('api-docs', app, document);
await app.listen(8004);
}
bootstrap();
启动服务后,我们访问http://localhost:8004/api-docs看看:
上图只能看到没有分组的接口,并且没有接口描述,也没有参数定义,但 @nestjs/swagger 给我们定义了很多装饰器去做,下面介绍下常用的装饰器方法。
@ApiTags: 用于为控制器或路由指定一个或多个标签,这些标签在Swagger UI中用于组织和分类API操作。@ApiOperation: 为控制器的方法提供一个简短的摘要和详细描述,这些信息将显示在 Swagger UI 中。@ApiParam: 描述一个路径参数、查询参数或响应参数,包括名称、类型、描述等信息。@ApiBody: 指定请求体的DTO类,用于描述请求体的结构。@ApiResponse: 描述API响应的结构,包括状态码和描述。@ApiBearerAuth: 指定请求需要携带Bearer Token进行身份验证。@ApiProperty: 用于DTO类中的属性,为其添加元数据,如描述、是否必填、默认值等。@ApiQuery: 描述查询参数,包括名称、类型、描述等。@ApiHeader: 描述请求头信息,包括名称、类型、描述等。@ApiExcludeEndpoint: 用于排除某个控制器方法,使其不显示在 Swagger UI 中。@ApiImplicitBody: 用于隐式设置请求体的定义,可以避免在 DTO 类上使用多个@ApiProperty装饰器。@ApiImplicitParam: 用于为请求隐式添加参数定义,类似于@ApiParam,但是用于请求体内部的参数。@ApiUseTags: 用于给控制器或路由指定标签,这些标签在Swagger UI中用于分类。
响应全局封装
上面的用户管理相关接口,我们都只是返回数据,但给到前端的时候一般还需要给到 code 去判断响应状态,返回 msg 抛错,返回 result 才是真正的结果。那我们需要进行全局拦截请求响应和错误处理。
创建全局拦截器
在 src 目录下新建 interceptors/transform.interceptor.ts:
typescript
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface Response<T> {
code: number;
msg: string;
result: T;
}
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<Response<T>> {
return next.handle().pipe(
map((data) => ({
code: 200,
msg: 'success',
result: data,
})),
);
}
}
全局异常处理
在 src 目录下新建 exceptions/http-exception.filter.ts:
ini
import {
ArgumentsHost,
ExceptionFilter,
HttpException,
HttpStatus,
} from '@nestjs/common';
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
let status: number;
let errorResponse: string | object;
if (exception instanceof HttpException) {
status = exception.getStatus();
errorResponse = exception.getResponse();
} else {
status = HttpStatus.INTERNAL_SERVER_ERROR;
errorResponse = exception;
}
// 这里需要判断错误响应是否是数组,并进行相应处理
let errorMessage: string | unknown = 'Internal Server Error';
if (
typeof errorResponse === 'object' &&
errorResponse !== null &&
'message' in errorResponse
) {
if (Array.isArray(errorResponse.message)) {
errorMessage = errorResponse.message[0];
} else {
errorMessage = errorResponse.message;
}
} else if (typeof errorResponse === 'string') {
errorMessage = errorResponse;
}
return response.status(status).json({
code: status,
msg: errorMessage || '服务异常',
result: null,
});
}
}
全局注册拦截器和过滤器
在 main.ts 使用 useGlobalFilters 和 useGlobalInterceptors 使用全局过滤器和拦截器:
javascript
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './exceptions/http-exception.filter';
import { TransformInterceptor } from './interceptors/transform.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 注册全局接口参数验证
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
stopAtFirstError: true,
skipMissingProperties: false, // 确保所有的属性都被验证
}),
);
// 注册全局过滤器
app.useGlobalFilters(new HttpExceptionFilter());
// 注册全局拦截器
app.useGlobalInterceptors(new TransformInterceptor());
const swaggerOptions = new DocumentBuilder()
.setTitle('cms-project api')
.setDescription('内容管理系统描述')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, swaggerOptions);
SwaggerModule.setup('api-docs', app, document);
await app.listen(8004);
}
bootstrap();
把 Swagger 导入 apifox
为了更好的进行单元测试和文档查看,我们使用 apifox 进行接口调试,那我们就要把 swagger 的 api 文档导入 apifox。
我们先把 api 导出到 json 文件中,在main.ts中新增:
javascript
import { writeFileSync } from 'fs';
// ...其他引入
async function bootstrap() {
// ...其他全局配置
// 将 Swagger 文档保存为 JSON 文件
writeFileSync('./swagger.json', JSON.stringify(document, null, 2), 'utf8');
await app.listen(8004);
}
bootstrap();
使用 apifox 的导入数据功能:
导入成功后,接口如下,发起请求试验下:
集成 nest-winston 进行日志记录
使用 nest-winston 集成 winston 来进行日志记录,并配置 winston 的传输方式,如控制台、文件、每日轮换文件等。
创建中间件来记录每个请求的信息,如请求方法、URL、IP 地址、状态码和响应时间。可以在请求结束时记录日志,并且可以根据响应状态码来决定日志级别。
安装 nest-winston 依赖
css
npm install nest-winston winston winston-daily-rotate-file chalk@4
配置 nest-winston
创建 config/winston.logger.ts 文件进行 winston 配置:
javascript
import * as chalk from 'chalk'; // 用于颜色化输出
import { createLogger, format, transports } from 'winston';
import * as DailyRotateFile from 'winston-daily-rotate-file';
// 定义日志级别颜色
const levelsColors = {
error: 'red',
warn: 'yellow',
info: 'black',
debug: 'blue',
verbose: 'cyan',
};
const winstonLogger = createLogger({
format: format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.splat(),
format.json(),
),
defaultMeta: { service: 'log-service' },
transports: [
new DailyRotateFile({
filename: 'logs/errors/error-%DATE%.log', // 日志名称,占位符 %DATE% 取值为 datePattern 值。
datePattern: 'YYYY-MM-DD', // 日志轮换的频率,此处表示每天。
zippedArchive: true, // 是否通过压缩的方式归档被轮换的日志文件。
maxSize: '20m', // 设置日志文件的最大大小,m 表示 mb 。
maxFiles: '14d', // 保留日志文件的最大天数,此处表示自动删除超过 14 天的日志文件。
level: 'error', // 日志类型,此处表示只记录错误日志。
}),
new DailyRotateFile({
filename: 'logs/warnings/warning-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
level: 'warn',
}),
new DailyRotateFile({
filename: 'logs/app/app-%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
}),
new transports.Console({
format: format.combine(
format.colorize({
colors: levelsColors,
}),
format.simple(),
format.printf((info) => {
// 获取 Info Symbols key
const symbols = Object.getOwnPropertySymbols(info);
const color = levelsColors[info[symbols[0]]]; // 获取日志级别的颜色
const chalkColor = chalk[color];
const message = `${chalkColor(info.timestamp)} ${chalkColor(info[symbols[2]])}`;
return message;
}),
),
level: 'debug',
}),
],
});
export default winstonLogger;
主模块应用winstonLogger配置
在应用的主模块中导入WinstonModule,并进行配置。
typescript
import { Module } from '@nestjs/common';
import { WinstonModule } from 'nest-winston';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import winstonLogger from './config/winston.logger';
@Module({
imports: [
// 注册日志记录文件
WinstonModule.forRoot({
transports: winstonLogger.transports,
format: winstonLogger.format,
defaultMeta: winstonLogger.defaultMeta,
exitOnError: false, // 防止意味退出
}),
// ...其他配置
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
创建全局日志中间件
为了监听整个系统的日志,需要建立全局中间件 middlewares/logger.middleware.ts:
typescript
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import * as moment from 'moment';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
private logger = new Logger();
use(req: Request, res: Response, next: NextFunction) {
// 记录开始时间
const start = Date.now();
// 获取请求信息
const { method, originalUrl, ip, httpVersion, headers } = req;
// 获取响应信息
const { statusCode } = res;
res.on('finish', () => {
// 记录结束时间
const end = Date.now();
// 计算时间差
const duration = end - start;
// 这里可以根据自己需要组装日志信息:[timestamp] [method] [url] HTTP/[httpVersion] [client IP] [status code] [response time]ms [user-agent]
const logFormat = `${moment().valueOf()} ${method} ${originalUrl} HTTP/${httpVersion} ${ip} ${statusCode} ${duration}ms ${headers['user-agent']}`;
// 根据状态码,进行日志类型区分
if (statusCode >= 500) {
this.logger.error(logFormat, originalUrl);
} else if (statusCode >= 400) {
this.logger.warn(logFormat, originalUrl);
} else {
this.logger.log(logFormat, originalUrl);
}
});
next();
}
}
在主模块配置该中间件,应用于所有路由,这样所有请求都会打印基础日志:
typescript
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { LoggerMiddleware } from './middlewares/logger.middleware';
@Module({
//...
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('*');
}
}
在 main.ts 中使用 useLogger 更换日志记录器,我们在使用的使用可以使用 NestJS 内置的 Logger 就行。
javascript
import { writeFileSync } from 'fs';
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './exceptions/http-exception.filter';
import { TransformInterceptor } from './interceptors/transform.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 注册全局接口参数验证
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
stopAtFirstError: true,
skipMissingProperties: false, // 确保所有的属性都被验证
}),
);
// 注册全局过滤器
app.useGlobalFilters(new HttpExceptionFilter());
// 注册全局拦截器
app.useGlobalInterceptors(new TransformInterceptor());
const swaggerOptions = new DocumentBuilder()
.setTitle('cms-project api')
.setDescription('内容管理系统描述')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, swaggerOptions);
SwaggerModule.setup('api-docs', app, document);
// 将 Swagger 文档保存为 JSON 文件
writeFileSync('./swagger.json', JSON.stringify(document, null, 2), 'utf8');
// 更换日志记录器
app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER));
await app.listen(8004);
}
bootstrap();