> For the complete documentation index, see [llms.txt](https://1425816423.gitbook.io/my-knowledge-base/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://1425816423.gitbook.io/my-knowledge-base/fu-wu-qi-duan/nodejs/nestjs-cong-ling-kai-shi.md).

# nestjs从零开始

tags: Nodejs Created time: June 17, 2022 5:01 PM emoji: <https://nestjs.com/favicon.264d6486.ico>

Nest 是一个用于构建高效，可扩展的 [Node.js](http://nodejs.cn/) 服务器端应用程序的框架。它使用渐进式 JavaScript，内置并完全支持 [TypeScript](https://www.tslang.cn/)（但仍然允许开发人员使用纯 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/` 的路由，并响应一条响应数据。

```jsx
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` 装饰器来改变状态码。

```jsx
@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}
```

当发送异常时，nest会返回500状态码，但是你可以通过主动抛出异常来改变状态码。

```jsx
@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`[状态代码](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)。

而`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类似。

```jsx
Get(':img')
findOne(@Param('img') img): string {
  return `返回${img}图片`;
}
```

通过`: + 参数名`的形式定义Param，然后可以通过`@Param(name)` 访问该路由参数。

Query查询字符串就是我们常见的url参数形式，例如`/login?username=123&password=123` ，查询字符串的格式是`key=value` ，并且通过`&`字符连接（第一个参数用`？`）。

```jsx
@Get()
getDocs(@Query() query) {
    return query;
}
```

前面两种方式都是将参数放到请求的url中，而最后这种方式是将参数放到请求体中，同理通过`@Post`装饰器来获取数据。

```jsx
@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`的相关方法。

```jsx
@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注入依赖关系。

```jsx
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等相关逻辑，而更具体、更复杂的的数据处理任务交给提供者。

```tsx
// 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`（或者，在单例的正常情况下，如果现有实例已在其他地方请求，则返回现有实例）。解析此依赖关系并将其传递给控制器的构造函数（或分配给指定的属性）：

```tsx
constructor(private readonly catsService: CatsService) {}
```

### 模块Module

模块是具有 `@Module()`装饰器的类。 `@Module()`装饰器提供了元数据，Nest 用它来组织应用程序结构。

每个 Nest 应用程序至少有一个模块，即根模块。根模块是 Nest 开始安排应用程序树的地方。事实上，根模块可能是应用程序中唯一的模块，特别是当应用程序很小时，但是对于大型程序来说这是没有意义的。在大多数情况下，您将拥有多个模块，每个模块都有一组紧密相关的**功能**。

`@module()` 装饰器接受一个描述模块属性的对象：

| providers   | 由 Nest 注入器实例化的提供者，并且可以至少在整个模块中共享 |
| ----------- | -------------------------------- |
| controllers | 必须创建的一组控制器                       |
| imports     | 导入模块的列表，这些模块导出了此模块中所需提供者         |
| exports     | 由本模块提供并应在其他模块中可用的提供者的子集。         |

`CatsController`和 `CatsService`属于同一个应用程序域，因此将它们都移入`CatsModule`中。

```tsx
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`直接或间接的导入到根模块中。

```tsx
@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` 写到同一个类中。

```tsx
// 这是一个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` ：当前处理的参数，

&#x20;`metadata` ：元数据。元数据对象包含一些属性：

```tsx
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。                  |

实现一个参数验证管道，如果传递的参数

```tsx
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 。

```tsx
// 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);
  }
}
```

可以全局注册该管道，也可以局部模块使用，在这种场景下我们可以全局注册使用。

```tsx
app.useGlobalPipes(new validRequest());
```

### 文件上传与下载

### 数据库

### 日志

### 压缩

### 鉴权

### 安全

中间层实现xss防护

### 自定义装饰器

### 拦截器

### Swagger文档

## 参考

[Nest.js 中文文档](https://docs.nestjs.cn/)

[Documentation | NestJS - A progressive Node.js framework](https://docs.nestjs.com/)
