NestJSを用いたWebAPIサーバーの開発と、仕様書作成/APIテストの効率化

カバー

[!] この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

私たちのチームでは、新しく作成するREST APIサーバーの開発にNestJS、API仕様書のフォーマットにOpenAPI(Swagger)を採用しました。NestJSでは、組み込みのモジュールを利用することにより、OpenAPI仕様書を容易に作成することが可能です。
また、APIテストフレームワークにDreddを用いることで、実装がOpenAPI仕様書通りに動いているかどうかを自動で確認することができます。この記事では、NestJSとDreddに関しての説明や利点、またサンプルコードを用いて実際にAPIサーバー構築・OpenAPI仕様書作成・APIテストまでの一連の流れを紹介していきます。

記事の全体図

この記事の対象者

  • Node.js, OpenAPI, JavaScriptの基礎を理解している方
  • 現在API仕様書を手動で記述している方
  • APIテストを自動で簡単にしてみたいと考えている方

この記事でやること

  • NestJSの使い方を理解し、実際にサーバーを構築
  • SwaggerでAPI仕様書を作成
  • Dreddによる自動APIテストの実行・試験結果に基づく修正

この記事でやらないこと

OpenAPI(Swagger)に関する解説は省略しています。OpenAPIに関する詳細な内容は、過去の記事で紹介していますので、こちらもあわせてご参照ください。

『OpenAPI Specificationの紹介』

用語解説

NestJS

NestJSとは、Node.jsを利用してサーバーサイドアプリケーションを構築するためのフレームワークです。
NestJSの特徴として以下が挙げられます。

  • TypeScriptで記述されたフレームワークなのでTypeScriptとの親和性が高い
  • デフォルトでNode.jsのフレームワークの一つであるExpressをコアとして動作しているため、Expressの要領で開発することが可能である
  • AngularライクなのでAngularを知っていればNestJSでの開発を行う際の学習コストがかかりづらくなる
  • 今回紹介するOpenAPI仕様書作成のためのモジュールなど、組み込みの便利なモジュールがあり、かつそれらの利用が容易である

NestJSを用いることで、APIサーバーの開発に加え、仕様書の作成およびメンテナンスを効率的に行うことができます。

Dredd

Dreddは、バックエンドの実装に対してAPI仕様書通りに実装が行われているか確認できるAPIテストフレームワークです。
対応している言語は、Node.js(JavaScript)をはじめ、PythonやRubyなどがあり、言語に依存することなく使用することが可能です。
今回紹介するDreddを用いたAPIテストでは、実際にAPI仕様書を引数として渡して容易に試験することが可能となっています。

NestJSを用いてOpenAPI仕様書を自動生成する

ここからは、実際にサンプルコードを用いて簡易的なメンバー管理アプリのAPIサーバーを作成し、OpenAPI仕様書を生成したいと思います。なお、APIサーバー・API仕様書を作成するにあたり、パッケージ管理ツールであるnpm(Node Package Manager)を使用するため、事前にNode.jsをインストールする必要があります。

APIサーバー作成

まず初めに@nestjs/cliをインストールします。こちらをインストールすることで、NestJSのコマンドが入力できるようになります。

$ npm i -g @nestjs/cli

次に、プロジェクトを作成します。今回はプロジェクト名をmemberManageにします。

$ nest new memberManage

上記のコマンドをターミナル上で入力すると、以下のディレクトリが自動作成されます。今回はメンバー管理用のモジュールを別途作成するためappファイルは使用しませんが、NestJSアプリの基本の部分となります。

member-manage/src
      ├ app.controller.spec.ts
      ├ app.controller.ts
      ├ app.module.ts
      ├ app.service.ts
      └ main.ts

それぞれが持つ役割については以下になります。

ファイル名役割
app.controller.spec.tsapp.controller.ts用のテストファイル
app.controller.tsAPIのURLに対応するServiceを呼び出す
app.module.tsアプリ本体となり、様々なモジュールを紐づけする
app.service.tsビジネスロジックを記載する
main.tsアプリを起動させるためのエントリーポイント

