首页/文章/nestjs

NestJS + Prisma 增删改查实践

2024-09-10
1370 字约 5 分钟
...

官网链接:NestJS,Prisma

当前版本

node: 20.11.0 @nestjs/cli: 10.3.2 prisma: 5.18.0

初始化项目

Set up Prisma NestJS官网

安装NestJS

npm i -g @nestjs/cli
nest new project-name
cd project-name

这里需要选择 package manager,本文是用 npm 作为包管理器的

安装Prisma

npm i prisma --save-dev
npx prisma init

执行完上面两条命令之后,会发现多了一个 prisma 的文件夹和 .env 文件

  • prisma : 指定数据库连接,以及数据库 `schema
  • .env: 用来配置环境变量

在VSCODE安装一下prisma插件,这样prisma.schema就可以高亮和格式化了

接下来修改一下连接数据库的配置,本文连接的是 MySQL,其他数据库可以去官方文档进行查看。 Prisma连接关系型数据库

先修改 .env 的环境变量

// 数据库类型://用户名:密码@ip:端口/数据库名
DATABASE_URL="mysql://root:123456@localhost:3306/demo"

然后修改 prisma/prisma.schema,建立一个User表。

prisma.schema
generator client {
    provider = "prisma-client-js"
}

datasource db {
    provider = "mysql"
    url = env("DATABASE_URL")
}

model User {
    id         Int        @id @default(autoincrement())
    name       String     @unique
    password   String
    avatar     String?
    createAt   DateTime   @default(now())
    updateAt   DateTime   @updatedAt
}

配置好 prisma.schema 之后,执行下面的命令来修改数据库。每次执行 prisma 都会生成一个变更记录,可以在 prisma/migrations 文件夹下面查看。

npx prisma migrate dev --name init

执行完之后,再去查看数据库结构,User 表已经建立好了。到这里,开发环境就算搭建好了。

prisma.service.ts

把 prisma 注入到 NestJS,需要在 src 目录下面建一个新文件

import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
  
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
    async onModuleInit() {
        await this.$connect();
    }
}

快速生成增删改查代码

NestJS 提供了快速生成增删改查的工具,可以通过 nest -h 来查看详细指令

// g = generate res=resource
nest g res user /module

执行完之后可以看到,在 src 下面多了 src/module/user 文件夹。

如果有修改过 src/app.module.ts 的文件结构, 先修改下 src/app.module.ts 文件,把 user 模块引入进来。

如果没有,NestJS 会自动帮你引入进来。

下面是实现增删改查的必要文件

user.module.ts

需要把 prismaService 放入 providers 中注入

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';

import { PrismaService } from '../../prisma.service';

@Module({
    controllers: [UserController],
    providers: [UserService, PrismaService],
})

export class UserModule {}
dto/create-user.dto.ts
export class CreateUserDto {
    name: string;
    password: string;
    avatar?: string;
    id?: number;
}
user.controller.ts
import {
    Controller,
    Get,
    Post,
    Body,
    Patch,
    Param,
    Delete,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { Prisma } from '@prisma/client';

@Controller('user')
export class UserController {
    constructor(private readonly userService: UserService) {}

    @Post()
    create(@Body() createUserDto: CreateUserDto) {
        return this.userService.createUser(createUserDto);
    }
    
    @Get()
    findAll(params: {
        skip?: number;
        take?: number;
        cursor?: Prisma.UserWhereUniqueInput;
        where?: Prisma.UserWhereInput;
        orderBy?: Prisma.UserOrderByWithRelationInput;
    }) {
        return this.userService.users(params);
    }

    @Get(':id')
    findOne(@Param('id') id: string) {
        return this.userService.user({ id: +id });
    }

    @Patch(':id')
    update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
        return this.userService.updateUser({
            where: { id: +id },
            data: updateUserDto,
        });
    }


    @Delete(':id')
    remove(@Param('id') id: string) {
        return this.userService.deleteUser({ id: +id });
    }
}
user.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma.service';
import { User, Prisma } from '@prisma/client';

@Injectable()
export class UserService {
    constructor(private prisma: PrismaService) {}
    
    async user(
        userWhereUniqueInput: Prisma.UserWhereUniqueInput,
    ): Promise<User | null> {
        return this.prisma.user.findUnique({
            where: userWhereUniqueInput,
            });
    } 
    
    async users(params: {
        skip?: number;
        take?: number;
        cursor?: Prisma.UserWhereUniqueInput;
        where?: Prisma.UserWhereInput;
        orderBy?: Prisma.UserOrderByWithRelationInput;
    }): Promise<User[]> {
        const { skip, take, cursor, where, orderBy } = params;
        return this.prisma.user.findMany({
            skip,
            take,
            cursor,
            where,
            orderBy,
        });
    }
    
    
    async createUser(data: Prisma.UserCreateInput): Promise<User> {
        return this.prisma.user.create({
            data,
        });
    }
    
    async updateUser(params: {
        where: Prisma.UserWhereUniqueInput;
        data: Prisma.UserUpdateInput;
    }): Promise<User> {
        const { where, data } = params;
        return this.prisma.user.update({
            data,
            where,
        });
    }
    
    async deleteUser(where: Prisma.UserWhereUniqueInput): Promise<User> {
        return this.prisma.user.delete({
            where,
        });
    }
}

最后执行 npm run start:dev 就可以启动了,默认端口3000。

npm run start:dev

开启Swagger、开启CORS

NestJS + Swagger 官网

安装 @nestjs/swagger

npm i --save @nestjs/swagger

在 NestJS 中,只需要在 main.js 中配置几行就能启用。

import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';




const config = new DocumentBuilder()
    .setTitle('NestjsAPI')
    .setDescription('The Nestjs API description')
    .setVersion('1.0')
    .addBearerAuth()
    .build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('swaggerApiDocs', app, document);

配置好 main.ts 之后,执行 npm run start:dev。等服务启动完访问 http://localhost:3000/swaggerApiDocs#/ 就可以看到 swagger 的页面了。

但是这里还是写着default,且没有任何注释说明。可以在 user.controller.ts 中通过 ApiTags 和 ApiOperation 给每个模块和接口定义备注。刷新一下 swagger 的页面就能看到备注已经生效了。

import { ApiOperation, ApiTags } from '@nestjs/swagger';

@ApiTags('User')
@Controller('user')
export class UserController {

    

    @ApiOperation({ summary: 'Create user' })
    @Post()
    create(@Body() createUserDto: CreateUserDto) {
        return this.userService.createUser(createUserDto);
    }
    
    
}

使用 swagger 来进行调试,会发现请求的参数是空,每次调用都需要手动输入参数。这时候可以配置 dto/create-user.dto.ts 来告诉 swagger,传参的格式是什么。配置完后,再刷新 swagger ,就能看到 Request body 里面的参数了。

import { ApiProperty } from '@nestjs/swagger';

export class CreateUserDto {
    @ApiProperty({ required: true })
    name: string;
    
    @ApiProperty({ required: true })
    password: string;
    
    @ApiProperty()
    avatar?: string;
    
    @ApiProperty()
    id?: number;
}

CORS NestJS官网

如果进行前后端联调,需要开启 CORS,开启 CORS 的方式也很简单,只需要在 main.ts 增加下面的配置就行了。

app.enableCors({
    origin: true,
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
    allowedHeaders: 'Content-Type, Accept, Authorization',
    credentials: true,
});

环境变量、连接Redis

安装prisma的时候会生成 .env 文件,这里用 dotenv 的方式来实现环境变量的方法。\

npm i --save @nestjs/config

然后可以定义属于你的环境变量文件,比如说 .env.production

新建一个 .env.production 文件来实验一下,加一个TEST_ENV 作为变量

// .env.production
// 修改成生产的数据库连接url
DATABASE_URL="mysql://root:123456@localhost:3306/demo"

TEST_ENV="production"

同时增加 .env.test 文件

// .env.test
DATABASE_URL="mysql://root:123456@localhost:3306/demo"

TEST_ENV="test"

安装 dotenv-cli

npm i --save-dev dotenv-cli

修改package.json, 让启动命令增加dotenv来制定要加载的命令

{
    "scripts": {
        "build": "nest build",
        "start:dev": "dotenv -e .env.test -- nest start --watch",
        "start:prd": "dotenv -e .env.production -- node dist/main",
    },
}

需要在 app.module.ts 中,引入 ConfigModule 并将其配置成全局共享。这里同时展示一下,如何连接 Redis。连接 Redis 需要先安装 ioredis 包。

npm i ioredis
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RedisModule } from '@nestjs-modules/ioredis';

@Module({
    imports: [
        ConfigModule.forRoot({
            
            isGlobal: true,
            
            
        }),
        
          * 启用redis缓存模块
          */
        RedisModule.forRootAsync({
            useFactory: (configService: ConfigService) => {
                const redis = {
                    password: configService.get('REDIS_PASSWORD'),
                    host: configService.get('REDIS_HOST'),
                    port: configService.get('REDIS_PORT'),
                    db: configService.get('REDIS_DB'),
                };

                return {
                    type: 'single',
                    url: `redis://:${redis.password}@${redis.host}:${redis.port}/${redis.db}`,
                };
            },
            inject: [ConfigService],
        }),
    ]
})
export class AppModule {}

