REST APIをExpressで実装する際にrequestのスキーマvalidation・sanitizationをするには?
はじめに
こんにちは、SHIFT の開発部門に所属しているKatayamaです。今期から転属になり、開発を担当していくことになりました。
現在、基本的な事から学ぶ研修中です。開発部門では新しく学ぶことがたくさんあり、それらを自身の振り返りアウトプットとして発信していけたらと思います。記事が溜まったら、noteのマガジンにもまとめる予定です。
今回はOpenAPIで設計したREST APIを1からExpressで実装をしてみて、requestのスキーマをvalidation/sanitizationする実装をどのように行うのか?について理解を深めました。その際に理解した事を備忘録のようなものとしてまとめておこうと思います。
(今後、responsのスキーマチェックの実装(jest-openapiで)、mockデータではなくDBに接続してCRUDを処理、といったテーマについても続編として投稿予定です。)
※本投稿ではAPIの実装とそのテストの実装までを扱っていますが、express-validatorを使う事でvalidation/sanitizationの実装も簡単にできるという事、Jestを使う事で後回しになりがちなテストも簡単に書けるといった、各フレームワークのメリットも体感頂ければ幸いです。
requestのスキーマvalidation/sanitizationをする
REST APIの実装
今回はexpress-validatorというライブラリを利用してrequestがOpenAPIのスキーマと一致しているか?のvalidation/sanitizationを実装していく。
実装するAPIとしては、単純なrequestスキーマのvalidation/sanitizationと、その結果を返すだけのもの。
requestのスキーマvalidation/sanitizationに使えるメソッドは以下の4種類がある。
・validator.js Validators
・validator.js Sanitizers
・Validation Chain API Additional methods
・Sanitization Chain API Additional methods
これらに基づいて、実装をすると以下のようになる。
※以下のコードの概要としては、
・router.get
GETリクエストを受け、ToDoというデータを取得
・router.post
POSTリクエストを受け、ToDoというデータを登録
・checkSchema
validation/sanitizationを実施
・(req, res) => {...}
HTTPの通信を実施
といった構成になっている。
import express from 'express';
import { checkSchema, validationResult } from 'express-validator';
const router = express.Router();
router.get(
'/todo/:id',
checkSchema({
id: {
in: ['params'],
isInt: {
options: { min: 1 }
},
toInt: true,
exists: true
}
}),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
return res.status(200).end();
}
);
router.post(
'/todo',
checkSchema({
title: {
in: ['body'],
isLength: {
options: { min: 1, max: 128 }
},
isString: true,
exists: true
},
description: {
in: ['body'],
isLength: {
options: { max: 255 }
},
isString: true,
optional: true
},
assigin_person: {
in: ['body'],
isLength: {
options: { min: 1, max: 20 }
},
isString: true,
exists: true
}
}),
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
return res.status(200).end();
}
);
export default router;
checkSchema(...)の中で書いている内容については以下の表の通り。
●in: ['params']
どのパラメータか?を指定しているもので、今回だと「パスパラメータ内の」という意味。
"The location of the field, can be one or more of body, cookies, headers, params or query." と書かれているように、arrayなので最大で5つ(body, cookies, headers, params, query)が設定可能。
●isInt
文字列が整数であるかを確認する(min:1は最小値を1以上に設定している)
・参考:validator.js ValidatorsのisInt(str [, options])
●toInt: true
入力文字列を整数に変換。入力が整数でない場合はNaNに変換。
・参考:validator.js SanitizersのtoInt(input [, radix])
●exists: true
リクエストの中(query/pathパラメータ, bodyなど)に存在するかを確認する。
・参考:.exists(options)
●isLength
文字列の長さが範囲内にあるかどうかを確認する。
※何も設定しないと、optionsのデフォルトが{min:0、max:undefined}に設定されているので、チェックされない(当たり前)。
・参考:validator.js ValidatorsのisLength(str [, options])
●isString
値が文字列であるかどうかを確認する。
・参考:.isString()
●.optional(options)
requestに含まれていない時にvalidationの結果をどのようにするか?を設定できるもので、trueにすると、その値が「"", 0, false, null」といった値になっていても、この値はオプションのフィールドとして扱われvalidationの結果はOKにするという設定になる。
さらにカスタマイズしたい場合は以下のようにする事もできる。
optional: {
options: { nullable: true }
}
・参考:express-validatorのSchema-Validationがよくわからないのでソースを読んでみる
REST APIのテスト(Jest)の実装
requestのスキーマvalidation/sanitizationの実行結果が期待通りであるか?を検証するテストを実装する。
今回はresponseBodyについてはあえて確認せず、responseのstatusのみをチェックするシンプルなテストを実装した。
テストのコードは以下(なお確認する数が違うだけで同じようなテストになるため、postの方のテストは省略している)。
※テストの実装方法については色々な手法があると思うが、今回は結合テストとしてシンプルにaxiosでAPIをCallすることでテストを実装した。
import axios from 'axios';
import config from 'config';
describe('Todos API ', () => {
const host = config.get('jest.restapiEndpoint');
describe('Test Block', () => {
// GETリクエストの結果、ステータス200が返ってくる事をテスト
test('GET:/api/todo/{id}', async () => {
const res = await axios.get(`${host}/api/todo/1`);
expect(res.status).toEqual(200);
});
});
});
import axios from 'axios';
import config from 'config';
describe('Todos API', () => {
const host = config.get('jest.restapiEndpoint');
describe('Test Block', () => {
// GETリクエストの結果、requestのvalidation/sanitizationでエラーになりステータス400が返ってくる事をテスト
test('GET:/api/todo/{id} id is not over 1(Bad Request)', async () => {
let res;
try {
res = await axios.get(`${host}/api/todo/0`);
} catch (error) {
res = error.response;
}
expect(res.status).toEqual(400);
});
// GETリクエストの結果、requestのvalidation/sanitizationでエラーになりステータス400が返ってくる事をテスト
test('GET:/api/todo/{id} id is not int(Bad Request)', async () => {
let res;
try {
res = await axios.get(`${host}/api/todo/test`);
} catch (error) {
res = error.response;
}
expect(res.status).toEqual(400);
});
});
});
ソースコードについて補足をすると・・・
●import config from 'config';
rootディレクトリのconfigフォルダ(./config)内の設定ファイルを読み込み、設定を簡単にできるようにしてくれる便利パッケージ
reference:https://github.com/lorenwest/node-config
{
"jest": {
"timeout": 3000,
"restapiEndpoint": "http://localhost:3300"
}
}
import config from 'config';
const host = config.get('jest.restapiEndpoint');
console.log(host); // <- this output is "http://localhost:3300"
今回実装したAPIのOpenAPI定義について
OpenAPIでREST APIを設計してみた に書かれているものと同一なのでそちらを参照。
まとめとして
今回はExpressでREST APIを実装するStep1のようなものとして、requestのスキーマvalidation/sanitizationを実装し、その結果が正しいか?をテストを実装して確かめる部分をやってみた。
今まではフロントエンドでバリデーションをするという事をよく考えていたが、これだけ楽にvalidation/sanitizationが実装できるのであればバックエンドでやるのが楽で色々なvalidation/sanitizationに対応できそうだと思った。また、Jestを使って簡単に結合テストを実装できるので、テストはきちんと書いていくべきというのを再認識した。
次回はresponseをOpenAPIの定義に合わせて返してそのスキーマが定義と一致しているか?を検証(テスト)するという部分を実装してみたい。
__________________________________
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/