NestJSの仕組み

次に、CLIのCRUDジェネレーターを使い、membersのCRUDコントローラーを作成します。CRUDとは、システムに必要とされる基本の機能の「Create(作成)」・「Read(読み取り)」・「Update(更新)」・「Delete(削除)」のそれぞれの頭文字から取った用語です。今回のメンバー管理アプリに置き換えると、メンバー登録・全体(一部)参照・メンバー更新・メンバー削除の機能の枠組みがCRUDジェネレーターによって生成されます。

$ cd ./member-manage/
$ nest generate resource members

上記のコマンドをターミナル上で入力すると、./src/members下に以下のディレクトリが自動生成されます。

member-manage/src/members
      ├ dto/
      │   ├ create-member.dto.ts
      │   └ update-member.dto.ts
      ├ entities/
      │   └ member.entity.ts
      ├ members.controller.spec.ts
      ├ members.controller.ts
      ├ members.module.ts
      ├ members.service.spec.ts
      └ members.service.ts

こちらも図で解説します。先ほどの仕組みとほぼ変わりませんが、member.entity.ts, create-member.dto.ts, update-member.dto.tsが増えたことにより、データの表示・登録・更新のそれぞれでどのようなデータを扱うのかを細かく指定することができます。

コマンドによって生成される各ファイルのフロー

今回のアプリケーションでメインとなるmembers.module.ts, members.controller.ts, members.service.tsのコードを以下に添付しました。 こちらのコードは、先ほどのnest generate resource membersコマンドを入力して自動生成されたコードになります。(コメントは私が追記しています)

./src/members/members.service.ts

ソースコードを開く/閉じる
/*
* インポート部分
* 追加することにより、モジュールを組み込んだり、
* 他で書かれたプログラムを繋ぎ合わせたりすることができる
*/
import { Injectable } from '@nestjs/common';
import { CreateMemberDto } from './dto/create-member.dto';
import { UpdateMemberDto } from './dto/update-member.dto';

@Injectable()
export class MembersService {
  
  // メンバー登録
  create(createMemberDto: CreateMemberDto) {
    return 'This action adds a new member';
  }

  // 全体参照
  findAll() {
    return `This action returns all members`;
  }

  // 一部参照
  findOne(id: number) {
    return `This action returns a #${id} member`;
  }

  // メンバー更新
  update(id: number, updateMemberDto: UpdateMemberDto) {
    return `This action updates a #${id} member`;
  }

  // メンバー削除
  remove(id: number) {
    return `This action removes a #${id} member`;
  }
}

./src/members/members.controller.ts

ソースコードを開く/閉じる
import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
} from '@nestjs/common';
import { MembersService } from './members.service';
import { CreateMemberDto } from './dto/create-member.dto';
import { UpdateMemberDto } from './dto/update-member.dto';

@Controller('members')
export class MembersController {
  constructor(private readonly membersService: MembersService) {}

  // Post(送信)のリクエストが送られた時の処理
  @Post()
  create(@Body() createMemberDto: CreateMemberDto) {
    return this.membersService.create(createMemberDto);
  }

  // Get(全体参照)のリクエストが送られた時の処理
  @Get()
  findAll() {
    return this.membersService.findAll();
  }

  // Get(一部参照)のリクエストが送られた時の処理
  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.membersService.findOne(+id);
  }

  // Patch(更新)のリクエストが送られた時の処理
  @Patch(':id')
  update(@Param('id') id: string, @Body() updateMemberDto: UpdateMemberDto) {
    return this.membersService.update(+id, updateMemberDto);
  }

  // Delete(削除)のリクエストが送られた時の処理
  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.membersService.remove(+id);
  }
}

./src/members/members.module.ts

ソースコードを開く/閉じる
import { Module } from '@nestjs/common';
import { MembersService } from './members.service';
import { MembersController } from './members.controller';