如果是在 service 或者 controller 中使用, 可以在 constructor 里面引入,和其他service的使用方法一样。

这里测试一下前面配置的 .env.test 中的 TEST_ENV 究竟有没有生效,修改一下 app.service.ts。

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class AppService {
    constructor(private configService: ConfigService) {}
    
    getHello(): string {
        return 'Hello World!' + this.configService.get('TEST_ENV');
    }
}

验证效果:

  • .env.test测试 -> npm run start:dev -> localhost:3000
  • .env.procution 测试 -> npm run build -> npm run start:prd -> 浏览器输入 localhsot:3000

参数类型校验

Class validator NestJS官网

可以使用 class-validator 插件来对传入参数进行类型校验。

npm i --save class-validator class-transformer

在 main.ts 里面修改以下代码

import {
    ValidationPipe,
    BadRequestException,
    HttpStatus,
} from '@nestjs/common';
import { ValidationError } from 'class-validator';



app.useGlobalPipes(
    new ValidationPipe({
    
    exceptionFactory: (errors: ValidationError[]) => {
        const message = Object.values(errors[0].constraints!)[0];
        return new BadRequestException({
            message,
            code: HttpStatus.BAD_REQUEST,
        });
    },
    }),
);


在 dto/create-user.dto.ts 里,根据需求放入相对应的注解。这里演示几个最简单的

