🦁

Nest.js × Swagger | バリデーターでAPISpecの生成を自動化する

2022/09/03に公開約6,800字

Nest.jsはFatだ!

それでもなお使う理由は、リクエストバリデーションやAPISpec生成をいい感じに巻き取ってくれるからだろう!

もちろんFastifyでも実現できるのですがが、今回はお手軽なNest.jsでの手順をメモします。swaggerは導入済みのものとします。

パッケージをインストール

yarn add -D class-validator class-transformer

グローバルバリデーターを有効化

src/main.ts
+ import { ValidationPipe } from '@nestjs/common'

;(async () => {
  const app = await NestFactory.create(AppModule)

+ //追加(すべてのエンドポイントが不正なデータを受信しないように保護)
+ app.useGlobalPipes(new ValidationPipe())

  await app.listen(3000)
})()

バリデーション用のDTOを作成

src/controller/sample/createSample.dto.ts
+ import { IsNotEmpty, IsOptional, IsNumber, IsString, IsDate } from 'class-validator';
+
+ // このDTOを使用するすべてのルートで、自動的にこのバリデーションルールが適用されるようになる 
+ export class CreateSampleDto {
+   @IsNotEmpty()
+   @IsString()
+   name: string;
+
+   @IsNotEmpty()
+   @IsNumber()
+   age: number;
+
+   @IsOptional()
+   @IsDate()
+   birthday?: Date;
+ }

エンドポイントでDTOを使用する

src/controller/sample/sample.controller.ts
@Controller('sample')
export class SampleController {
+  @Post()
+  create(@Body() createSampleDto: CreateSampleDto) {
+    return createSampleDto
+  }
}

検証

# 起動yarn start:dev

# 正しいリクエストcurl -X POST localhost:3000/sample -H "Content-Type: application/json" -d '{"name":"miramira", "age":22}'
{"name":"mirai","age":22}

# 不正なリクエストcurl -X POST localhost:3000/sample -H "Content-Type: application/json" -d '{"name":"miramira", "age":"HI MI TSU"}'
{"statusCode":400,"message":["age must be a number conforming to the specified constraints"],"error":"Bad Request"}

🎉  無事、不正なリクエストに対して400を返してくれるようになりました!

Swaggerと連携する

現在SwaggerUIはこんな表示になっています。
swagger-example.png
swagger-schema.png
簡単な設定でバリデーションの情報をSwaggerに再利用します。

@ApiProperty() デコレーターを追加する

これだけですw

src/controller/sample/createSample.dto.ts
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsDate, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator';

export class CreateSampleDto {
+ @ApiProperty()
  @IsNotEmpty()
  @IsString()
  name: string;

+ @ApiProperty()
  @IsNotEmpty()
  @IsNumber()
  age: number;

+ @ApiPropertyOptional() // @ApiProperty({ required: false }) でもOK
  @IsOptional()
  @IsDate()
  birthday?: Date;
}

swagger-example-decorator.png

swagger-schema-decorator.png

description, exampleも必要に応じて追加できます。

src/controller/sample/createSample.dto.ts
export class CreateSampleDto {
  @ApiProperty({
+   description: 'サンプルデータの名前(これがswaggerのコメントになる)',
+   example: 'miramira',
  })
  @IsNotEmpty()
  @IsString()
  name: string;

  @ApiProperty({
+   description: 'サンプルデータの年齢(これがswaggerのコメントになる)',
+   example: 22,
  })
  @IsNotEmpty()
  @IsNumber()
  age: number;