@Module({
  // controller指定
  controllers: [MembersController],
  // service相当の指定
  providers: [MembersService],
})
export class MembersModule {}

実際にAPIが動作しているか確認するために、試しに単体取得(GET)のAPIを動かしてみたいと思います。
まず、サーバーを起動させます。

$ npm start

起動が確認できたら、今回はID番号が1番のメンバーの情報を表示させたいので、ブラウザ上でhttp://localhost:3000/members/1と入力します。
そうすると、members.service.tsのfindOne()の内容がきちんとレスポンスされました。

単体取得(GET)API

API仕様書作成

必要なAPIサーバーが構築されたので、ここからAPI仕様書を作成していきます。
始めに@nestjs/swaggerをインストールします。こちらをインストールすることにより、NestJSの実装部分にAPI仕様を追加することが可能になります。

$ npm install --save @nestjs/swagger

main.tsでSwaggerモジュールを初期化します。

./src/main.ts

  import { NestFactory } from '@nestjs/core';
+ import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
  import { AppModule } from './app.module';

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

+   const config = new DocumentBuilder().build();
+   const document = SwaggerModule.createDocument(app, config);
+   SwaggerModule.setup('api', app, document);

    await app.listen(3000);
}
bootstrap();

サーバーを起動して、Webブラウザからhttp://localhost:3000/apiにアクセスすると、以下のようなAPI仕様書が自動でできあがります。

API仕様書(初期化後)

前の節で作成したコードにAPI仕様の説明を加えていきます。
使用するデコレーターは@ApiOperation(), @ApiResponse(), @ApiProperty(), @ApiTags()などがあります。デコレーターとは、直訳すると「装飾」という意味となります。クラスをはじめ、メソッド・アクセサー・プロパティ・パラメーターに@デコレーター関数を付与することによって、関数の説明を追加で書き加えることが可能になります。一例で、API仕様書にタグを追加させるためのデコレーターである@ApiTags()とリンクしているapi-use-tags.decorator.d.tsの関数の中身を見てみたいと思います。

api-use-tags.decorator.d.ts

export declare function ApiTags(...tags: string[]): MethodDecorator & ClassDecorator;

ApiTagsの関数は、引数にstring型(配列)のtagsを持っていることが分かります。members.controller.ts@ApiTags()のデコレーターを付与し、引数にAPIのタグとして/membersを指定してあげます。

./src/members/members.controller.ts

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
} from '@nestjs/common';
  import { MembersService } from './members.service';
  import { CreateMemberDto } from './dto/create-member.dto';
  import { UpdateMemberDto } from './dto/update-member.dto';
+ import { ApiTags } from '@nestjs/swagger';

  @Controller('members')
+ @ApiTags('/members')
  export class MembersController {
    constructor(private readonly membersService: MembersService) {}

    @Post()
    create(@Body() createMemberDto: CreateMemberDto) {
      return this.membersService.create(createMemberDto);
    }

    @Get()
    findAll() {
     return this.membersService.findAll();
    }

    @Get(':id')
    findOne(@Param('id') id: string) {
      return this.membersService.findOne(+id);
    }

    @Patch(':id')
    update(@Param('id') id: string, @Body() updateMemberDto: UpdateMemberDto) {
      return this.membersService.update(+id, updateMemberDto);
    }

    @Delete(':id')
    remove(@Param('id') id: string) {
      return this.membersService.remove(+id);
    }
  }

再度サーバーを起動すると、APIのタグ一覧に/membersが追加されました。

API仕様書(タグを付けたバージョン)

ここから、具体的なAPI仕様を追加していきます。
今回は/membersの実装を主に使用するので、/membersのAPI仕様を追加したいと思います。

./src/main.ts

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';

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

