前言 NestJS 是一个高效、可扩展的 Node.js 框架,它提供了一套成熟的模式和功能,帮助开发者构建可维护和可扩展的服务端应用。本教程将引导你使用 NestJS 搭建一个服务端应用,并介绍如何对接 PostgreSQL 数据库、配置环境变量以及实现用户密码加密。
初始化项目 请确保你的操作系统上安装了 Node.js(version ≥ 16)
我们采用官网提供的 Nest CLI 方式设置项目。
1 2 3 npm i -g @nestjs/cli nest new project-name --strict
要使用 TypeScript 更严格的功能集创建新项目,请将 --strict
标志传递给 nest new
命令。
目录简介 src 目录 这里是主要代码,初始化的项目打开会看到
1 2 3 4 5 src \\app.controller .spec .ts src \\app.controller .ts src \\app.module .ts src \\app.service .ts src \\main .ts
之后运行 pnpm run start:dev
。就可以访问http://localhost:3000/
会出现 hello world 的字眼。
package.json 这个文件前端开发都熟悉,不过多解释。主要是了解初始化的项目已经安装了什么。
分别安装了主要的代码检测和格式化插件eslint
和 prettier
1 2 3 4 5 npm run lint npm run format
nest-cli.json 这个文件主要是配置插件和库以及项目的配置,例如你需要@nestjs/graphql
插件或者自定义插件。以下是简单的教程,基本上可以不用修改文件,仅做了解。
1 2 3 4 5 6 7 8 9 10 11 12 13 { "$schema" : "<https://json.schemastore.org/nest-cli>" , "collection" : "@nestjs/schematics" , "sourceRoot" : "src" , "compilerOptions" : { "deleteOutDir" : true , "plugins" : [ "@nestjs/graphql" ] , "typeFileNameSuffix" : [ ".input.ts" , ".args.ts" ] , "introspectComments" : true } }
项目文件别名 项目内引入文件的时候,我们都会使用@
来指向src
目录。我们需要修改tsconfig.json
文件。
1 2 3 4 5 6 7 8 { "compilerOptions" : { "paths" : { "@/*" : [ "src/*" ] } } }
数据库的连接 说明以及安装 在连接数据库之前,我们需要了解数据库和 orm 之间的关系,orm 是一种数据关系映射的工具,它用作编写纯 SQL 或使用其他数据库访问工具。这样的话,我们不用关心多种数据库的连接,改变数据库版本之后要修改代码等等问题。
1 pnpm install prisma --save-dev
1 2 npx prisma // 获取命令 npx prisma init // 初始化prisma
初始化之后会得到 prisma 的文件夹(如果你初始化失败,八成是你的网络连不上国外的网络)。
新建.env
文件,这个是本地的环境变量设置
1 2 3 4 DATABASE_URL= "file:./dev.db" DATABASE_URL= "postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public" DATABASE_URL= "mysql://USER:PASSWORD@HOST:PORT/DATABASE"
prisma 配置 上面新建的环境变量文件,那么就需要 prisma 来连接数据库了。
打开prisma\\schema.prisma
文件
1 2 3 4 5 6 7 8 9 10 generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL" ) }
创建数据库 添加如下的模型到prisma\\schema.prisma
文件中
1 2 3 4 5 6 model User { id Int @id @default(autoincrement()) email String @unique name String? }
之后做如下操作,同步本地的表结构到数据库中。
1 npx prisma migrate dev --name init
后续数据库有更新,用npx prisma db push/pull
来操作。
Nestjs 上使用 安装客户端
1 pnpm install @prisma/client
安装完之后为了让项目结构更加清晰,这里新建 src/prismac 文件夹,用来存放客户端创建 prisma 的操作。
该目录下新建 prisma.service.ts
1 2 3 4 5 6 7 8 9 import { Injectable, OnModuleInit } from '@nestjs/common' ; import { PrismaClient } from '@prisma/client' ; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { async onModuleInit() { await this.$connect (); } }
之后再在该目录下新建 prisma.module.ts
1 2 3 4 5 6 7 8 import { Module } from '@nestjs/common' ; import { PrismaService } from './prisma.service' ; @Module({ providers: [PrismaService ], exports: [PrismaService ], }) export class PrismaModule {}
之后在users.module.ts
中导入
1 2 3 4 5 6 7 8 9 10 11 import { Module } from '@nestjs/common' ; import { UsersController } from './users.controller' ; import { UserService } from './users.service' ; import { PrismaService } from '@/prismac/prisma.service' ; @Module({ imports: [], controllers: [UsersController ], providers: [UserService , PrismaService ], }) export class UsersModule {}
prisma 命令说明
命令
说明
init
在应用中初始化 Prisma
generate
主要用来生成 Prisma Client
db
管理数据库的模式和生命周期
migrate
迁移数据库
studio
启动一个 Web 端的工作台来管理数据
validate
检查 Prisma 的模式文件的语法是否正确
format
格式化 Prisma 的模式文件,默认就是 prisma/schema.prisma
项目架构 这里我通过我的一个项目来分享大致的项目架构是什么样子的,我这里写一个 users 的接口。
在 src 文件夹下新建 users 文件夹。
然后新建如下文件
1 2 3 src\\users\\users.controller.ts src\\users\\users.module.ts src\\users\\users.service.ts
因为代码都是要使用到服务层,所以我们先写服务层代码
users.service.ts 编写服务代码,主要这层就是操作 orm,来实现数据库的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 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, } ); } }
users.controller.ts 这里就是调用前面写的 service 层的函数,这里主要负责请求的参数解析等,请求的连接定义等,我这里的就是/users/sign post 类型的接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { Body, Controller, Post } from '@nestjs/common' ; import { UserService } from './users.service' ; import { User as UserModel } from '@prisma/client' ; @Controller('/users' ) export class UsersController { constructor(private readonly userService: UserService) {} @Post('sign' ) async signupUser( @Body() userData: { name?: string; email: string }, ): Promise<UserModel> { return this.userService.createUser(userData); } }
users.modules.ts 这里主要是负责注册或者抛出模块,,因为 cotroller 层需要 PrismaService ,所以注册进来。让后表明 controller 层。
1 2 3 4 5 6 7 8 9 10 11 import { Module } from '@nestjs/common' ; import { UsersController } from './users.controller' ; import { UserService } from './users.service' ; import { PrismaService } from '@/prismac/prisma.service' ; @Module({ imports: [], controllers: [UsersController ], providers: [UserService , PrismaService ], }) export class UsersModule {}
最后在 app.module.ts 中导入这个 UsersModule
1 2 3 4 5 6 7 8 9 import { Module } from '@nestjs/common' ; import { UsersModule } from './users/users.module' ; @Module({ imports: [UsersModule ], controllers: [], providers: [], }) export class AppModule {}
以上就是最基础的操作,为了项目的规范化,下面还有其他层。
Dto 层
在前面的例子中,我们获取客户端的参数都是直接写在控制器内每个方法的参数中的,这样做引发的问题有:
会降低代码的可读性,一大串参数写在方法里很不优雅。
当很多方法都都需要传入相同参数时,要写很多重复代码,可维护性大大降低。
参数的有效性验证需要写在控制器内的方法中,会产生冗余代码。
DTO 层的作用就是解决上述问题的,我们用class
来处理客户端传入的参数。
新建src/users/users.dto.ts
1 2 3 4 export class UsersDto { public user: string; public email: string; }
之后在src/users/users.controller.ts
中使用
1 2 3 4 5 6 7 8 9 10 11 ...... @Controller('/users' ) export class UsersController { constructor(private readonly userService: UserService) {} @Post('user' ) async signupUser(@Body() userData: UsersDto): Promise<UserModel> { return this.userService.createUser(userData); } } //这样就解决了问题1 和问题2
使用管道验证参数的有效性 下面的需要安装包
1 pnpm add class -validator class -transformer
接下来,我们使用管道来解决第 3 个问题,在 nest 官网中,它提供了8 个开箱即用的内置管道 ,此处我们需要用它的ValidationPipe
管道来验证参数。
根据文档所述,在使用前我们需要先绑定管道,官网给出了两种方法:
绑在 controller 或是其方法上,我们使用 @UsePipes()
装饰器并创建一个管道实例,并将其传递给 Joi 验证。
在入口处将其设置为全局作用域的管道,用于整个应用程序中的每个路由处理器。
此处我们使用全局作用域的管道,修改 main.ts 文件,代码如下所示:
1 2 3 4 5 6 7 8 9 10 import { NestFactory } from '@nestjs/core' ; import { AppModule } from './app.module' ; import { ValidationPipe } from '@nestjs/common' ; async function bootstrap () { const app = await NestFactory.create(AppModule); app.useGlobalPipes(new ValidationPipe()); await app.listen(3000 ); } bootstrap();
随后,我们即可在 dto 层中使用它的相关装饰器来校验参数了
1 2 3 4 5 6 7 8 import { IsString, MinLength } from "class-validator" ; export class UsersDto { @MinLength(2 ) @IsString() public user: string; @IsString() public email: string; }
之后你测试127.0.0.1:3000/users/user
接口,输入名字就 1 个字的时候,接口会返回提示。
VO 层(返回给客户端的视图) 通常情况下,我们返回给客户端的字段是固定的,在本文前面的 controller 层中,两个方法我们都返回了code
、data
、msg
这三个字段,只是数据不同。那么我们就应该把它封装起来,将数据作为参数传入,这样就大大的提高了代码的可维护性,也就是我们所说的 VO 层。
封装工具类 我们在 src 目录下创建VO
文件夹,在其目录下创建 result.vo.ts
文件,代码如下所示:
简单创建了一个类,添加了三个字段
为每个字段写了 get 和 set 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 typescript复制代码; export class ResultVO <T> { private code!: number ; private msg!: string ; private data!: T | null ; public getCode (): number { return this .code ; } public setCode (value : number ): void { this .code = value; } public getMsg (): string { return this .msg ; } public setMsg (value : string ): void { this .msg = value; } public getData (): T | null { return this .data ; } public setData (value : T | null ): void { this .data = value; } }
随后,我们在 src 目录下创建 utils 文件夹,在其目录下创建voUtils.ts
文件,封装常用方法,便于其他层直接调用,代码如下所示:
我们封装了success
与error
方法
成功时,传入 data 进来
失败时,传入 code 与 msg 告知客户端错误原因
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 返回给调用者的视图结构 import { ResultVO } from "../VO/ResultVO" ; export class VOUtils { public static success<T>(data ?: T): ResultVO<T> { const resultVo = new ResultVO<T>(); resultVo.setCode(0 ); resultVo.setMsg("接口调用成功" ); resultVo.setData(data || null); return resultVo; } public static error(code: number, msg: string): ResultVO<null> { const resultVo = new ResultVO<null>(); resultVo.setCode(code); resultVo.setMsg(msg); return resultVo; } }
注意:success
方法支持传入的参数是任意类型的,实际的业务需求中,data 这一层会很复杂,你在实际使用时,可以根据具体的业务需求创建对应业务的 vo 类,然后对其进行实例化,为每个字段赋值。最后在调用 success 方法时将你实例化后的对象传入即可。
在业务代码中使用 随后,我们就可以在service
层来使用我们创建好的工具类了,示例代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export class UserService { ...... import { VOUtils } from '@/utils/voUtils' ; async updateUser(params: { where : Prisma.UserWhereUniqueInput; data : Prisma.UserUpdateInput; }): Promise<VOUtils> { const { where , data } = params; let result = await this.prisma.user.update({ data , where , }); return VOUtils.success(result); }
然后修改一些 Controller 层
1 2 3 4 5 6 7 8 9 10 11 @Controller('/users' ) export class UsersController { @Post('update' ) async updateUser(@Body() userData: UsersDto): Promise<VOUtils> { return this.userService.updateUser({ where : { id: Number.parseInt(userData.id) }, data : { name: userData.name, email: userData.email }, }); } }
返回的结果就会变成
1 2 3 4 5 6 7 8 9 10 { "code" : 0 , "msg" : "接口调用成功" , "data" : { "id" : 1 , "email" : "test1@qq.com" , "name" : "testChange" , "age" : null } }
接口层 这一层用于声明每个 service 类中都有哪些方法,可以很大程度提升代码的可读性。如果没有这一层,当 service 中的方法越来越多时,代码也会特别长,想快速找到某个方法,将会变得很费时。
举例说明 接下来我们在 src 目录下创建interface
文件夹,在其目录下新建一个AppInterface.ts
文件。
举个例子,我们需要在声明 5 个方法,分别如下所示:
getTitle
getName
getAge
setName
setTitle
实现代码 在 TypeScript 中用interface
关键字来声明一个接口,那么上述例子转换为代码后就如下所示:
1 2 3 4 5 6 7 8 typescript复制代码; export interface AppInterface { getTitle (): string ; getName (): string ; getAge (): string ; setName (): string ; setTitle (): string ; }
做完上述操作后,我们还需要改造下 service 层的代码,让其实现这个接口,部分代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 typescript复制代码 @Injectable ()export class AppService implements AppInterface { getAge (): string { return "" ; } getName (): string { return "" ; }
在 TypeScript 中,我们使用 implements 关键字来实现一个接口。
模块层 这一层是使用@Module()
装饰器的类,它提供了元数据,Nest 用它来组织应用程序结构。我们有了控制层和服务层后,它们还无法运行,因为它们缺少一个组织。
实现代码 接下来,我们在 src 目录下创建module
文件夹,在其目录下创建AppModule.ts
文件,代码如下所示:
controllers 是一个数组类型的数据,我们把 controller 层的控制器在这里一一引入即可。
providers 也是一个数组类型的数据,我们把 service 层的服务在这里一一引入即可。
1 2 3 4 5 6 7 8 9 10 11 typescript复制代码; import { Module } from "@nestjs/common" ;import { AppController } from "../controller/AppController" ;import { AppService } from "../service/AppService" ;@Module ({ imports : [], controllers : [AppController ], providers : [AppService ], }) export class AppModule {}
有关 controllers 与 providers 的详细介绍,请移步:Nest-@module