import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNumber, IsOptional, IsString } from 'class-validator';

export class CreateUserDto {
    @ApiProperty({ required: true })
    @IsEmail()
    name: string;
    
    @ApiProperty({ required: true })
    @IsString()
    password: string;
    
    @ApiProperty()
    @IsString()
    @IsOptional()
    avatar?: string;
    
    @ApiProperty()
    @IsNumber()
    @IsOptional()
    id?: number;
}

这里用了 IsEmail 来举例,只要传进来的 name 不符合邮箱的规范,那么就会返回格式错误。

如果是 Get 请求,因为传入后端的都是字符串,需要在 class-validator 里面将数值进行二次转换。

@Type(() => Number)
id?: number;

格式化返回

先写一个返回类。

import { ApiProperty } from '@nestjs/swagger';

export const SUCCESS_CODE = 200;

export class ResultData {
    constructor(
        code = SUCCESS_CODE,
        message?: string,
        data?: any,
    ) {
        this.code = code;
        this.message = message;
        this.data = data || null;
    }
    
    @ApiProperty({ type: 'number', default: SUCCESS_CODE })
    code: number;
    
    @ApiProperty({ type: 'string', default: 'ok' })
    message?: string;
    
    data?: any;
    
    static ok(message?: string, data?: any): ResultData {
        return new ResultData(SUCCESS_CODE, message, data);
    }
    
    static fail(code: number, message?: string, data?: any) {
        return new ResultData(code, message, data);
    }
}

然后在每个 controller 的返回里面加上 ResultData.ok(...) 或者 ResultData.fail(...),类似下面这个案例

@ApiOperation({ summary: 'Create user' })
@Post()
async create(@Body() createUserDto: CreateUserDto) {
    const result = await this.userService.createUser(createUserDto);
    return ResultData.ok('success', result);
}

Exception filters 异常处理

Exception filters 官网

Exception filters 是 NestJS 提供的错误捕获器。NestJS 在运行时会遇到很多种不同的错误,需要对不同类型的错误进行错误处理。