-  const config = new DocumentBuilder().build();
+  const config = new DocumentBuilder()
+   .setTitle('Members API docs')
+   .setDescription('MembersのAPI仕様書です。')
+   .setVersion('1.0')
+   .build();
   const document = SwaggerModule.createDocument(app, config);
   SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

./src/members/members.controller.ts

  import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
  import { MembersService } from './members.service';
  import { CreateMemberDto } from './dto/create-member.dto';
  import { UpdateMemberDto } from './dto/update-member.dto';
- import { ApiTags } from '@nestjs/swagger';
+ import { ApiTags, ApiProduces, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
+ import { Member } from './entities/member.entity';

  @Controller('members')
  @ApiTags('/members')
  export class MembersController {
   constructor(private readonly membersService: MembersService) {}
 
  @Post()
+ @ApiProduces('application/json; charset=utf-8')
+ @ApiOperation({ summary: '単体登録API' })
+ @ApiResponse({
+   status: 201,
+   description: '登録したメンバー設定を返却',
+   type: Member,
+ })
  create(@Body() createMemberDto: CreateMemberDto) {
     return this.membersService.create(createMemberDto);
   }

  @Get()
+ @ApiProduces('application/json; charset=utf-8')
+ @ApiOperation({ summary: '全体取得API' })
+ @ApiResponse({
+   status: 200,
+   description: '登録済メンバー設定を複数返却',
+   type: Member,
+ })
   findAll() {
     return this.membersService.findAll();
   }
 
  @Get(':id')
+ @ApiProduces('application/json; charset=utf-8')
+ @ApiOperation({ summary: '単体取得API' })
+ @ApiParam({
+   name: 'id',
+   type: Number,
+   example: 1,
+ })
+ @ApiResponse({
+   status: 200,
+   description: '指定されたIDのメンバー設定を返却',
+   type: Member,
+ })
  findOne(@Param('id') id: string) {
    return this.membersService.findOne(+id);
  }

  @Patch(':id')
+ @ApiProduces('application/json; charset=utf-8')
+ @ApiOperation({ summary: '単体更新API' })
+ @ApiParam({
+   name: 'id',
+   type: Number,
+   example: 1,
+ })
+ @ApiResponse({
+   status: 200,
+   description: '更新後のメンバー設定を返却',
+   type: Member,
+ })
  update(@Param('id') id: string, @Body() updateMemberDto: UpdateMemberDto) {
    return this.membersService.update(+id, updateMemberDto);
  }

  @Delete(':id')
+ @ApiProduces('application/json; charset=utf-8')
+ @ApiOperation({ summary: '単体削除API' })
+ @ApiParam({
+   name: 'id',
+   type: Number,
+   example: 1,
+ })
+ @ApiResponse({
+   status: 200,
+   description: '削除されたメンバーの設定を返却',
+   type: Member,
+ })
  remove(@Param('id') id: string) {
    return this.membersService.remove(+id);
  }
 }

エンティティにid、名前、年齢を持たせるようにします。

./src/members/entities/member.entity.ts

+ import { ApiProperty } from '@nestjs/swagger';

  export class Member {
+   @ApiProperty({ example: 1, description: 'メンバーID' })
+   id: number;

+  @ApiProperty({ example: 'アルファ太郎', description: 'メンバーの氏名' })
+   name: string;

+  @ApiProperty({ example: 25, description: 'メンバーの年齢' })
+   age: number;
}

再度サーバーを起動すると、追加で加えた項目が更新されています。

API仕様書(単体取得-1) API仕様書(単体取得-2)

今回はステータスコードが200(成功)の場合のみ実装しましたが、他にも404や500エラー、バリデーションエラーなど、細かい仕様を追加することも可能です。

DreddでOpenAPI仕様書を元にAPIテストを行う

本章では、前章で作成したAPIサーバーとAPI仕様書をもとにAPIテストを行うためのスキーマの準備(Dreddで扱えるスキーマへの変換)、Dreddのインストール、実際にコマンドを入力して試験を行います。その後、APIテスト結果・API仕様書をもとに実装を作成し、仕様書と実装の間の差分が無くなるようにしていきます。

DreddによるAPIテストの最終目標

事前準備

  1. NestJSのアプリ起動時にopenapi.ymlが作成されるようにする

APIテストを行うためには、YAMLやJSONで書かれたスキーマファイル、すなわちAPIの仕様が書かれた構造ファイルが必要になります。前の章の実装では、スキーマファイルが生成されていないため、ファイル出力をする実装を追加して、自動でmember-manageディレクトリ内にYAML形式のスキーマファイルを出力するようにします。

./src/main.ts

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
+import * as path from 'path';
+import { writeFileSync } from 'fs';
+import { dump } from 'js-yaml';

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

   const config = new DocumentBuilder()
     .setTitle('Members API docs')
     .setDescription('MembersのAPI仕様書です。')
     .setVersion('1.0')
     .build();
   const document = SwaggerModule.createDocument(app, config);
   SwaggerModule.setup('api', app, document);
+  const outputPath = path.resolve(process.cwd(), 'openapi.yml');
+  writeFileSync(outputPath, dump(document, {}));

  await app.listen(3000);
}
bootstrap();

