首页/文章/nestjs

NestJS + Prisma 增删改查实践

2024-09-10
13705 分钟
...

官网链接:NestJSPrisma

当前版本

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 中通过 ApiTagsApiOperation 给每个模块和接口定义备注。刷新一下 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 filtersNestJS 提供的错误捕获器。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.jsonscript 中增加一个指令

{
    "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...
学习
AI2026-09-01
Deep Agents 01:何为 Agent Harness,以及如何开始
1、本篇任务:完成一份多步骤、带证据的技术调研 普通客服 Agent 的问题短、工具少、输出即时。技术调研或编码任务会持续很久,产生计划、搜索结果、文件和中间结论。Deep Agents 在 LangChain/LangGraph 之上预装规划、虚拟文件系统、上下文压缩和子 Agent,适合这类开放任务。 本课让 Agent 比较两种向量数据库,并交付一份可验证报告。 2、什么时候需要 Deep Agent 满足以下两项以上再考虑:任务...
学习
AI2026-09-01
Deep Agents 02:子 Agent、虚拟文件系统与长期记忆
1、本篇任务:让主管只看结论,让子 Agent 处理细节 技术调研会产生几十次搜索和大量文件。如果全部进入主管上下文,真正的目标会被噪音淹没。本课用两个子 Agent: 收集证据, 检查结论;主管负责计划与最终合成。 2、什么时候委派,什么时候直接调用工具 适合委派:子任务有多步;需要专门提示或工具;会产生大量中间结果;只需返回有限结论。不适合:一步查询;主管需要全部中间上下文;协调成本超过任务本身。 3、配置专门子 Agent Pyt...
学习
AI2026-09-01
Deep Agents 03:生产化、Sandbox、权限与上线验收
1、本篇任务:让 Deep Agent 在隔离环境中分析代码 只读研究 Agent 风险有限;编码 Agent 需要读写文件、安装依赖和执行测试。本课不讲如何让模型写更漂亮的代码,只讲执行环境、权限、恢复和上线验收。 2、先做威胁模型 资产包括源代码、用户文件、云凭证、生产网络和发布权限;攻击入口包括用户消息、仓库内容、网页、依赖包、MCP 返回和命令输出。 Prompt injection 不是靠一句 system prompt 解决...
学习
AI2026-09-01
LangChain 01:全景、原理与学习路线
1、本篇学完要得到什么 这一篇只解决三个问题:LangChain 到底负责什么;它与 LangGraph、Deep Agents、LangSmith 是什么关系;后面应按什么顺序学习。 贯穿整套课程的项目是“退款政策与订单助手”。它最终能够:回答知识库中的退款规则;查询当前用户的订单;生成结构化答复;对真正的退款操作进行人工审批;断线后恢复;通过评测后发布。 先记住一句话: 模型负责理解与生成,应用负责数据、权限、状态和副作用。 如果把...
学习
AI2026-09-01
LangChain 02:模型、消息与结构化输出
1、本篇任务:让模型输出成为程序可以依赖的合同 上一课只证明 Agent 能运行。本课暂时不接业务工具,只完成一个“客服分诊器”:输入用户问题,输出意图、紧急程度、是否需要人工和给用户的答复。 本课的核心不是学更多模型参数,而是理解三层合同:消息决定模型看到了什么;schema 决定程序期待什么;业务校验决定结果是否真的可用。 2、消息不是一段字符串,而是一条执行记录 一次工具型对话通常包含四种消息: | 类型 | 由谁产生 | 作用...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录