从0死磕全栈第十天:nest.js集成prisma完成CRUD

量子纠缠般的恋爱现状:

秒回消息嫌粘人,已读不回说渣男;

主动觉得掉价,被动又怕错过

从0死磕全栈系列

从0死磕全栈第1天:从写一个React的hello world开始

手把手教你配置Vite:打造极速企业级前端开发环境

从0 死磕全栈第3天:React Router (Vite + React + TS 版):构建小时站实战指南

从0死磕全栈第4天:使用React useState实现用户注册功能

从0死磕全栈第五天:React 使用zustand实现To-Do List项目

从0死磕全栈第6天:React useEffect副作用起了大作用之实现购物车功能

从0死磕全栈第 7 天:一文搞定 React 组件通信技巧

从0死磕全栈第八天:使用nest.js五分钟搭建后端开发环境

从0死磕全栈第九天:Trae AI IDE一把梭,使用react-query快速打通前后端接口调试

前面一节nest.js已经介绍了如何使用nest.js来定义简单的接口,下面继续来讲nest配合数据库实现后端对数据库的访问。

和其他的后端框架一样,要访问数据库都需要通过ORM框架来操作,比如Java里面有mybatis,jpa等多个ORM框架,go有gorm,那么node里面,基于ts的orm框架有prisma,typeorm。

这里以prisma为例来讲解如何快速搭建用户的CRUD功能。

prisma简介

Prisma 是一款为现代应用开发打造的强大 ORM(对象关系映射)工具,尤其适合 Node.js 和 TypeScript 项目。它通过直观的 schema 文件定义数据模型,自动生成类型安全的数据库客户端,彻底告别手写 SQL 的繁琐与类型错误。

与传统 ORM 不同,Prisma 采用声明式语法,让开发者只需关注数据结构而非底层实现。其自动迁移工具可轻松管理数据库变更,支持 PostgreSQL、MySQL、SQLite 等多种数据库。无论是简单的 CRUD 操作还是复杂查询,Prisma 都能提供简洁优雅的 API,大幅提升开发效率。

凭借类型安全、开发体验友好、性能出色等特性,Prisma 已成为全栈开发者的热门选择,尤其在 Next.js、NestJS 等现代框架生态中广泛应用。

第一步,安装依赖

  
npm install prisma @prisma/client

这里安装了prisma,prisma-client 2个框架, prisma-client就是我们开发者需要使用的工具来实现对数据库的crud。

第二步,生成prisma文件

  
npx prisma init

这个命令会在nest项目里面生成prisma文件夹存放数据库schema和迁移等文件,还有.env环境配置文件。

.env

  
# Environment variables declared in this file are automatically made available to Prisma.  
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema  
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.  
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings  
DATABASE_URL="file:./dev.db"

schema.prisma

  
generator client {  
  provider = "prisma-client-js"  
}  
  
datasource db {  
  provider = "sqlite"  
  url      = "file:./dev.db"  
}  
  
model User {  
  id        Int      @id @default(autoincrement())  
  email     String   @unique  
  name      String?  
  password  String  
  createdAt DateTime @default(now())  
  updatedAt DateTime @updatedAt  
}

这里为了快速使用prisma,我们选择了轻量级数据库sqlite。3分钟就可以快速搭建好一个数据库。

接着执行迁移命令

  
npx prisma migrate dev --name init

它会自动创建dev.db数据库文件,并且把定义的表结构加进去。

  
npx prisma generate

这个命令会生成client。

第三步,创建prisma服务

使用如下2个命令创建prisma模块

  
nest generate module prisma  
nest generate service prisma
  
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';  
import { PrismaClient } from '@prisma/client';  
  
@Injectable()  
export class PrismaService extends PrismaClient   
  implements OnModuleInit, OnModuleDestroy {  
  
  constructor() {  
    super({  
      log: ['query', 'info', 'warn', 'error'],  
    });  
  }  
  
  async onModuleInit() {  
    await this.$connect();  
  }  
  
  async onModuleDestroy() {  
    await this.$disconnect();  
  }  
}

prisma.module.ts

  
import { Global, Module } from '@nestjs/common';  
import { PrismaService } from './prisma.service';  
  
@Global()  
@Module({  
  providers: [PrismaService],  
  exports: [PrismaService],  
})  
export class PrismaModule {}

第四步,使用prisma

现在可以在users模块里面使用prisma

  
import { Injectable } from '@nestjs/common';  
import { PrismaService } from 'src/prisma/prisma.service';  
import { CreateUserDto } from './dto/create-user.dto';  
@Injectable()  
export class UsersService {  
    constructor (private prisma:PrismaService){}  
    async create(user:CreateUserDto){  
        console.log("create user")  
        return this.prisma.user.create({data:user})  
    }  
    async findAll(){  
        return this.prisma.user.findMany()  
    }  
}

这里保存用户的时候创建了一个接受前端数据的dto

  
export class CreateUserDto {  
    email:string;  
    name:string;  
    password:string;  
}