上記を追記した上で、再びサーバーを起動すると、member-manageディレクトリ内にopenapi.ymlが自動生成されます。Dreddは、API仕様としてこのファイルをベースに試験していくことになります。

./openapi.yml

クリックして開く/閉じる
openapi: 3.0.0
paths:
  /:
    get:
      operationId: AppController_getHello
      parameters: []
      responses:
        '200':
          description: ''
  /members:
    post:
      operationId: MembersController_create
      summary: 単体登録API
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateMemberDto'
      responses:
        '201':
          description: 登録したメンバー設定を返却
          content:
            application/json; charset=utf-8:
              schema:
                $ref: '#/components/schemas/Member'
      tags: &ref_0
        - /members
    get:
      operationId: MembersController_findAll
      summary: 全体取得API
      parameters: []
      responses:
        '200':
          description: 登録済メンバー設定を複数返却
          content:
            application/json; charset=utf-8:
              schema:
                $ref: '#/components/schemas/Member'
      tags: *ref_0
  /members/{id}:
    get:
      operationId: MembersController_findOne
      summary: 単体取得API
      parameters:
        - name: id
          required: true
          in: path
          example: 1
          schema:
            type: number
      responses:
        '200':
          description: 指定されたIDのメンバー設定を返却
          content:
            application/json; charset=utf-8:
              schema:
                $ref: '#/components/schemas/Member'
      tags: *ref_0
    patch:
      operationId: MembersController_update
      summary: 単体更新API
      parameters:
        - name: id
          required: true
          in: path
          example: 1
          schema:
            type: number
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateMemberDto'
      responses:
        '200':
          description: 更新後のメンバー設定を返却
          content:
            application/json; charset=utf-8:
              schema:
                $ref: '#/components/schemas/Member'
      tags: *ref_0
    delete:
      operationId: MembersController_remove
      summary: 単体削除API
      parameters:
        - name: id
          required: true
          in: path
          example: 1
          schema:
            type: number
      responses:
        '200':
          description: 削除されたメンバーの設定を返却
          content:
            application/json; charset=utf-8:
              schema:
                $ref: '#/components/schemas/Member'
      tags: *ref_0
info:
  title: Members API docs
  description: MembersのAPI仕様書です。
  version: '1.0'
  contact: {}
tags: []
servers: []
components:
  schemas:
    CreateMemberDto:
      type: object
      properties: {}
    Member:
      type: object
      properties:
        id:
          type: number
          example: 1
          description: メンバーID
        name:
          type: string
          example: アルファ太郎
          description: メンバーの氏名
        age:
          type: number
          example: 25
          description: メンバーの年齢
      required:
        - id
        - name
        - age
    UpdateMemberDto:
      type: object
      properties: {}

  1. Dreddの設定ファイル(dredd.yml)を準備してopenapi.ymlを指定