如果是 Prisma 产生的错误,它会直接返回,会导致默认格式不一样,导致前端处理不了。

还有 class-validaor 的错误处理,也需要对其进行错误的特殊处理

快速创建 filters 文件

nest g filter global /common/filter


nest g filter prisma /common/filter

prisma.filter.ts

import {
    ArgumentsHost,
    Catch,
    ExceptionFilter,
    HttpStatus,
} from '@nestjs/common';
import { Prisma } from '@prisma/client';

@Catch(Prisma.PrismaClientKnownRequestError)
export class PrismaFilter implements ExceptionFilter {
    catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse();
        response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
            code: exception.code,
            message: exception.message,
        });
    }
}

global.filter.ts

import {
    ArgumentsHost,
    BadRequestException,
    Catch,
    ExceptionFilter,
    HttpException,
    HttpStatus,
} from '@nestjs/common';

@Catch()
export class GlobalFilter implements ExceptionFilter {
    catch(exception: any, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse();
        const status =
            exception instanceof HttpException
            ? exception.getStatus()
            : HttpStatus.INTERNAL_SERVER_ERROR;
        if (exception instanceof BadRequestException) {
            response.status(status).json(exception.getResponse());
        } else {
            response.status(status).json({
                code: status,
                message: `Service Error: ${exception}`,
            });
        }
    }

}

接下来配置 main.ts

import { GlobalFilter } from './common/filter/global/global.filter';
import { PrismaFilter } from './common/filter/prisma/prisma.filter';




app.useGlobalFilters(new GlobalFilter());
app.useGlobalFilters(new PrismaFilter());


全局守卫、jwt校验

全局守卫意味着每个请求到达 controller 之前,会经过全局守卫。全局守卫比较适合做权限校验,将不符合条件的用户请求全部拦截。

JWT NestJS 官网

安装 @nest/jwt

npm i --save @nestjs/jwt

在 module/user.module.ts 中注册 jwt 模块

import { JwtModule } from '@nestjs/jwt';
  
@Module({
    imports: [
        JwtModule.register({
            global: true,
            secret: 'SECRET',
            signOptions: { expiresIn: '7d' },
        }),
    ],

})

在 service 生成 token

const access_token = await this.jwtService.signAsync(payload);

使用 nest 快速生成 guard 文件

nest g gu auth /common/guard

auth.guard.ts

