在用腻了
egg.js
之后,开始尝试一下NestJS
吧!
当前版本
node: 20.11.0 @nestjs/cli: 10.3.2 prisma: 5.18.0
初始化项目
安装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';
// 需要引入prismaService
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
npm i --save @nestjs/swagger
在 NestJS
中,只需要在 main.js
中配置几行就能启用。
// main.ts
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
// ...
// 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
的页面就能看到备注已经生效了。
// user.controller.ts
// ...
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
里面的参数了。
// dto/create-user.dto.ts
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
,开启 CORS
的方式也很简单,只需要在 main.ts
增加下面的配置就行了。
// main.ts 开启cors
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
// app.module.ts
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RedisModule } from '@nestjs-modules/ioredis';
@Module({
imports: [
ConfigModule.forRoot({
// 全局配置,其他模块读取的环境变量
isGlobal: true,
// 靠前的配置文件优先级高
// envFilePath: [`.env.production`, '.env'],
}),
/**
* 启用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
插件来对传入参数进行类型校验。
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
里,根据需求放入相对应的注解。这里演示几个最简单的
// 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
是 NestJS
提供的错误捕获器。NestJS
在运行时会遇到很多种不同的错误,需要对不同类型的错误进行错误处理。
如果是 Prisma
产生的错误,它会直接返回,会导致默认格式不一样,导致前端处理不了。
还有 class-validaor
的错误处理,也需要对其进行错误的特殊处理
快速创建 filters
文件
## 生成全局错误捕获
nest g filter global /common/filter
## 生成prisma错误捕获
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
之前,会经过全局守卫。全局守卫比较适合做权限校验,将不符合条件的用户请求全部拦截。
安装 @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> {
// context 请求的(Response/Request)的引用
// 获取请求头部数据
const request = context.switchToHttp().getRequest();
// 获取请求头中的 authorization 字段
let token = context.switchToRpc().getData().headers.authorization;
token = this.extractTokenFromHeader(token);
// 验证token的合理性以及根据token做响应的操作
if (token) {
try {
// 校验 token
const jwtService = new JwtService();
const res = jwtService.verify(token, { secret: 'SECRET' });
// 传递用户信息到 controller
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,就会报没有登录的错误。
// main.ts
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"]
谢谢你能看到这里,希望对你有帮助。如有问题,欢迎指出