実際に試験するサーバーや、使用されている言語、API仕様書などを指定する設定ファイルを作成します。こちらも作成コマンドを入力し、質問に答えていくことで自動でYAMLファイルが出力されます。
まず初めに、以下のコマンドを入力してパッケージをインストールします。

$ npm install -g dredd

次に、dredd initと入力し、APIテストの元となるdredd.ymlを生成します。 dredd initでは、対話形式で入力することができます。

$ dredd init
? Location of the API description document (openapi.yml) (node:16336) Warning: Accessing non-existent property 'padLevels' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
? Location of the API description document openapi.yml   
? Command to start the API server under test npm start
? Host of the API under test http://localhost:3000/
? Do you want to use hooks to customize Dredd's behavior? Yes
? Programming language of the hooks JavaScript
? Do you want to report your tests to the Apiary inspector? Yes
? Enter Apiary API key (leave empty for anonymous, disposable test reports)
? Dredd is best served with Continuous Integration. Do you want to create CI configuration? No

完了すると、dredd.ymlが生成されます。
これで下準備は完了です。

./dredd.yml

クリックして開く/閉じる
color: true
dry-run: null
hookfiles: null
language: nodejs
require: null
server: npm start
server-wait: 3
init: false
custom: {}
names: false
only: []
reporter: apiary
output: []
header: []
sorted: false
user: null
inline-errors: false
details: false
method: []
loglevel: warning
path: []
hooks-worker-timeout: 5000
hooks-worker-connect-timeout: 1500
hooks-worker-connect-retry: 500
hooks-worker-after-connect-wait: 100
hooks-worker-term-timeout: 5000
hooks-worker-term-retry: 500
hooks-worker-handler-host: 127.0.0.1
hooks-worker-handler-port: 61321
config: ./dredd.yml
blueprint: openapi.yml
endpoint: 'http://localhost:3000/'

実施手順

次からは実際にAPIテストを行っていきます。ターミナル上で以下のコマンドを入力します。

$ dredd

dredd.ymlでサーバーをnpm startに設定したため、自動でhttp://localhost:3000/が立ち上がり、ターミナル上に以下のログが出力されます。

> member-manage@0.0.1 start
> nest start

[Nest] 17760  - 2022/12/23 15:03:14     LOG [NestFactory] Starting Nest application...
[Nest] 17760  - 2022/12/23 15:03:14     LOG [InstanceLoader] AppModule dependencies initialized +25ms
[Nest] 17760  - 2022/12/23 15:03:14     LOG [InstanceLoader] MembersModule dependencies initialized +0ms
[Nest] 17760  - 2022/12/23 15:03:19     LOG [RoutesResolver] AppController {/}: +4981ms
[Nest] 17760  - 2022/12/23 15:03:19     LOG [RouterExplorer] Mapped {/, GET} route +2ms
[Nest] 17760  - 2022/12/23 15:03:19     LOG [RoutesResolver] MembersController {/members}: +1ms
[Nest] 17760  - 2022/12/23 15:03:19     LOG [RouterExplorer] Mapped {/members, POST} route +2ms
[Nest] 17760  - 2022/12/23 15:03:19     LOG [RouterExplorer] Mapped {/members, GET} route +1ms
[Nest] 17760  - 2022/12/23 15:03:19     LOG [RouterExplorer] Mapped {/members/:id, GET} route +1ms
[Nest] 17760  - 2022/12/23 15:03:19     LOG [RouterExplorer] Mapped {/members/:id, PATCH} route +1ms
[Nest] 17760  - 2022/12/23 15:03:19     LOG [RouterExplorer] Mapped {/members/:id, DELETE} route +2ms
[Nest] 17760  - 2022/12/23 15:03:19     LOG [NestApplication] Nest application successfully started +3ms

しばらく待つと、ターミナル上に結果が返ってきます。

