nestjs从零开始
Nest 是一个用于构建高效,可扩展的 Node.js 服务器端应用程序的框架。它使用渐进式 JavaScript,内置并完全支持 TypeScript(但仍然允许开发人员使用纯 JavaScript 编写代码)并结合了 OOP(面向对象编程),FP(函数式编程)和 FRP(函数式响应编程)的元素。
在底层,Nest使用强大的 HTTP Server 框架,如 Express(默认)和 Fastify。Nest 在这些框架之上提供了一定程度的抽象,同时也将其 API 直接暴露给开发人员。这样可以轻松使用每个平台的无数第三方模块。
基础概念
nestjs的三个基础概念:控制器、提供者和模块。
控制器负责处理传入的请求和传出的响应。
提供者 是
Nest
的一个基本概念。许多基本的 Nest
类可能被视为 provider,例如 service
repository
, factory
, helper
等等。模块是具有
@Module()
装饰器的类。 @Module()
装饰器提供了元数据,Nest 用它来组织应用程序结构。控制器Controller
在nestjs中要创建一个控制器需要定义一个由
@Controller
装饰器装饰的类,装饰器会将类与所需的元数据相关联,使nest能够创建路由映射。路由
例如我们实现一个最简单的
Get /cat/
的路由,并响应一条响应数据。import { Controller, Get } from '@nestjs/common'; @Controller('cats') export class CatsController { @Get() findAll(): string { return 'This action returns all cats' } }
这里除了
@Controller
装饰器,还用到了@Get
装饰器,表示匹配Get HTTP请求,除了@Get
装饰器外还有@Delete
、@Post
、@Patch
等装饰器,对应着HTTP方法。这些装饰器可以传递一个字符串,例如
@Get(’index’)
就对应着路径/cats/index
。注意:
路由与装饰方法的方法名称没有直接联系,例如上面代码中你可以把findAll方法名改成其他任意名称。
状态码
在Restfull API中状态码由很重要的作用,默认情况下nest总是返回200状态码(Post方法是201),可以通过
@HttpCode
装饰器来改变状态码。@Post() @HttpCode(204) create() { return 'This action adds a new cat'; }
当发送异常时,nest会返回500状态码,但是你可以通过主动抛出异常来改变状态码。
@Post() @UseInterceptors(FileInterceptor('file')) @ApiConsumes('multipart/form-data') @ApiBody({ description: '图片文件', type: FileUploadDto, }) update(@UploadedFile() file: Express.Multer.File) { const reg = /^image/; if (!reg.test(file.mimetype)) { throw new HttpException('只接受图片文件', HttpStatus.FORBIDDEN); } if (file.size > IMAGE_SIZE) { throw new HttpException('图片不能大于10MB', HttpStatus.FORBIDDEN); } try { this.imageService.saveImage(file); return new ResponseJSON(true); } catch { throw new HttpException('上传失败', 500); } }
HttpException
是nest内置的一个异常类,可以通过主动抛出HttpException
异常来自定义响应。HttpException
构造函数有两个必要的参数来决定响应:response
参数定义JSON
响应体。它可以是string
或object
,如下所述。
status
参数定义HTTP
状态代码。
而
HttpStatus
其实是nest内置的一个枚举,拥有所有HTTP状态码,你可以直接写状态码,也可以通过HttpStatus
枚举成员来传递状态码,例如HttpStatus.FORBIDDEN
就是403 禁止
。参数
一般来说HTTP中可以通过三种方式传递参数。
- Param 路由参数
- Query 查询字符串
- Body 请求体
Param路由参数你可以想象加载图片的场景,例如你想加载一个a.jpg图片,通常是通过特定url+图片名的形式访问的,例如
/images/a.jpg
,当你想访问b.jpg图片时就是/images/b.jpg
,而a.jpg和b.jpg其实就是路由参数,这和vue-router的Param类似。Get(':img') findOne(@Param('img') img): string { return `返回${img}图片`; }
通过
: + 参数名
的形式定义Param,然后可以通过@Param(name)
访问该路由参数。Query查询字符串就是我们常见的url参数形式,例如
/login?username=123&password=123
,查询字符串的格式是key=value
,并且通过&
字符连接(第一个参数用?
)。@Get() getDocs(@Query() query) { return query; }
前面两种方式都是将参数放到请求的url中,而最后这种方式是将参数放到请求体中,同理通过
@Post
装饰器来获取数据。@Post() async create(@Body() createCatDto) { return 'This action adds a new cat'; }
控制器中的装饰器
nest(默认情况下)其实是对express的封装,你可以通过这些装饰器来很方便地获取相应的数据。
装饰器 | 对应的对象 |
@Request(),@Req() | req |
@Response(),@Res()* | res |
@Next() | next |
@Session() | req.session |
@Param(key?: string) | req.params/req.params[key] |
@Body(key?: string) | req.body/req.body[key] |
@Query(key?: string) | req.query/req.query[key] |
@Headers(name?: string) | req.headers/req.headers[name] |
@Ip() | req.ip |
@HostParam() | req.hosts |
Headers
要自定义响应头可以通过
@Header
或者直接使用express
的相关方法。@Post() @Header('Cache-Control', 'none') create() { return 'This action adds a new cat'; } // 或者 const imageCacheTime = 100000 @Get(':imagePath') fetchImage(@Param() param: FetchImageDto, @Res() res) { res .set('cache-control', `public, max-age=${imageCacheTime}`) .sendFile(join(__dirname, '../../../public/images/' + param.imagePath)); }
自定义headers是非常有用的,你可以通过自定义header来实现重定向、http缓存等功能,当然nest提供相应的装饰器来方便地实现这些功能,但是他们的底层仍然是通过HTTP headers来实现的,学会自定义header可以更加精确地控制行为。
提供者Provider
提供者是一个用
@Injectable()
装饰器注释的类,可以通过contructor注入依赖关系。import { Injectable } from '@nestjs/common'; import { Cat } from './interfaces/cat.interface'; @Injectable() export class CatsService { private readonly cats: Cat[] = []; create(cat: Cat) { this.cats.push(cat); } findAll(): Cat[] { return this.cats; } }
提供者的意义在于,它负责处理复杂的任务,以前面的控制器为例,你可以让控制器只负责路由、HTTP等相关逻辑,而更具体、更复杂的的数据处理任务交给提供者。
// cat.controller.ts // 控制器关注路由、HTTP相关的逻辑 @Controller(APIBase + 'aircle') export class CatController { constructor(private readonly catsService : CatsService ) {} // 依赖注入 @Get() findAll() { return this.catsService.findAll(); } } // cat.service.ts // 而提供者通常负责更复杂的任务 @Injectable() export class CatsService { private readonly cats: Cat[] = []; create(cat: Cat) { this.cats.push(cat); } findAll(): Cat[] { return this.cats; } }
在
Nest
中,借助 TypeScript 功能,管理依赖项非常容易,因为它们仅按类型进行解析。在下面的示例中,Nest
将 catsService
通过创建并返回一个实例来解析 CatsService
(或者,在单例的正常情况下,如果现有实例已在其他地方请求,则返回现有实例)。解析此依赖关系并将其传递给控制器的构造函数(或分配给指定的属性):constructor(private readonly catsService: CatsService) {}
模块Module
模块是具有
@Module()
装饰器的类。 @Module()
装饰器提供了元数据,Nest 用它来组织应用程序结构。每个 Nest 应用程序至少有一个模块,即根模块。根模块是 Nest 开始安排应用程序树的地方。事实上,根模块可能是应用程序中唯一的模块,特别是当应用程序很小时,但是对于大型程序来说这是没有意义的。在大多数情况下,您将拥有多个模块,每个模块都有一组紧密相关的功能。
@module()
装饰器接受一个描述模块属性的对象:providers | 由 Nest 注入器实例化的提供者,并且可以至少在整个模块中共享 |
controllers | 必须创建的一组控制器 |
imports | 导入模块的列表,这些模块导出了此模块中所需提供者 |
exports | 由本模块提供并应在其他模块中可用的提供者的子集。 |
CatsController
和 CatsService
属于同一个应用程序域,因此将它们都移入CatsModule
中。import { Module } from '@nestjs/common'; import { CatsController } from './cats.controller'; import { CatsService } from './cats.service'; @Module({ controllers: [CatsController], providers: [CatsService], }) export class CatsModule {}
根模块
nest必须要有一个根模块,根模块是 Nest 开始安排应用程序树的地方。

