为什么学习 Nest ?
仅前端开发的应用没有记忆,不能互联 🤔 → 学习后端,为应用赋能
同为 JS 语言,学习成本较小,多门技能多条路 😆 → 了解服务端编码思想
同类型框架中占绝对领先地位(star、下载量、Google 趋势)→ Nest.js 优势
Nest.js 介绍、HelloWorld、CRUD、Swagger、数据库集成 → ~~原理?~ 🤷 应用!
Nest.js 介绍
Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用 JavaScript 的渐进增强的能力,使用并完全支持 TypeScript (仍然允许开发者使用纯 JavaScript 进行开发),并结合了 OOP(面向对象编程)、FP(函数式编程)和 FRP(函数响应式编程)。
在底层,Nest 构建在强大的 HTTP 服务器框架上,例如 Express (默认),并且还可以通过配置从而使用 Fastify
可扩展?
- 模块化架构:应用拆分成更小的模块,各部分可独立地进行扩展和修改,不会对整个系统造成大的影响
// 模块化架构 → 类比前端组件化、页面? src ├── ... ├── app.module.ts ├── modules ├────── car/ ├────── engine/
依赖注入:内置依赖注入容器,用于管理组件之间的依赖关系,将组件的依赖关系解耦。依赖注入的核心概念是将组件的依赖项从组件内部获取,而不是在组件内部直接创建或实例化依赖项。通过这种方式,组件不需要关心依赖项的创建和生命周期管理,而是通过依赖注入容器(DI 容器)来自动解析和提供所需的依赖项。
@Injectable() class CarService { private engine: Engine; constructor(engine: Engine) { this.engine = engine; } // 使用 this.engine 进行操作 }
中间件和拦截器:Nest.js 提供了中间件和拦截器的机制,使得在请求的处理过程中可以插入自定义的逻辑。中间件和拦截器可以用于实现诸如权限验证、日志记录、缓存等功能。通过这些机制,你可以在应用程序的不同层面上进行扩展,而不必改变核心的业务逻辑。
// 可以在不同层面插入自定义逻辑 → 可以类比前端的路由守卫? import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; @Injectable() export class AuthMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { // 在这里编写身份验证逻辑 if (!req.headers.authorization) { // 如果请求头中没有授权信息,则返回未经授权的响应 res.status(401).json({ message: 'Unauthorized' }); return; } // 如果身份验证通过,则继续请求处理 next(); } }
- 数据库集成:Nest.js 与多种数据库集成框架(如TypeORM、Mongoose等)兼容,这使得在应用程序中使用数据库变得更加容易,并且可以方便地进行扩展和迁移。
构建在 Express 之上?
Express.js 是一个基于 Node.js 的 Web 应用程序框架,可以被视为对 Node.js 的封装。Express.js 提供了简化和抽象化的 API,使得在 Node.js 上构建 Web 应用程序变得更加简单和高效。
Node.js 本身提供了处理网络请求和构建 Web 服务器的能力,但它的 API 相对底层,需要开发者手动处理很多细节。而 Express.js 通过封装 Node.js 的功能,提供了更高层次的抽象和开发工具,使得构建 Web 应用程序变得更加便捷。
import * as http from "http"; const app = http.createServer(); app.on("request", (request, response) => { const { method, url } = request; if (method === 'GET' && url === '/hello') { response.end('hello world'); } }); app.listen(3000); // Express 实现上述功能 const express = require('express'); const app = express(); app.get('/hello', (request, response) => { response.send('hello world'); }); app.listen(3000);
其实严格来说 Express.js 并不是一个框架,它只是提供了基于中间件的请求响应处理流程。Express.js 并没有规定代码应该怎么组织,怎么复用,怎么集成各种方案,所以代码能写成各种样子,对于大项目开发来说很难维护。所以出现了更上层的 Node 框架,比如 Egg、Midway、Nest 这些,它们额外提供了架构能力,这类框架也叫企业级开发框架。
Nest.js 基于 Express.js 提供了更高层次的抽象和功能扩展。它引入了一些新的概念,如模块、控制器和中间件,以帮助开发者更好地组织和管理代码。Nest.js 还提供了一套强大的依赖注入系统,可以帮助解耦组件之间的依赖关系,并方便进行单元测试。
Nest.js 优势?
- TypeScript 支持 → 题外话:一定是优势吗?
- 无缝集成:直接使用 npm 安装常用的库
- 模块化架构
- 依赖注入
- 强大的路由系统
- 内置的中间件支持
- 生态系统和插件...
Hello world
脚手架安装 & 项目初始化
$ npm i -g @nestjs/cli $ nest -v // 9.5.0 $ nest new project-name // "@nestjs/common": "^9.0.0", // "@nestjs/core": "^9.0.0", // "@nestjs/platform-express": "^9.0.0",
目录结构 & 文件介绍
src ├── app.controller.spec.ts // 针对控制器的单元测试 ├── app.controller.ts // 带有单个路由的基本控制器 ├── app.module.ts // 应用程序的根模块(root module) ├── app.service.ts // 具有单一方法的基本服务(service) ├── app.service.spec.ts // 针对服务的单元测试 ├── main.ts // 应用程序的入口文件
入口文件(main.js):应用程序的入口文件,它使用核心函数 NestFactory 来创建 Nest 应用程序的实例
// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(3000); // curl http://localhost:3000 } bootstrap();
模块(Modules):Nest.js 应用程序由多个模块组成,每个模块都是一个逻辑上的独立单元。模块可以包含路由、控制器、服务和其他相关的组件。模块之间可以进行嵌套和引用,形成层次化的结构。
// src/app.module.ts import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @Module({ imports: [], // 导入模块的列表,如果需要使用其他模块的服务,需要通过这里导入 controllers: [AppController], // 控制器 providers: [AppService], // 服务提供者,处理具体的业务逻辑 }) export class AppModule {}
控制器(Controllers):控制器处理来自客户端的请求,并将请求分发给相应的服务进行处理。控制器负责定义路由和请求处理程序,它们使用装饰器来标记路由和请求方法。
// src/app.controller.ts import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} @Get() getHello(): string { return this.appService.getHello(); } }
在 Nest.js 中,使用控制器(Controller)和服务(Service)的组合是一种推荐的架构模式。控制器负责处理客户端的请求和响应,而服务则负责处理具体的业务逻辑。这种模式的好处是将请求处理和业务逻辑分离,使代码更具可读性、可维护性和可测试性。
相关装饰器介绍
路由装饰器(Route decorators):
@Get():定义处理 GET 请求的路由。
@Post():定义处理 POST 请求的路由。
@Put():定义处理 PUT 请求的路由。
@Delete():定义处理 DELETE 请求的路由。
@Patch():定义处理 PATCH 请求的路由。
...
请求体装饰器(Request body decorators):
@Body():从请求体中提取数据。
@Query():从查询参数中提取数据。
@Param():从路由参数中提取数据。
@Headers():从请求头中提取数据。
响应装饰器(Response decorators):
@Res():注入原生 response 对象。
@Headers():设置响应头。
状态码装饰器(Status code decorators):
@HttpCode():设置响应的状态码。
CRUD 接口实现
准备 user 模块
// $ nest generate|g module|mo user // $ nest generate|g controller|co user // $ nest generate|g service|s user // app.module.ts import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { UserModule } from './modules/userBase/user.module'; @Module({ imports: [UserModule], controllers: [AppController], providers: [AppService], }) export class AppModule {} // src/modules/user/userBase.module.ts import { Module } from '@nestjs/common'; import { UserController } from './user.controller'; import { UserService } from './user.service'; @Module({ controllers: [UserController], providers: [UserService], }) export class UserModule {}
user.controller.ts
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { UserBaseService } from './userBase.service'; import { UserItem } from 'src/core/types/user'; @Controller('/userBase') export class UserBaseController { constructor(private readonly userBaseService: UserBaseService) {} @Get('/getUserList') getUserList(): UserItem[] { return this.userBaseService.getUserList(); } @Post('addUser') addUser(@Body() userData): UserItem[] { return this.userBaseService.addUser(userData); } @Get('getUserDetail/:id') getUserDetail(@Param('id') id: string): UserItem { return this.userBaseService.getUserDetail(id); } @Put('updateUser') updateUser(@Body() userData): UserItem[] { return this.userBaseService.updateUser(userData); } @Delete('deleteUser') deleteUser(@Query() query): UserItem[] { return this.userBaseService.deleteUser(query.id); } }
user.service.ts
import { Injectable } from '@nestjs/common'; import { UserItem } from 'src/core/types/user'; @Injectable() export class UserBaseService { private userList = []; // 没有数据库,暂时通过该方式定义数据 getUserList(): UserItem[] { return this.userList; } addUser(userData: UserItem): UserItem[] { this.userList.push(userData); return this.userList; } getUserDetail(targetUserId: string): UserItem { const targetUserArray = this.userList.filter( (item) => item.id === parseInt(targetUserId), ); return targetUserArray[0] || {}; } updateUser(userData: UserItem): UserItem[] { this.userList = this.userList.map((item) => { if (item.id === userData.id) { return userData; } return item; }); return this.userList; } deleteUser(deleteId): UserItem[] { this.userList = this.userList.filter( (item) => item.id !== parseInt(deleteId), ); return this.userList; } }
参数校验
在 Nest.js 中,可以使用类验证器(class-validator)库来进行参数校验
$ npm install class-validator class-transformer // class-validator 常用的装饰器: // @IsNumber():验证值是否为数字类型 // @IsString():验证值是否为字符串类型 // @IsBoolean():验证值是否为布尔类型 // @IsNotEmpty():验证值是否非空 // @IsOptional():验证值是否可选 // @IsEmail():验证值是否为有效的电子邮件地址 // @IsUrl():验证值是否为有效的 URL 地址 // @IsDate():验证值是否为有效的日期 // @Min():验证值是否大于或等于指定的最小值 // @Max():验证值是否小于或等于指定的最大值 // @Length():验证字符串长度是否在指定的范围内 // @Matches():验证字符串是否匹配指定的正则表达式 // @IsIn():验证值是否属于指定的允许值列表 // @IsNotEmptyObject():验证对象是否为非空对象 // 装饰器支持可选参数,例如:@IsNumber({message: 'id is not number!'}) // 具体参数说明:https://github.com/typestack/class-validator#readme
Body、Query 参数校验
创建 DTO(数据传输对象)类,用于定义要验证的数据结构和规则。DTO不是中间件或过滤器,而是一种设计模式,用于处理和验证输入数据。通过使用DTO进行参数验证,我们可以在控制器中对请求的数据进行验证,确保数据的完整性和有效性,从而减少错误和异常情况。
// src/modules/user/dto/addUser.dto.ts import { IsNotEmpty, IsString, IsNumber, IsIn } from 'class-validator'; export class AddUserDto { @IsNotEmpty({ message: 'id should not be empty' }) @IsNumber({ allowNaN: false }, { message: 'id must be a number' }) id: number; @IsNotEmpty() @IsString() name: string; @IsNotEmpty() @IsNumber() age: number; @IsNotEmpty() @IsIn([1, 2]) gender: string; }
- 在控制器中使用 DTO 类进行参数校验
// src/modules/user/user.controller.ts import { Controller, Post, Body } from '@nestjs/common'; import { AddUserDto } from './dto/addUser.dto'; import { DeleteUserDto } from './dto/deleteUser.dto'; @Controller('users') export class UserController { @Post('addUser') addUser(@Body() userData: AddUserDto): UserItem[] { return this.userService.addUser(userData); } @Delete('deleteUser') deleteUser(@Query() query: DeleteUserDto): UserItem[] { return this.userService.deleteUser(query.id); } }
在应用程序的主模块中启用参数校验:
// src/main.ts import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); // 全局使用管道,用于 Controller 层参数校验 app.useGlobalPipes(new ValidationPipe()); await app.listen(3000); } bootstrap();
Params 参数校验
- 自定义管道来验证 Params
// src/modules/user/dto/getUserDetail.dto.ts import { Injectable, PipeTransform, BadRequestException } from '@nestjs/common'; @Injectable() export class GetUserDetailDto implements PipeTransform { async transform(value: string) { const targetUserId = parseInt(value); if (targetUserId <= 0) { throw new BadRequestException('UsedId is positive number'); } return targetUserId; } }
在控制器中使用封装好的管道验证参数
import { Controller, Get, Param } from '@nestjs/common'; import { GetUserDetailDto } from './dto/getUserDetail.dto'; @Controller('users') export class UserController { @Get('getUserDetail/:id') getUserDetail(@Param('id', GetUserDetailDto) id: string): UserItem { return this.userService.getUserDetail(id); } }
设置状态码
在正常情况下,默认情况下,Nest.js会自动设置响应的状态码为 200。如果没有显式地设置状态码,Nest.js 会将响应的状态码设置为 200,表示请求成功。当使用class-validator库的装饰器对 DTO 进行校验时,如果校验失败,Nest.js 会自动抛出一个BadRequestException异常,状态码为 400,包含有关校验错误的详细信息。
那如果我想手动设置状态码呢?
通过 @HttpCode() 装饰器设置
// src/modules/user/user.controller.ts import { Controller, Get, Param, HttpCode, HttpException, HttpStatus } from '@nestjs/common'; import { UserService } from './user.service'; import { GetUserDetailDto } from './dto/getUserDetail.dto'; import { UserItem } from 'src/core/types/user'; @Controller('/user') export class UserController { constructor(private readonly userService: UserService) {} @Get('getUserDetail/:id') @HttpCode(200) getUserDetail( @Param('id', GetUserDetailDto) id: string, ): UserItem | HttpException { const targetUser = this.userService.getUserDetail(id); if (targetUser) { return targetUser; } else { throw new HttpException('User was not found', HttpStatus.NOT_FOUND); } } }
通过 @Res() 参数装饰器设置
// src/modules/user/user.controller.ts import { Controller, Get, Param, Res } from '@nestjs/common'; import { UserService } from './user.service'; import { GetUserDetailDto } from './dto/getUserDetail.dto'; import { Response } from 'express'; @Controller('/user') export class UserController { constructor(private readonly userService: UserService) {} @Get('getUserDetail/:id') getUserDetail( @Param('id', GetUserDetailDto) id: string, @Res() res: Response, ): void { const targetUser = this.userService.getUserDetail(id); if (targetUser) { res.status(200).json(targetUser); } else { res.status(404).json({ message: 'User was not found' }); } } }
两者的区别
@HttpCode() 装饰器用于设置控制器方法的返回状态码。它可以在控制器方法上直接使用,并指定所需的状态码。例如,@HttpCode(200) 将设置返回状态码为 200。这个装饰器只会影响返回的状态码,而不会对响应的内容进行其他处理。经常需要配合 HttpException 一起使用。
@Res() 参数装饰器用于将 Express 的 Response 对象注入到控制器方法中,以便你可以直接操作响应。通过 @Res(),你可以访问和修改响应的状态码、头部信息、发送响应等。这个装饰器提供了更灵活的方式来处理响应,并且你可以在控制器方法中进行更多的定制。
统一响应体结构
Why?构建统一的响应体数据结构的好处是,它方便前端开发人员拦截和处理响应,提高代码可读性,并为错误处理提供统一的结构和信息,从而提升开发效率。
封装统一响应工具函数
// src/core/utils/resWrapper.util.ts function RES_WRAPPER<T>( data: T, msg: string, code: number, ): Common.CommonRes<T> { return { data, msg, code, }; } export function SUCCESS_RES<T>(data: T, msg = 'success') { return RES_WRAPPER(data, msg, 200); } export function ERROR_RES(msg: string, code = 400, data?: any) { return RES_WRAPPER(data, msg, code); }
// src/modules/user/user.controller.ts @Get('/getUserList') getUserList(): Common.CommonRes<UserItem[]> { const userList = this.userService.getUserList(); return SUCCESS_RES(userList, 'success'); }
// src/modules/user/user.controller.ts @Get('getUserDetail/:id') getUserDetail( @Param('id', GetUserDetailDto) id: string, @Res() res: Response, ): void { const targetUser = this.userService.getUserDetail(id); if (targetUser) { res.status(200).json(SUCCESS_RES(targetUser)); } else { res.status(404).json(ERROR_RES('User was not found')); } }
封装统一响应过滤器
- 封装异常响应过滤器
import { Catch, HttpException, ExceptionFilter, ArgumentsHost } from '@nestjs/common'; @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); // 获取请求上下文 const response = ctx.getResponse(); // 获取请求上下文中的 response对象 const status = exception.getStatus(); // 获取异常状态码 // 设置错误信息 const message = exception.message ? exception.message : `${status >= 500 ? 'Service Error' : 'Client Error'}`; const errorResponse = { data: {}, message: message, code: -1, }; // 设置返回的状态码,发送错误信息 response.status(status).json(errorResponse); } }
- 正常响应拦截器
import { NestInterceptor, ExecutionContext, CallHandler, Injectable } 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) => { return { data, code: 0, msg: '请求成功', }; }), ); } }
在 main.ts 中全局注册
import { TransformInterceptor } from './path/to/your/interceptor'; import { HttpExceptionFilter } from './path/to/your/filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); // 全局注册的响应过滤器 app.useGlobalInterceptors(new TransformInterceptor()); app.useGlobalFilters(new HttpExceptionFilter()); await app.listen(3000); } bootstrap();
这样就可以统一的返回错误请求了,只需要抛出异常即可,比如:
throw new HttpException('User was not found', 404);
配置 Swagger API 文档
安装 & 配置
- 安装
@nestjs/swagger
swagger-ui-express
npm install --save @nestjs/swagger swagger-ui-express
- 在应用程序的根模块(
main.ts
)中配置 Swagger
// src/main.ts import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; import { AppModule } from './app.module'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; async function bootstrap() { const app = await NestFactory.create(AppModule); // 全局使用管道,用于 Controller 层参数校验 app.useGlobalPipes(new ValidationPipe()); const config = new DocumentBuilder() .setTitle('接口文档') .setDescription('Hello-nest 接口文档') .setVersion('1.0') .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('swagger', app, document); await app.listen(3000); } bootstrap();
装饰器介绍
@ApiTags(tags: string[]): 用于给控制器或方法添加标签,用于组织和分类 API 文档
@ApiOperation(options: OperationOptions): 用于给方法添加操作信息,包括方法的摘要、描述、响应等
@ApiParam(options: ApiParamOptions): 用于给方法定义请求参数的描述信息
@ApiQuery(options: ApiQueryOptions): 用于给方法定义查询参数的描述信息
@ApiBody(options: ApiBodyOptions): 用于给方法定义请求体的内容
@ApiProperty(options: ApiPropertyOptions): 用于给模型定义各属性的描述信息
@ApiResponse(options: ApiResponseOptions): 用于给方法添加响应的描述信息
@ApiHeader(options: ApiHeaderOptions): 用于定义请求头的描述信息
了解更多:https://github.com/nestjs/swagger
配置示例
// src/modules/user/user.controller.ts import { Controller, Get } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; @Controller('users') @ApiTags('用户相关接口') export class UsersController { constructor(private readonly userService: UserService) {} @Post('addUser') @ApiOperation({ summary: '获取用户列表', // description: '获取所有的用户列表', }) @ApiResponse({ status: 200, description: '成功返回200', schema: { type: 'array', example: [ { id: 1, name: '张三', age: 18, gender: 1, }, ], }, }) addUser(@Body() userData: AddUserDto): UserItem[] { return this.userService.addUser(userData); } }
// src/modules/user/dto/addUser.dto.ts import { IsNotEmpty, IsString, IsNumber, IsIn } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class AddUserDto { @IsNotEmpty({ message: 'id should not be empty' }) @IsNumber({ allowNaN: false }, { message: 'id must be a number' }) @ApiProperty({ example: 1, description: '用户唯一 id' }) id: number; @IsNotEmpty() @IsString() @ApiProperty({ example: '张三', description: '用户名' }) name: string; @IsNotEmpty() @IsNumber() @ApiProperty({ example: 18, description: '用户年龄' }) age: number; @IsNotEmpty() @IsIn([1, 2]) @ApiProperty({ example: 1, description: '用户性别: 1 -> 男、2 -> 女' }) gender: 1 | 2; }
配置完成访问:http://localhost:3000/swagger
当然,如果你觉得 Swagger 界面不符合你风格的话,可以试试 https://app.apifox.com/
Nest.js 集成数据库
之前我们的数据都是直接存在 user.service.ts 的 private userList 中,如果要将数据存到数据库该怎么做?
在 Nest.js 中,与数据库集成可以通过各种数据库框架和库实现,如 TypeORM、Mongoose、Sequelize 等。这些框架提供了与不同类型的数据库进行交互的功能,并且与 Nest.js 集成非常方便。我们以 Mongoose 为例:
安装 Mongoose 和 @nestjs/mongoose:
npm install mongoose @nestjs/mongoose
在 AppModule 中导入 MongooseModule 并配置连接:
// src/app.module.ts import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; @Module({ imports: [ MongooseModule.forRoot('mongodb://localhost/nestjs', { // 链接自己服务器的数据库 useNewUrlParser: true, useUnifiedTopology: true, }), ], }) export class AppModule {}
为用户模块创建 Mongoose 模型和模式:
// src/modules/user/schema/user.schema.ts -> 理解成配置数据库字段? import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; @Schema() export class User extends Document { @Prop({ required: true }) name: string; @Prop({ required: true }) age: number; @Prop({ required: true }) gender: 1 | 2; } export const UserSchema = SchemaFactory.createForClass(User);
在 user 模块引入 UserSchema,注册模型
// src/modules/user/user.module.ts import { Module } from '@nestjs/common'; import { UserController } from './user.controller'; import { UserService } from './user.service'; import { MongooseModule } from '@nestjs/mongoose'; import { User, UserSchema } from './schema/user.schema'; @Module({ imports: [ MongooseModule.forFeature([ // 注册模型 { name: User.name, schema: UserSchema, collection: 'user' }, ]), ], controllers: [UserController], providers: [UserService], }) export class UserModule {}
改写 user.controller.ts 和 user.service.ts
// src/modules/user/user.controller.ts @Get('/getUserList') async getUserList(): Promise<Common.CommonRes<UserItem[]>> { const userList = await this.userService.getUserList(); return SUCCESS_RES(userList); } // src/modules/user/user.service.ts constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {} async getUserList(): Promise<UserItem[]> { return this.userModel.find().exec(); }