API試験結果(一部抜粋)

pass: GET (200) / duration: 53ms
fail: POST (201) /members duration: 30ms
fail: GET (200) /members duration: 9ms
fail: GET (200) /members/1 duration: 9ms
fail: PATCH (200) /members/1 duration: 8ms
fail: DELETE (200) /members/1 duration: 8ms

APIのテストの結果で、passがAPI仕様書通りの実装であるという意味になり、failがAPIの仕様書通りの実装ではないという意味になります。
6項目中5項目がAPI仕様通りの実装ではないことを確認しましたので、今回はそのうちの単体取得(GET)のAPIの実装を、API仕様書通りの実装に修正したいと思います。

単体取得(GET)のAPI仕様書では、application/json; charset=utf-8が返ることを想定していますが、自動生成されたコードのままだとtext/html; charset=utf-8でレスポンスされる実装のためエラーが返ってくるようになります。

エラー出力結果(一部抜粋)

クリックして開く/閉じる
fail: GET (200) /members/1 duration: 9ms
info: Displaying failed tests...


fail: GET (200) /members/1 duration: 9ms
fail: headers: At '/content-type' No enum match for: "text/html; charset=utf-8"
body: Can't validate actual media type 'text/plain' against the expected media type 'application/json'.

request:
method: GET
uri: /members/1
headers:
    Accept: application/json; charset=utf-8
    User-Agent: Dredd/14.1.0 (Windows_NT 10.0.19044; x64)

body:



expected:
headers:
    Content-Type: application/json; charset=utf-8

body:
{
  "id": 1,
  "name": "アルファ太郎",
  "age": 25
}
statusCode: 200


actual:
statusCode: 200
headers:
    x-powered-by: Express
    content-type: text/html; charset=utf-8
    content-length: 31
    etag: W/"1f-cxD8UaISAOaz8Awh05Vrp6LAZ28"
    date: Fri, 23 Dec 2022 06:03:20 GMT
    connection: close

bodyEncoding: utf-8
body:
This action returns a #1 member



complete: 1 passing, 5 failing, 0 errors, 0 skipped, 6 total
complete: Tests took 11765ms

このように、API仕様書と違う実装になっていることが分かるようになります。

./src/members/members.service.ts

  //返却値のcontent-typeがtext/htmlである
  findOne(id: number) {
    return `This action returns a #${id} member`;
  }

ファイルの種類が異なる例

また、レスポンスヘッダー以外にも、レスポンスのボディーの内容がAPI仕様書と異なっている場合にもエラーが返るようになっています。
今度は、レスポンスのファイルの種類をJSONに修正し、データの部分の年齢をあえて削除して、API仕様とは異なるようにしてみます。members.service.tsの返却値は、members.jsonのデータに基づき、指定されたidの番号の情報(id・名前・年齢)を返す処理に修正しています。

./src/members/members.service.ts

 import { Injectable } from '@nestjs/common';
 import { CreateMemberDto } from './dto/create-member.dto';
 import { UpdateMemberDto } from './dto/update-member.dto';
+import { promises } from 'fs';
+const { readFile } = promises;

@Injectable()
export class MembersService {
  create(createMemberDto: CreateMemberDto) {
    return 'This action adds a new member';
  }

  findAll() {
    return `This action returns all members`;
  }

- findOne(id: number) {
+ async findOne(id: number) {
-    return `This action returns a #${id} member`;
+    const contents = await readFile('src/members/members.json', 'utf-8');
+    const json = JSON.parse(contents);
+    let tmp = null;
+
+    json.members.forEach((member: { id: number }) => {
+      if (member.id == id) {
+        tmp = member;
+      }
+    });
+    return tmp;
  }

    update(id: number, updateMemberDto: UpdateMemberDto) {
    return `This action updates a #${id} member`;
  }

  remove(id: number) {
    return `This action removes a #${id} member`;
  }
}

./src/members/members.json