import {
    Injectable,
    CanActivate,
    HttpException,
    HttpStatus,
    ExecutionContext,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthGuard implements CanActivate {
    
    async canActivate(context: ExecutionContext): Promise<boolean> {
        
        
        const request = context.switchToHttp().getRequest();
        
        
        let token = context.switchToRpc().getData().headers.authorization;
        token = this.extractTokenFromHeader(token);
        
        
        if (token) {
            try {
                
                const jwtService = new JwtService();
                const res = jwtService.verify(token, { secret: 'SECRET' });
                
                
                request['user'] = res;
                
                return res;
            } catch (e) {
                throw new HttpException(
                    '没有授权访问,请先登陆',
                    HttpStatus.UNAUTHORIZED,
                );
            }
        } else {
            
            if (this.hasUrl(this.urlList, request.url)) {
                return true;
            }
            throw new HttpException(
                '没有授权访问,请先登陆',
                HttpStatus.UNAUTHORIZED,
            );
        }
    }
    
    
    private urlList: string[] = ['/', '/user'];
    
    
    private hasUrl(urlList: string[], url: string): boolean {
        let flag: boolean = false;
        if (urlList.indexOf(url.split('?')[0]) >= 0) {
            flag = true;
        }
        return flag;
    }
    
    private extractTokenFromHeader(authorization: string): string | undefined {
        const [type, token] = authorization?.split(' ') ?? [];
        return type === 'Bearer' ? token : undefined;
    }
}

在 main.ts 中注册全局守卫,注册完成后,访问非白名单的url,就会报没有登录的错误。

import { AuthGuard } from './common/guard/auth/auth.guard';



app.useGlobalGuards(new AuthGuard());


生产环境

如果生产环境走的是服务器上的CICD,在 npm install 之后需要执行以下 npx prisma generate 来生成必要的 typescript 文件。

在 package.json 的 script 中增加一个指令

{
    "prisma:generate": "dotenv -e .env.production -- npx prisma generate"
}

这里贴上一个部署用的 Dockerfile

# node镜像
FROM node:20-alpine

ENV NODE_VERSION 20.15.1
# Set working dir inside base docker image
WORKDIR /usr/src/app
# Copy our project files to docker image
COPY . .
# npm 源,选用国内镜像源以提高下载速度
RUN npm config set registry http://mirrors.cloud.tencent.com/npm/
RUN npm install -g node-gyp
# Install project dependencies
RUN npm install
# Generate Prisma client files
RUN npm run prisma:generate
RUN npm run build

EXPOSE 3000

CMD ["npm", "run","start:prd"]

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
nestjs2024-07-10
nestjs的核心:利用装饰器实现依赖注入,实现类与类的解耦
在上一篇文章《nestjs系列三:nestjs如何通过Module组织代码结构?》( %E4%B8%AD%EF%BC%8C%E6%88%91%E4%BB%AC%E7%9F%A5%E9%81%93%E4%BA%86%60@Module%60%EF%BC%8C%60@Controller%60%EF%BC%8C "https://juejin.cn/post/7388470580464844852)%E4%B8%AD%EF%BC%8C%E6...
学习
http-网络2026-10-15
OAuth 2.0是什么,以及如何正确接入
一、先从一个需求理解 OAuth 2.0 假设你开发了一个相册整理工具,用户希望它读取某个云盘里的照片。 把云盘密码交给工具,会带来几个问题:工具拿到的权限太大,密码可能泄漏,用户也难以只撤销这个工具的访问。 OAuth 2.0 提供一套 委托授权机制 :用户在云盘自己的页面确认授权,工具取得一个有权限范围和有效期的访问令牌,再用它调用云盘 API。用户的云盘密码不交给工具。 可以把它理解为:给工具一张“限定用途的通行证”。但令牌泄漏同...
学习
前端2026-10-15
前端动画全景指南
一、前端动画到底是什么 前端动画,就是让画面随时间、用户输入或页面状态连续变化。变化的对象可以是按钮的位置、弹窗的透明度、SVG 的线条、Canvas 里的粒子,也可以是三维场景中的镜头。 先把三个问题分开: | 问题 | 常见答案 | | | | | 画什么? | DOM/CSS、SVG、Canvas 2D、GPU 渲染的场景、预制动画素材 | | 怎样变化? | CSS 过渡/关键帧、Web Animations API、逐帧计算、...
学习
flutter2026-10-14
Flutter Android 正式签名实战:生成密钥、配置构建与验证 APK
Flutter Android 正式签名实战:生成密钥、配置构建与验证 APK Android 正式签名不需要购买证书。开发者可以自己生成密钥,用它给 APK 签名;密钥的用途是建立稳定的应用身份。签名有效并不等于应用已经通过商店审核,也不等于应用功能已完成真机验收。 本文面向 Flutter Android 项目,介绍从开发签名切换到独立正式签名的完整流程。应用名称、目录、包名和证书信息均使用通用示例,不包含真实项目密码、私钥、签名指...
学习
flutter2026-10-14
Flutter 新版本发布实战:完整安装包与 Shorebird 热更新
一、先判断该发完整包还是补丁 | 这次改动 | 发布方式 | | | | | Dart 文案、Flutter 页面布局、业务逻辑修复 | 通常可以发布补丁 | | 纯 Dart 依赖变化 | 可以尝试补丁构建,仍需验证兼容性 | | Kotlin、Java、Swift、Objective C 或原生构建配置变化 | 发布完整包 | | 插件的原生实现变化、新增原生插件、权限变化 | 发布完整包 | | 新增或修改打包图片、字体、应用图标...
学习
react2026-09-15
React 19.3 新特性总结
React 19.3 已发布到 npm,核心是把去年以实验 API 形式公布的 View Transitions 和 Fragment Refs 正式转正,同时新增了几个 DOM 与 RSC 相关的 API。下面按特性整理。 一、View Transitions(视图过渡动画,正式发布) 是什么 是基于浏览器原生 封装的组件。把你想要加动画的元素包起来,当元素 进入、退出、移动或尺寸变化 时,React 自动播放过渡动画。 基本用法 只...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录