  @ApiPropertyOptional({
+   description: 'サンプルデータの誕生日(これがswaggerのコメントになる)',
+   example: '2000-01-01',
  })
  @IsOptional()
  @IsDate()
  birthday?: Date;

swagger-example-decorator-description.png
swagger-schema-decorator-description.png
descriptionとexampleが追加されていますね!あっぱれ😀

@ApiProperty さえ書きたくない

そこはNest.js先生!
お安い御用のようです。先に結果を載せます。

src/controller/sample/createSample.dto.ts
export class CreateSampleDto {
-  @ApiProperty({
-  description: 'サンプルデータの名前(これがswaggerのコメントになる)',
-   example: 'miramira',
- })
+ /**
+  * サンプルデータの名前(これがswaggerのコメントになる)
+  * @example miramira(これがswaggerのサンプル値になる)
+  */
  @IsNotEmpty()
  @IsString()
  name: string;

- @ApiProperty({
-  description: 'サンプルデータの年齢(これがswaggerのコメントになる)',
-   example: 22,
- })
+ /**
+  * サンプルデータの年齢(これがswaggerのコメントになる)
+  * @example 22(これがswaggerのサンプル値になる)
+  */
  @IsNotEmpty()
  @IsNumber()
  age: number;

- @ApiPropertyOptional({
-  description: 'サンプルデータの誕生日(これがswaggerのコメントになる)',
-   example: '2000-01-01',
- })
+ /**
+  * サンプルデータの誕生日(これがswaggerのコメントになる)
+  * @example '2000-01-01'(これがswaggerのサンプル値になる)
+  */
  @IsOptional()
  @IsDate()
  birthday?: Date;
}

swagger-example-comment.png

swagger-schema-comment.png

コメントが description, exampleとして反映され、class-validatorのバリデーションルールを再利用できました!

設定方法

NestCLIにコンパイラオプションを追加するだけです。

nest-cli.json
{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
+ "compilerOptions": {
+   "plugins": [
+     {
+       "name": "@nestjs/swagger",
+       "options": {
+         "classValidatorShim": true,
+         "introspectComments": true
+       }
+     }
+   ]
+ }
}

compilerOptions.plulgins.optionsPluginOptions インターフェイスを満たしていればOKです。

interface PluginOptions {
  // DTOファイルのサフィックスを追加・削除できる
  // default) ['.dto.ts', '.entity.ts']
  dtoFileNameSuffix?: string[];

  // コントローラーファイルのサフィックスを追加・削除できる
  // default) ['.controller.ts']
  controllerFileNameSuffix?: string[];

  // class-validatorのデコレーターをswaggerの記述に再利用する
  // default) true
  classValidatorShim?: boolean;

  // @ApiProperty()のどのキーにコメントを設定するか指定する
  // default) 'description'
  dtoKeyOfComment?: string;

  // @ApiOperation()のどのキーにコメントを設定するか指定する
  // default) 'description'
  controllerKeyOfComment?: string;

  // コメントでプロパティの説明や、サンプル値を記述できる
  // default) false
  introspectComments?: boolean;
}

今回は classValidatorShimintrospectComments を明示的に true にすることで、DTOで定義したバリデーションルールをSwaggerに反映し、コメントでプロパティの説明や初期値を設定できるようにしました!

おわりに

中級エンジニアを育成するプラハチャレンジ、ついに最終章へ突入しました!

https://twitter.com/miramira_dev/status/1562406262096424960?s=20&t=FAJOO9yeII4nQeK1ZQCnzg

最後の目玉となる「チーム開発」では、これまで学んだことを総動員して、技術選定、疑似POへのヒアリング、設計からテストまでを1スプリント2週間 × 4スプリントで行っていきます。

https://twitter.com/miramira_dev/status/1563163048982237186?s=20&t=FAJOO9yeII4nQeK1ZQCnzg

全員がDDD+オニオンアーキテクチャでの特大実装を経験済みなので、Nest.jsとズブズブの実装は行いませんが、今回はフロントとの繋ぎ込みも見据えて下地を整えたので、それをまとめてみました!

現在募集が止まっていますが、6期もしばらくしたら開催されると思うので、実務2年くらいまでのエンジニャーさんたちはぜひ、waiting listに入っておきましょう!めっちゃたのしいよ😎

Discussion

ログインするとコメントできます