NestJS实战-后端开发-全局配置

本章主要介绍 NestJS 实战后端开发的全局配置:基础栈介绍、项目基础搭建、Apifox接入、navicat数据库连接、Swagger API文档生成、数据响应全局封装、全局异常处理、全局拦截器、全局日志监听、服务监控、JWT权限配置、登录登出、公共服务等。

对于Nest基础学习,请看NodeJS-NestJS基础

技术栈介绍

  • 主框架NestJSTypeScriptRxjs
  • 身份验证JWT
  • API接口文档Swagger
  • 接口调试工具Apifox
  • 单元测试jest
  • 数据库MySQLtypeormmysql2
  • 数据库可视化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 使用 useGlobalFiltersuseGlobalInterceptors 使用全局过滤器和拦截器:

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 进行接口调试,那我们就要把 swaggerapi 文档导入 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();
相关推荐
米饭同学i1 小时前
浏览器记住密码导致忘记密码页面输入框回显错乱?看这篇就够了
前端
陆枫Larry1 小时前
从一个按钮间距,聊透 CSS 的 gap 属性
前端
北冥有鱼1 小时前
mqtt 测试
前端·后端
张鑫旭2 小时前
都AI时代了,我为何还在学习前端基础知识?
前端
swipe2 小时前
正则表达式入门到进阶:从表单校验到手写模板引擎
前端·javascript·面试
阿祖zu2 小时前
别再优化 RAG 了,适配 Agent 的 LLM Wiki 知识库理念
前端·后端·aigc
kyriewen2 小时前
前端错误监控最全指南:捕获 JS 异常、Promise 拒绝、资源加载失败,附上报代码
前端·javascript·监控
狗哥哥3 小时前
船队运营可视化技术方案
前端
大家的林语冰3 小时前
ESLint 近期动态大全,新版本正式发布,antfu 大佬推荐的插件也更新了!
前端·javascript·前端工程化