写完了之后,我们需要快速测试接口,这里我推荐安装一个vscode插件restclient

picture.image

目前唯一的缺点是还没有插件可以实现自动识别nest接口生成对应的http文件,需要手动去写.http文件

下面是2个简单的例子

  
POST http://localhost:3000/users  
Content-Type: application/json  
{  
 "email":"er11",  
 "name":"hu",  
 "password":"rt"  
}  
###  
GET  http://localhost:3000/users

到此我们就学会了使用nest+prisma写CRUD了。

下面来实现一个分页查询的接口

  
    async findByPageWithName(  
        page: number = 1,  
        pageSize: number = 10,  
        name?: string  
    ) {  
        const skip = (page - 1) * pageSize;  
        const take = pageSize;  
        // 构建查询条件  
        const where = name  
            ? {  
                OR: [  
                    { name: { contains: name } },   
                ]  
            }  
            : {};  
        const [users, total] = await Promise.all([  
            this.prisma.user.findMany({  
                skip,  
                take,  
                where,  
            }),  
            this.prisma.user.count({ where }),  
        ]);  
        return {  
            users,  
            total,  
            page,  
            pageSize,  
            totalPages: Math.ceil(total / pageSize),  
        };  
    }

controller

  
 // 分页查询  
    @Get('page')  
    async getByPage(  
        @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,  
        @Query('pageSize', new DefaultValuePipe(10), ParseIntPipe) pageSize: number,  
        @Query('name') name?: string,  
    ) {  
        return this.userService.findByPageWithName(page, pageSize, name);  
    }

这里用到了@Query装饰器,如果了解Spring,可以类比为Spring里面的注解。

装饰器本质是一种特殊函数,它能给类、方法、参数添加“元数据”(额外信息),告诉NestJS如何处理这些代码。简单说,装饰器就像“标签”,让框架知道:“这个类是控制器”“这个方法处理GET请求”“这个参数来自查询字符串”。

装饰器本质是一种特殊函数,它能给类、方法、参数添加“元数据”(额外信息),告诉NestJS如何处理这些代码。简单说,装饰器就像“标签”,让框架知道:“这个类是控制器”“这个方法处理GET请求”“这个参数来自查询字符串”。

我们会发现返回的时间类似这样的2025-08-09T05:54:14.508Z,不是我们希望的 yyyy-MM-dd HH:mm:ss。这个时候就需要引入拦截器的概念。

拦截器简介

Nest拦截器是框架里的一种功能,能在请求处理前后执行额外逻辑(如日志、数据转换、异常处理),就像‘过滤器’,不改变核心业务,却能扩展功能,让代码更简洁易维护。

第一步,创建拦截器

在src下面创建一个目录 interceptors,定义拦截器的业务

  
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';  
import { Observable } from 'rxjs';  
import { map } from 'rxjs/operators';  
@Injectable()  
export class TransformInterceptor implements NestInterceptor {  
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {  
    return next.handle().pipe(  
      map((data) => this.transformDates(data)),  
    );  
  }  
  private transformDates(data: any): any {  
    if (!data) return data;  
    // 如果是数组,递归处理每个元素  
    if (Array.isArray(data)) {  
      return data.map((item) => this.transformDates(item));  
    }  
    // 如果是对象,处理每个属性  
    if (typeof data === 'object') {  
      // 遍历对象的所有属性  
      for (const key in data) {  
        if (data.hasOwnProperty(key)) {  
          // 处理日期类型  
          if (data[key] instanceof Date) {  
            data[key] = this.formatDate(data[key]);  
          }  
          // 如果属性值是对象或数组,递归处理  
          else if (typeof data[key] === 'object' && data[key] !== null) {  
            data[key] = this.transformDates(data[key]);  
          }  
        }  
      }  
    }  
    return data;  
  }  
  // 格式化日期为UTC+8时区的yyyy-MM-dd HH:mm:ss格式  
  private formatDate(date: Date): string {  
    // 设置时区为UTC+8  
    const utc8Date = new Date(date.getTime() + 8 * 60 * 60 * 1000);  
    return utc8Date.toISOString()  
      .replace(/T/, ' ')       // 替换T为空格  
      .replace(/\..+/, '')    // 删除小数点及之后的部分  
      .slice(0, 19);          // 截取到秒  
  }  
}

在app.module.ts引入拦截器

  
import { Module } from '@nestjs/common';  
import { AppController } from './app.controller';  
import { AppService } from './app.service';  
import { PrismaModule } from './prisma/prisma.module';  
import { UsersModule } from './users/users.module';  
import { APP_INTERCEPTOR } from '@nestjs/core';  
import { TransformInterceptor } from './interceptors/transform.interceptor';  
@Module({  
  imports: [PrismaModule, UsersModule],  
  controllers: [AppController],  
  providers: [  
    AppService,  
    {  
      provide: APP_INTERCEPTOR,  
      useClass: TransformInterceptor,  
    },  
  ],  
})  
export class AppModule {}
0
0
0
0
评论
未登录
暂无评论