# nest-prisma-template **Repository Path**: Kanbara-Take/nest-prisma-template ## Basic Information - **Project Name**: nest-prisma-template - **Description**: nest-prisma-mysql-redis-minio-swagger 后端单体项目模板 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2024-08-20 - **Last Updated**: 2024-08-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README

Nest Logo

## 项目概述 `node版本 18.20.0` 技术栈: - Nest 后端框架 - Prisma ORM 连接数据库框架 - mysql 持久化存储 - redis 缓存以及临时数据存储 - minio OSS对象存储 - swagger swagger文档 - winston 日志 功能点 - jwt 单 token 登录注册无感刷新 - svg-captcha 图形验证码 - email 邮箱验证码 - 动态读取环境配置 - 多版本共存开发 ## 项目使用 ```bash # 下载依赖 $ npm install # 开发环境 $ npm run start:dev # 生成环境 $ npm run build ``` ## 项目初始化 ### 1.下载nest脚手架 ```BASH npm install -g @nestjs/cli ``` ### 2.创建项目 ```BASH nest new 项目名 ``` ### 3.解决 ESLint 的 Delete `CR` 报错 .eslintrc.js ```JS module.exports = { .... rules: { .... "prettier/prettier": ["error", { endOfLine: "auto" }], }, }; ``` ### 4.关闭自动生成测试模块 .nest-cli.json ```JSON "generateOptions": { "spec": false }, ``` ### 5. user 用户模块(模块例子) ``` bash # 生成模块(不生成CRUD) nest g resource user ``` /views/user/user.controller.ts ``` js import { Controller, Post, Body } from '@nestjs/common'; import { UserService } from './user.service'; import { RegisterUserDto } from './dto/register-user.dto'; @Controller('user') export class UserController { constructor(private readonly userService: UserService) {} @Post('register') async register(@Body() registerUser: RegisterUserDto) { return await this.userService.create(registerUser); } } ``` /views/user/user.service.ts ``` js import { Inject, Injectable } from '@nestjs/common'; import { PrismaService } from 'src/middleware/prisma/prisma.service'; import { Prisma } from '@prisma/client'; @Injectable() export class UserService { @Inject(PrismaService) private prisma: PrismaService; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore async create(data: Prisma.UserCreateInput) { // 校验验证码 delete data.captcha; const list = data; // 自动生成账号 唯一id const table = await this.prisma.user.findMany(); list.account = `HH${(table.length + 1).toString().padStart(5, '0')}`; // 自动生成默认账号头像 list.avatarURL = 'http://localhost:9000/dev/err.png'; await this.prisma.user.create({ data: list, select: { id: true, }, }); return '新增成功'; } } ``` /views/user/dto/register-user.dto.ts ``` js export class RegisterUserDto { username: string; nickName: string; password: string; email: string; phone: string; captcha: string; } ``` ## 项目开发 ### 1. swagger 配置 ``` bash npm install --save @nestjs/swagger ``` ```js import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; async function bootstrap() { ... const config = new DocumentBuilder() .setTitle('Nest-template') .setDescription('Nest.js 后端项目模板') .setVersion('V1.0.0') .addBearerAuth({ type: 'http', description: '基于 jwt 的认证', }) .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('swagger-doc', app, document); ... } bootstrap(); ``` `注释实例` 注册接口 /views/user/user.controller.ts ```JS import { Controller, Post, Body, HttpStatus } from '@nestjs/common'; import { UserService } from './user.service'; import { RegisterUserDto } from './dto/register-user.dto'; import { ApiOperation, ApiTags, ApiResponse,ApiQuery,ApiBearerAuth } from '@nestjs/swagger'; import { PublicVoSuccess, PublicVoError } from 'src/utils/publicVo/index'; @ApiTags('用户管理') // 模块名描述 @Controller('user') export class UserController { constructor(private readonly userService: UserService) {} @Post('register') // 接口描述 @ApiOperation({ summary: '用户注册', description: '用户注册接口' }) // jwt 配置 描述 @ApiBearerAuth() // query 请求体 接口描述 @ApiQuery({ name: 'refreshToken', type: String, description: '刷新token', required: true, }) // 公共 返回体 接口描述 VO @ApiResponse({ status: HttpStatus.OK, description: '请求成功', type: PublicVoSuccess, }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '请求失败', type: PublicVoError, }) async register(@Body() registerUser: RegisterUserDto) { return await this.userService.create(registerUser); } } ``` 注册 请求体 body 接口描述 dto /views/user/dto/register-user.dto.ts ```JS import { ApiProperty } from '@nestjs/swagger'; export class RegisterUserDto { // body 请求体 描述 @ApiProperty({ description: '用户名', required: true }) username: string; @ApiProperty({ description: '密码', required: true }) password: string; @ApiProperty({ description: '昵称', required: false }) nickName: string; @ApiProperty({ description: '邮箱', required: false }) email: string; @ApiProperty({ description: '电话', required: false }) phone: string; @ApiProperty({ description: '验证码', required: true }) captcha: string; } ``` 公共 返回体 接口描述 VO /utils/publicVo/index.ts ```JS import { ApiProperty } from '@nestjs/swagger'; export class PublicVoSuccess { @ApiProperty({ example: '200', description: '状态码', required: false }) code: number; @ApiProperty({ example: '请求成功', description: '请求消息', required: false, }) msg: string; @ApiProperty({ example: 'success', description: '数据', required: false }) data: string; } export class PublicVoError { @ApiProperty({ example: '500', description: '状态码', required: false }) code: number; @ApiProperty({ example: '请求失败', description: '请求消息', required: false, }) msg: string; @ApiProperty({ example: 'error', description: '数据', required: false }) data: string; } ``` ### 2. env动态读取环境配置 ``` bash npm install --save @nestjs/config ``` /app.module.ts ```JS import { ConfigModule } from '@nestjs/config'; @Module({ imports: [ ... ConfigModule.forRoot({ isGlobal: true, envFilePath: 'src/.env', }), ], ... }) export class AppModule {} ``` .nest-cli.json /main.ts `动态读取项目运行端口号配置` ```JS import { ConfigService } from '@nestjs/config'; async function bootstrap() { ... const configService = app.get(ConfigService); ... await app.listen(configService.get('nest_server_port')); } bootstrap(); ``` `动态 读取 redis 配置 方式一` /middleware/redis/redis.module.ts ```JS import { ConfigService } from '@nestjs/config'; @Global() @Module({ providers: [ { ... async useFactory(configService: ConfigService) { const client = createClient({ socket: { host: configService.get('redis_server_host'), port: configService.get('redis_server_port'), }, database: configService.get('redis_server_db'), }); ... }, inject: [ConfigService], }, ], ... }) export class RedisModule {} ``` `动态 读取 email配置 方式二` /middleware/email/email.service.ts ```JS ... import { ConfigService } from '@nestjs/config'; @Injectable() export class EmailService { ... constructor(private configService: ConfigService) { this.transporter = createTransport({ host: this.configService.get('nodemailer_host'), port: this.configService.get('nodemailer_port'), secure: false, auth: { user: this.configService.get('nodemailer_auth_user'), pass: this.configService.get('nodemailer_auth_pass'), }, }); } ... } ``` .package.json `打包时将.env文件复制到dist文件里` ```JS { ... "scripts": { ... "build": "nest build && cp .env dist/", ... }, ... } ``` ### 3. 封装 prisma 为中间件 `安装 prisma` ``` bash npm install prisma --save-dev ``` `prisma 初始化创建 schema` ``` bash npx prisma init ``` `prisma 更改配置` /prisma ``` bash DATABASE_URL="mysql://root:你的密码@localhost:3306/数据库名" ``` .env ``` bash datasource db { provider = "mysql" url = env("DATABASE_URL") } ``` ``` bash # 重置数据库 npx prisma migrate reset # 创建表 npx prisma migrate dev --name 表名 ``` `封装 prisma 为中间件` ``` bash # 生成模块 nest g service prisma nest g module prisma ``` /middleware/prisma/prisma.module.ts ```js import { Global, Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; @Global() @Module({ providers: [PrismaService], exports: [PrismaService], }) export class PrismaModule {} ``` /middleware/prisma/prisma.service.ts ``` js import { Injectable, OnModuleInit } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { constructor() { super({ log: [ { emit: 'stdout', level: 'query' } ] }) } async onModuleInit() { await this.$connect(); } } ``` ### 4. 封装 redis 为中间件 ``` bash # 生成模块 nest g module redis nest g service redis ``` ``` bash npm install --save redis ``` /middleware/redis/redis.module.ts ```JS import { Global, Module } from '@nestjs/common'; import { RedisService } from './redis.service'; import { createClient } from 'redis'; @Global() @Module({ providers: [ RedisService, { provide: 'REDIS_CLIENT', async useFactory() { const client = createClient({ socket: { host: 'localhost', port: 6379, }, database: 2, }); await client.connect(); return client; }, }, ], exports: [RedisService], }) export class RedisModule {} ``` /middleware/redis/redis.service.ts ```JS import { Injectable, Inject } from '@nestjs/common'; import { RedisClientType } from 'redis'; @Injectable() export class RedisService { @Inject('REDIS_CLIENT') private redisClient: RedisClientType; async get(key: string) { return await this.redisClient.get(key); } async set(key: string, value: string | number, ttl?: number) { await this.redisClient.set(key, value); if (ttl) { await this.redisClient.expire(key, ttl); } } async del(key: string) { await this.redisClient.del(key); } } ``` ### 5. jwt 配置 ``` bash # 生成模块 nest g module jwt nest g service jwt ``` ``` bash npm install --save @nestjs/jwt --save ``` /middleware/jwt/jwt.module.ts ```JS import { Global, Module } from '@nestjs/common'; import { JwtService } from './jwt.service'; import { JwtModule } from '@nestjs/jwt'; @Global() @Module({ imports: [ JwtModule.registerAsync({ global: true, useFactory() { return { secret: 'HH', signOptions: { expiresIn: '30m', // 默认 30 分钟 }, }; }, }), ], providers: [JwtService], exports: [JwtService], }) export class JwtsModule {} ``` /views/user/user.service.ts `userLogin`方法 ```JS ... import { JwtService } from '@nestjs/jwt'; export class UserService { @Inject(JwtService) private jwtService: JwtService; ... async login() { ... return { foundUser, token: this.jwtService.sign( { userId: foundUser.id, username: foundUser.username, }, { expiresIn: '7d', }, ), }; } } ``` ### 6. 登录鉴权 ``` bash # 生成模块 nest g guard auth --flat ``` /middleware/auth/auth.guard.ts ```JS import { CanActivate, ExecutionContext, Inject, Injectable, UnauthorizedException, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { Request, Response } from 'express'; import { Observable } from 'rxjs'; interface JwtUserData { userId: number; username: string; } declare module 'express' { interface Request { user: JwtUserData; } } @Injectable() export class AuthGuard implements CanActivate { @Inject() private reflector: Reflector; @Inject(JwtService) private jwtService: JwtService; canActivate( context: ExecutionContext, ): boolean | Promise | Observable { const request: Request = context.switchToHttp().getRequest(); const response: Response = context.switchToHttp().getResponse(); const requireLogin = this.reflector.getAllAndOverride('require-login', [ context.getClass(), context.getHandler(), ]); if (!requireLogin) { return true; } const authorization = request.headers.authorization; if (!authorization) { throw new UnauthorizedException('用户未登录'); } try { const token = authorization.split(' ')[1]; const data = this.jwtService.verify(token); request.user = { userId: data.userId, username: data.username, }; response.header( 'token', this.jwtService.sign( { userId: data.userId, username: data.username, }, { expiresIn: '7d', }, ), ); return true; } catch (e) { console.log(e); throw new UnauthorizedException('token 失效,请重新登录'); } } } ``` /middleware/auth/custom.decorator.ts ```JS import { SetMetadata } from '@nestjs/common'; import { createParamDecorator, ExecutionContext } from '@nestjs/common'; import { Request } from 'express'; export const RequireLogin = () => SetMetadata('require-login', true); export const UserInfo = createParamDecorator( (data: string, ctx: ExecutionContext) => { const request = ctx.switchToHttp().getRequest(); if (!request.user) { return null; } return data ? request.user[data] : request.user; }, ); ``` /app.module.ts ```JS import { APP_GUARD } from '@nestjs/core'; import { AuthGuard } from './middleware/auth/auth.guard'; @Module({ ... providers: [ { provide: APP_GUARD, useClass: AuthGuard, }, ], }) export class AppModule {} ``` ### 7. 响应拦截器 `自定义返回的成功响应格式` /middleware/format/success-response.ts ```JS import { CallHandler, ExecutionContext, Injectable, NestInterceptor, } from '@nestjs/common'; import { Response } from 'express'; import { map, Observable } from 'rxjs'; @Injectable() export class FormatSuccessResponse implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { const response = context.switchToHttp().getResponse(); return next.handle().pipe( map((data) => { return { code: response.statusCode, msg: 'success', data, }; }), ); } } ``` main.ts ```JS import { FormatSuccessResponse } from './middleware/format/success-response'; async function bootstrap() { ... app.useGlobalInterceptors(new FormatSuccessResponse()); ... } bootstrap(); ``` `自定义返回的异常响应格式` /middleware/format/error-response.ts ```JS import { ArgumentsHost, Catch, ExceptionFilter, HttpException, } from '@nestjs/common'; import { Response } from 'express'; @Catch(HttpException) export class FormatErrorFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const http = host.switchToHttp(); const response = http.getResponse(); const statusCode = exception.getStatus(); const res = exception.getResponse() as { message: string[] }; response.status(statusCode).json({ code: statusCode, msg: res?.message?.join ? res?.message?.join(',') : exception.message, data: 'error', }); } } ``` main.ts ```JS import { FormatErrorFilter } from './middleware/format/error-response'; async function bootstrap() { ... app.useGlobalFilters(new FormatErrorFilter()); ... } bootstrap(); ``` `接口记录` /middleware/format/invoke-record.interceptor.ts ```JS import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor, } from '@nestjs/common'; import { Request } from 'express'; import { Observable, tap } from 'rxjs'; @Injectable() export class InvokeRecordInterceptor implements NestInterceptor { private readonly logger = new Logger(InvokeRecordInterceptor.name); intercept( context: ExecutionContext, next: CallHandler, ): Observable | Promise> { const request = context.switchToHttp().getRequest(); const userAgent = request.headers['user-agent']; const { ip, method, path } = request; return next.handle().pipe( tap(() => { this.logger.debug(`Mapped {${path},${method}} route`); }), ); } } ``` main.ts ```JS import { InvokeRecordInterceptor } from './middleware/format/invoke-record.interceptor'; async function bootstrap() { ... app.useGlobalInterceptors(new InvokeRecordInterceptor()); ... } bootstrap(); ``` ### 8. 请求体校验 ``` bash npm install --save class-validator class-transformer ``` ```js import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { ... app.useGlobalPipes(new ValidationPipe()); ... } bootstrap(); ``` `检验实例` /views/user/dto/register-user.dto.ts ```JS import { ApiProperty } from '@nestjs/swagger'; // 校验引入 import { IsNotEmpty, MinLength, IsEmail, MaxLength } from 'class-validator'; export class RegisterUserDto { @ApiProperty({ description: '用户名', required: true }) // 非空校验 @IsNotEmpty({ message: '用户名不能为空', }) username: string; @ApiProperty({ description: '密码', required: true }) @IsNotEmpty({ message: '密码不能为空', }) // 最小长度校验 @MinLength(6, { message: '密码不能少于6位', }) password: string; @ApiProperty({ description: '昵称', required: false }) nickName: string; @ApiProperty({ description: '邮箱', required: true }) @IsNotEmpty({ message: '邮箱不能为空', }) // 校验邮箱 @IsEmail( {}, { message: '不是合法的邮箱格式', }, ) email: string; @ApiProperty({ description: '电话', required: false }) phone: string; @ApiProperty({ description: '验证码', required: true }) @IsNotEmpty({ message: '验证码不能为空', }) // 最长长度校验 @MaxLength(6, { message: '验证码最大6位', }) captcha: string; } ``` ### 9. 封装 email 为中间件 ``` bash # 生成模块 nest g module email nest g service email ``` ``` bash npm install nodemailer --save ``` /middleware/email/email.module.ts ```JS import { Global, Module } from '@nestjs/common'; import { EmailService } from './email.service'; @Global() @Module({ providers: [EmailService], exports: [EmailService], }) export class EmailModule {} ``` /middleware/email/email.service.ts ```JS import { Injectable } from '@nestjs/common'; import { createTransport, Transporter } from 'nodemailer'; @Injectable() export class EmailService { transporter: Transporter; constructor() { this.transporter = createTransport({ host: 'smtp.qq.com', port: 587, secure: false, auth: { user: '625151917@qq.com', pass: 'gevnvqbuymnfbeia', }, }); } async sendMail({ to, subject, html }) { await this.transporter.sendMail({ from: { name: 'Nest-template 项目', address: '625151917@qq.com', }, to, subject, html, }); } } ``` ### 10. 封装 svg 为中间件 ``` bash # 生成模块 nest g module svg nest g service svg ``` ``` bash npm install svg-captcha --save ``` /middleware/svg/svg.module.ts ```JS import { Global, Module } from '@nestjs/common'; import { SvgService } from './svg.service'; @Global() @Module({ providers: [SvgService], exports: [SvgService], }) export class SvgModule {} ``` /middleware/svg/svg.service.ts ```JS import { Inject, Injectable } from '@nestjs/common'; import * as svgCaptcha from 'svg-captcha'; import { RedisService } from '../redis/redis.service'; @Injectable() export class SvgService { @Inject(RedisService) private redisService: RedisService; async sendSvg() { const captcha = svgCaptcha.create({ ignoreChars: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', size: 6, // 验证码长度 noise: 1, // 干扰线条数目 }); // 图片验证码 文字 const text = captcha.text; // 图片验证码 const svg = captcha.data; await this.redisService.set(`svg_${text}`, captcha.text, 2 * 60); return svg; } } ``` ### 11. 封装 minio 为中间件 ``` bash # 生成模块 nest g module minio ``` ``` bash npm install --save minio ``` /middleware/minio/minio.module.ts ```JS import { Global, Module } from '@nestjs/common'; import * as Minio from 'minio'; @Global() @Module({ providers: [ { provide: 'MINIO_CLIENT', async useFactory() { const client = new Minio.Client({ endPoint: 'localhost', port: 9000, useSSL: false, accessKey: 'q8uTzgDj84Fq5jrgmBy2', secretKey: 'yXneqpBC7Nt6NIwYzEF9BrtxyZV6odkmTNRlVFRj', }); return client; }, }, ], exports: ['MINIO_CLIENT'], }) export class MinioModule {} ``` ### 12. 封装 winston 日志 为中间件 ``` bash npm install --save nest-winston winston npm install --save winston-daily-rotate-file ``` /middleware/winston/winston.module.ts ```JS import { Global, Module } from '@nestjs/common'; import { WinstonService } from './winston.service'; import { utilities, WinstonModule } from 'nest-winston'; import * as winston from 'winston'; import 'winston-daily-rotate-file'; import { format } from 'winston'; @Global() @Module({ imports: [ WinstonModule.forRootAsync({ useFactory: () => ({ level: 'debug', format: format.combine( format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), format.prettyPrint(), ), transports: [ // new winston.transports.Console(), new winston.transports.DailyRotateFile({ level: 'debug', dirname: 'daily-log', filename: 'log-%DATE%.log', datePattern: 'YYYY-MM-DD', zippedArchive: true, maxSize: '20m', maxFiles: '7d', format: format.combine( format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss', }), format.json(), ), }), new winston.transports.Console({ format: winston.format.combine( winston.format.timestamp(), utilities.format.nestLike(), ), }), ], }), }), ], providers: [WinstonService], exports: [WinstonService], }) export class WinstonsModule {} ``` /main.ts ```JS import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; async function bootstrap() { ... app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)); ... } bootstrap(); ``` /middleware/prisma/prisma.service.ts ```JS import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { PrismaClient, Prisma } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { private readonly logger = new Logger('Prisma'); ... async onModuleInit() { ... // 监听 Prisma 的查询事件,并使用显式类型 const neverValue: never = 'query' as unknown as never; this.$on(neverValue, (event: Prisma.QueryEvent) => { this.logger.log(`Mapped {Query: ${event.query} } SQL`); this.logger.log( `Mapped {Params: ${event.params} ,Duration: ${event.duration}ms} Params`, ); }); } } ``` ### 13. 多版本共存开发 `url 更推荐` ```JS async function bootstrap() { ... app.enableVersioning({ type: VersioningType.URI, }) ... } bootstrap(); ``` ```JS import { Version } from '@nestjs/common'; @Controller({ path: 'inform', version: '1' }) export class InformController { constructor(private readonly informService: InformService) {} @Get() @Version('1') async list1() { return 'v1'; } @Get() @Version('2') async list1() { return 'v2'; } } // 使用 // 请求的时候url 带上 http://localhost:3000/v2/xxx ``` `请求头中` /mian.ts ```JS async function bootstrap() { ... app.enableVersioning({ type: VersioningType.HEADER, header: 'version' }) ... } bootstrap(); ``` ```JS import { Version } from '@nestjs/common'; @Controller({ path: 'inform', version: '1' }) export class InformController { constructor(private readonly informService: InformService) {} @Get() @Version('1') async list1() { return 'v1'; } @Get() @Version('2') async list1() { return 'v2'; } } // 使用 // 请求的时候在headers里带上 key:version value 2 ```