其他模块通过
import
直接或间接的导入到根模块中。@Module({ imports: [ MongooseModule.forRoot('mongodb://localhost/myblog'), DocsModule, ImageModule, LoginModule, AircleModule, ], controllers: [AppController], providers: [AppService], })
实践总结
使用nest构建一套restfull风格的API。
Typescript
nest提供良好的typescript支持,这也是我选择nest而不是egg的原因之一。typescript可以使用
type
、interface
和class
定义DTO,但是推荐使用class,因为接口和type在编译后会消失,而类会被保留,这在管道等功能中会起作用。Joi or class-validator
typescript提供编译时的类型支持,这在编写代码的时候非常有用,但是typescript无法提供运行时的验证功能,比如想验证客户端传入的参数是否有误。在nodejs中有两个流行的数据验证库:
Joi
和class-validator
两者都能提供数据验证功能,但是他们的风格不相同,class-validator
是使用类+装饰器的方式实现数据验证,而Joi
则链式调用相应方法的方法实现数据验证,很明显class-validator
更符合nest的风格,你甚至可以将Typescript DTO和class-validator
写到同一个类中。// 这是一个typescript类,可以在编译时提供类型支持 // 同时也能在运行时进行数据验证 export class LoginDto { @IsEmail() email: string; @IsString() password: string; }
参数验证
在实现后端API接口时,有一个可能出现的场景是前端传入错误的参数,比如参数类型错误,参数个数错误等,而这个时候我们想给前端一个友好的提示,而不是直接报500。
要实现这个功能就需要在执行路由处理函数前先对参数进行验证,如果验证通过才执行处理函数返回响应,如果验证失败则抛出一个友好的异常。要达到这个目的需要用到nest的管道功能。
管道
管道是一个很有趣的概念,在nodejs中也有管道的概念。数据流流入管道中,经过一系列的处理,最终从管道的另一头流出,而在这个过程中,管道可以实现两个目的
- 转换:将输入数据转换成所需的数据输出
- 验证:对输入数据进行验证,只有验证成功的数据才能输出
Nest
自带八个开箱即用的管道,即ValidationPipe
ParseIntPipe
ParseBoolPipe
ParseArrayPipe
ParseUUIDPipe
DefaultValuePipe
ParseEnumPipe
ParseFloatPipe
所有管道都必须实现一个
transform
方法,该方法接收两个参数:value和metadata。value
:当前处理的参数,metadata
:元数据。元数据对象包含一些属性:type: 'body' | 'query' | 'param' | 'custom'; metatype?: Type<unknown>; data?: string;
参数 | 描述 |
type | 告诉我们该属性是一个 body @Body(),query @Query(),param @Param() 还是自定义参数 在这里阅读更多。 |
metatype | 属性的元类型,例如 String。 如果在函数签名中省略类型声明,或者使用原生 JavaScript,则为 undefined。 |
data | 传递给装饰器的字符串,例如 @Body('string')。 如果您将括号留空,则为 undefined。 |
实现一个参数验证管道,如果传递的参数
import { ArgumentMetadata, HttpException, HttpStatus, Injectable, PipeTransform, } from '@nestjs/common'; import { plainToClass } from 'class-transformer'; import { validate } from 'class-validator'; @Injectable() export class validRequest implements PipeTransform { async transform(value: any, metadata: ArgumentMetadata) { const metatype = metadata.metatype; // 如果没有DTO,或者DTO是String等,则直接通过 if (!metatype || !this.toValidate(metatype)) { return value; } /* * metatype是参数对象的类 * plainToClass函数将value转换成metatype的实例 * validate进行验证 */ const object = plainToClass(metatype, value); const errors = await validate(object); // 存在异常,返回参数错误响应 if (errors.length > 0) { throw new HttpException('参数错误', HttpStatus.BAD_REQUEST); } return value; } private toValidate(metatype: any): boolean { const types: any[] = [String, Boolean, Number, Array, Object]; return !types.includes(metatype); } }
metatype
就是路由处理函数中的DTO,例如下面代码中metatype就是LoginDto 。// login.dto.ts // 这是一个typescript类,可以在编译时提供类型支持 // 同时也能在运行时进行数据验证 export class LoginDto { @IsEmail() email: string; @IsString() password: string; } // login.controller.ts @Controller('user') export class LoginController { constructor(private readonly loginService: LoginService) {} @Post('login') login(@Body() body: LoginDto) { return this.loginService.login(body); } }
可以全局注册该管道,也可以局部模块使用,在这种场景下我们可以全局注册使用。
app.useGlobalPipes(new validRequest());
文件上传与下载
数据库
日志
压缩
鉴权
安全
中间层实现xss防护