[!] この記事は公開されてから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に関する詳細な内容は、過去の記事で紹介していますので、こちらもあわせてご参照ください。
用語解説
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.ts | app.controller.ts用のテストファイル |
app.controller.ts | APIのURLに対応するServiceを呼び出す |
app.module.ts | アプリ本体となり、様々なモジュールを紐づけする |
app.service.ts | ビジネスロジックを記載する |
main.ts | アプリを起動させるためのエントリーポイント |
次に、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()の内容がきちんとレスポンスされました。
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仕様の説明を加えていきます。
使用するデコレーターは@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仕様を追加していきます。
今回は/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;
}
再度サーバーを起動すると、追加で加えた項目が更新されています。
今回はステータスコードが200(成功)の場合のみ実装しましたが、他にも404や500エラー、バリデーションエラーなど、細かい仕様を追加することも可能です。
DreddでOpenAPI仕様書を元にAPIテストを行う
本章では、前章で作成したAPIサーバーとAPI仕様書をもとにAPIテストを行うためのスキーマの準備(Dreddで扱えるスキーマへの変換)、Dreddのインストール、実際にコマンドを入力して試験を行います。その後、APIテスト結果・API仕様書をもとに実装を作成し、仕様書と実装の間の差分が無くなるようにしていきます。
事前準備
- 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: {}
- 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仕様書を作成するメリット
- 普通にOpenAPI仕様書をYAML形式で書くより楽
今回使用したSwaggerでは、YAML形式やJSON形式の記述に対応しています。ですので手動で記述することも可能です。小規模なプログラムであればそれでも構わないとは思いますが、大規模なシステムであったり、万が一APIの仕様に変更が生じた際の修正の手間を考えると、ファイル出力のプログラムを追加してAPI仕様書作成・更新を自動化させる方が圧倒的に楽であるといえます。
- NestJSに組み込まれているためセットアップが容易(main.tsで指定するだけ)
実際に導入してみて感じたのがAPI仕様書を作成するまでのハードルの低さでした。今回この記事を執筆するにあたり、初めてSwaggerを触りましたが、パッケージインストールとDocumentBuilderを設定してあげるだけですので、躓くことなく簡単に作成することができました。
Dreddを用いるメリット
Dreddを使う利点は、別途APIテストコードを書かなくてもある程度API仕様書に沿った試験を自動的に行うことができる点です。
また、自動でAPI仕様書を読み込んでくれるため、正確に試験を実施することが可能となります。
おわりに
今回は、APIサーバー構築方法と、OpenAPI仕様書作成・APIテストを自動で行ってくれるツールについて紹介しました。
手動で行うよりも効率よく開発が行えると思いますので、みなさんもぜひ試していただければと思います。