 {
     "members": [
       { "id": 1, "name": "テスト次郎" },
       { "id": 2, "name": "テック三郎" },
       { "id": 3, "name": "ブログ四郎" }
     ]
 }

必須要件である年齢が見つかりませんとエラーが返ってきました。

info: Displaying failed tests...
fail: GET (200) /members/1 duration: 66ms
fail: body: At '/age' Missing required property: age

レスポンスのボディの形式が異なる

エラー出力結果(一部抜粋)

クリックして開く/閉じる
fail: GET (200) /members/1 duration: 66ms
info: Displaying failed tests...

fail: GET (200) /members/1 duration: 16ms
fail: body: At '/age' Missing required property: age

request:
method: GET
uri: /members/1
headers:
    Accept: application/json; charset=utf-8
    User-Agent: Dredd/14.1.0 (Windows_NT 10.0.19044; x64)

body:



expected:
headers:
    Content-Type: application/json; charset=utf-8

body:
{
  "id": 1,
  "name": "アルファ太郎",
  "age": 25
}
statusCode: 200


actual:
statusCode: 200
headers:
    x-powered-by: Express
    content-type: application/json; charset=utf-8
    content-length: 33
    etag: W/"21-YclVsVtE1CMIGd4OlyBgxwhzPag"
    date: Fri, 23 Dec 2022 06:44:31 GMT
    connection: close

bodyEncoding: utf-8
body:
{
  "id": 1,
  "name": "テスト次郎"
}




complete: 1 passing, 5 failing, 0 errors, 0 skipped, 6 total
complete: Tests took 11662ms

最後はテストが通るように、members.jsonに年齢の項目を追加して修正します。

./src/members/members.json

{
    "members": [
      { "id": 1, "name": "テスト次郎", "age": 30 },
      { "id": 2, "name": "テック三郎", "age": 28 },
      { "id": 3, "name": "ブログ四郎", "age": 26 }
    ]
}

APIテストの結果がpassとなり、API仕様通りの実装であることを確認できました。

pass: GET (200) /members/1 duration: 124ms
complete: 2 passing, 4 failing, 0 errors, 0 skipped, 6 total
complete: Tests took 12110ms

それぞれの利点について

今回NestJSおよびDreddを試してみて感じたメリットについて紹介したいと思います。

NestJSを使ってOpenAPI仕様書を作成するメリット

  1. 普通にOpenAPI仕様書をYAML形式で書くより楽

今回使用したSwaggerでは、YAML形式やJSON形式の記述に対応しています。ですので手動で記述することも可能です。小規模なプログラムであればそれでも構わないとは思いますが、大規模なシステムであったり、万が一APIの仕様に変更が生じた際の修正の手間を考えると、ファイル出力のプログラムを追加してAPI仕様書作成・更新を自動化させる方が圧倒的に楽であるといえます。

  1. NestJSに組み込まれているためセットアップが容易(main.tsで指定するだけ)

実際に導入してみて感じたのがAPI仕様書を作成するまでのハードルの低さでした。今回この記事を執筆するにあたり、初めてSwaggerを触りましたが、パッケージインストールとDocumentBuilderを設定してあげるだけですので、躓くことなく簡単に作成することができました。

Dreddを用いるメリット

Dreddを使う利点は、別途APIテストコードを書かなくてもある程度API仕様書に沿った試験を自動的に行うことができる点です。
また、自動でAPI仕様書を読み込んでくれるため、正確に試験を実施することが可能となります。

おわりに

今回は、APIサーバー構築方法と、OpenAPI仕様書作成・APIテストを自動で行ってくれるツールについて紹介しました。
手動で行うよりも効率よく開発が行えると思いますので、みなさんもぜひ試していただければと思います。

参考サイト


TOP
アルファロゴ 株式会社アルファシステムズは、ITサービス事業を展開しています。このブログでは、技術的な取り組みを紹介しています。X(旧Twitter)で更新通知をしています。