ノードAPIスキーマ検証にJoiを使用する方法
序章
新しいユーザーを作成するためにAPIエンドポイントで作業していると想像してください。 firstname
、lastname
、age
、birthdate
などのユーザーデータをリクエストに含める必要があります。 数値を期待しているときに、ユーザーが誤ってage
フィールドの値として自分の名前を入力することは望ましくありません。 特定の日付形式を期待しているときに、birthdate
フィールドに誕生日を入力するユーザーも望ましくありません。 悪いデータがアプリケーションを通過することは望ましくありません。 これは、データ検証で対処できます。
Sequelize、Knex、Mongoose(MongoDBの場合)などのノードアプリケーションを構築するときに ORM(オブジェクトリレーショナルマッピング)を使用したことがある場合は、検証制約を設定できることがわかります。モデルスキーマ。 これにより、データをデータベースに永続化する前に、アプリケーションレベルでのデータの処理と検証が容易になります。 APIを構築する場合、データは通常、特定のエンドポイントへのHTTPリクエストから取得され、リクエストレベルでデータを検証できるようにする必要がすぐに発生する可能性があります。
このチュートリアルでは、Joi検証モジュールを使用してリクエストレベルでデータを検証する方法を学習します。 APIリファレンスを確認すると、Joiとサポートされているスキーマタイプの使用方法について詳しく知ることができます。
このチュートリアルを終了すると、次のことができるようになります。
- リクエストデータパラメータの検証スキーマを作成します
- 検証エラーを処理し、適切なフィードバックを提供する
- リクエストをインターセプトして検証するミドルウェアを作成する
前提条件
このチュートリアルを完了するには、次のものが必要です。
- Node.jsのローカル開発環境。 Node.jsをインストールしてローカル開発環境を作成する方法に従ってください。
- APIエンドポイントのテストには、Postmanなどのツールをダウンロードしてインストールすることをお勧めします。
このチュートリアルは、ノードv14.2.0、npm
v6.14.5、およびjoi
v13.0.2で検証されました。
ステップ1—プロジェクトの設定
このチュートリアルでは、学校のポータルを構築していて、APIエンドポイントを作成したいとします。
/people
:新しい生徒と教師を追加します/auth/edit
:教師のログイン資格情報を設定します/fees/pay
:学生に料金を支払う
Expressを使用してこのチュートリアルのRESTAPIを作成し、Joiスキーマをテストします。
まず、コマンドラインターミナルを開き、新しいプロジェクトディレクトリを作成します。
mkdir joi-schema-validation
次に、そのディレクトリに移動します。
cd joi-schema-validation
次のコマンドを実行して、新しいプロジェクトを設定します。
npm init -y
そして、必要な依存関係をインストールします。
npm install [email protected]<6> [email protected] [email protected] [email protected] [email protected]<^>
プロジェクトのルートディレクトリにapp.js
という名前の新しいファイルを作成して、Expressアプリを設定します。
nano app.js
これがアプリケーションのスターターセットアップです。
まず、express
、morgan
、およびbody-parser
が必要です。
app.js
// load app dependencies const express = require('express'); const logger = require('morgan'); const bodyParser = require('body-parser');
次に、app
を初期化します。
app.js
// ... const app = express(); const port = process.env.NODE_ENV || 3000; // app configurations app.set('port', port); // establish http server connection app.listen(port, () => { console.log(`App running on port ${port}`) });
次に、morgan
ロギングとbody-parser
ミドルウェアをアプリのリクエストパイプラインに追加します。
app.js
// ... const app = express(); const port = process.env.NODE_ENV || 3000; // app configurations app.set('port', port); // load app middlewares app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); // establish http server connection app.listen(port, () => { console.log(`App running on port ${port}`) });
これらのミドルウェアは、application/json
およびapplication/x-www-form-urlencoded
リクエストの現在のHTTPリクエストの本文をフェッチして解析し、リクエストのルート処理ミドルウェアのreq.body
で使用できるようにします。
次に、Routes
を追加します。
app.js
// ... const Routes = require('./routes'); const app = express(); const port = process.env.NODE_ENV || 3000; // app configurations app.set('port', port); // load app middlewares app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); // load our API routes app.use('/', Routes); // establish http server connection app.listen(port, () => { console.log(`App running on port ${port}`) });
app.js
ファイルは今のところ完成しています。
エンドポイントの処理
アプリケーションのセットアップから、routes.js
ファイルからルートをフェッチすることを指定しました。
プロジェクトのルートディレクトリにファイルを作成しましょう。
nano routes.js
express
を要求し、"success"
の応答とリクエスト内のデータを使用してリクエストを処理します。
ルート.js
const express = require('express'); const router = express.Router(); // generic route handler const genericHandler = (req, res, next) => { res.json({ status: 'success', data: req.body }); }; module.exports = router;
次に、people
、auth/edit
、およびfees/pay
のエンドポイントを確立します。
ルート.js
// ... // create a new teacher or student router.post('/people', genericHandler); // change auth credentials for teachers router.post('/auth/edit', genericHandler); // accept fee payments for students router.post('/fees/pay', genericHandler); module.exports = router;
これで、POSTリクエストがこれらのエンドポイントのいずれかにヒットすると、アプリケーションはgenericHandler
を使用して応答を送信します。
最後に、start
スクリプトをpackage.json
ファイルのscripts
セクションに追加します。
nano package.json
次のようになります。
package.json
// ... "scripts": { "start": "node app.js" }, // ...
アプリを実行して、これまでに何があり、すべてが正しく機能していることを確認します。
npm start
"App running on port 3000"
のようなメッセージが表示されます。 サービスが実行されているポート番号をメモします。 そして、アプリケーションをバックグラウンドで実行したままにします。
エンドポイントのテスト
Postmanなどのアプリケーションを使用してAPIエンドポイントをテストできます。
注: Postmanを初めて使用する場合は、このチュートリアルでPostmanを使用するためのいくつかの手順を次に示します。
- 新しいリクエストの作成から始めます。
- リクエストタイプをPOSTに設定します(デフォルトでは、 GET に設定されている場合があります)。
- Enter request URL フィールドに、サーバーの場所(ほとんどの場合、
localhost:3000
)とエンドポイント(この場合は/people
)を入力します。 - ボディを選択します。
- エンコーディングタイプをRawに設定します(デフォルトでは、 none "に設定されている場合があります)。
- フォーマットをJSONに設定します(デフォルトでは、 Text に設定されている場合があります)。
- あなたのデータを入れてください。
次に、送信をクリックして応答を表示します。
管理者が「GladChinda」という名前の教師の新しいアカウントを作成しているシナリオを考えてみましょう。
このサンプルリクエストを提供しました:
{ "type": "TEACHER", "firstname": "Glad", "lastname": "Chinda" }
次の応答例を受け取ります。
Output{ "status": "success", "data": { "type": "TEACHER", "firstname": "Glad", "lastname": "Chinda" } }
"success"
ステータスを受け取り、送信したデータが応答にキャプチャされます。 これにより、アプリケーションが期待どおりに機能していることが確認されます。
ステップ2—Joi検証ルールを試す
簡単な例は、後のステップで何を達成するかについてのアイデアを与えるのに役立つ場合があります。
この例では、Joiを使用して検証ルールを作成し、新しいユーザーを作成するリクエストの電子メール、電話番号、および誕生日を検証します。 検証が失敗した場合は、エラーを送り返します。 それ以外の場合は、ユーザーデータを返します。
test
エンドポイントをapp.js
ファイルに追加しましょう。
nano app.js
次のコードスニペットを追加します。
app.js
// ... app.use('/', Routes); app.post('/test', (req, res, next) => { const Joi = require('joi'); const data = req.body; const schema = Joi.object().keys({ email: Joi.string().email().required(), phone: Joi.string().regex(/^\d{3}-\d{3}-\d{4}$/).required(), birthday: Joi.date().max('1-1-2004').iso() }); }); // establish http server connection app.listen(port, () => { console.log(`App running on port ${port}`) });
このコードは、新しい/test
エンドポイントを追加します。 リクエスト本文からdata
を定義します。 また、email
、phone
、およびbirthday
のJoiルールを使用してschema
を定義します。
email
の制約は次のとおりです。
- 有効なメール文字列である必要があります
- 必須です
phone
の制約は次のとおりです。
XXX-XXX-XXXX
の形式の数字を含む文字列である必要があります- 必須です
birthday
の制約は次のとおりです。
- ISO8601形式の有効な日付である必要があります
- 2004年1月1日以降にすることはできません
- 必須ではありません
次に、検証の合格と不合格を処理します。
app.js
// ... app.use('/', Routes); app.post('/test', (req, res, next) => { // ... Joi.validate(data, schema, (err, value) => { const id = Math.ceil(Math.random() * 9999999); if (err) { res.status(422).json({ status: 'error', message: 'Invalid request data', data: data }); } else { res.json({ status: 'success', message: 'User created successfully', data: Object.assign({id}, value) }); } }); }); // establish http server connection app.listen(port, () => { console.log(`App running on port ${port}`) });
このコードはdata
を受け取り、schema
に対して検証します。
email
、phone
、またはbirthday
のいずれかのルールが失敗すると、ステータスが"error"
で、メッセージが"Invalid request data"
。
email
、phone
、birthday
のすべてのルールに合格すると、ステータスが"success"
、メッセージが"User created successfully"
。
これで、サンプルルートをテストできます。
端末から次のコマンドを実行して、アプリを再起動します。
npm start
Postmanを使用して、サンプルルートPOST /test
をテストできます。
リクエストを設定します。
POST localhost:3000/test Body Raw JSON
データをJSONフィールドに追加します。
{ "email": "[email protected]", "phone": "555-555-5555", "birthday": "2004-01-01" }
次のような応答が表示されるはずです。
Output{ "status": "success", "message": "User created successfully", "data": { "id": 1234567, "email": "[email protected]", "phone": "555-555-5555", "birthday": "2004-01-01T00:00:00.000Z" } }
これを実現するデモビデオは次のとおりです。
基本スキーマにさらに検証制約を指定して、有効と見なされる値の種類を制御できます。 各制約はスキーマインスタンスを返すため、メソッドチェーンを介して複数の制約をチェーンし、より具体的な検証ルールを定義することができます。
Joi.object()
またはJoi.object().keys()
を使用してオブジェクトスキーマを作成することをお勧めします。 これらの2つのメソッドのいずれかを使用する場合、オブジェクトリテラルメソッドを使用して行うことはできないいくつかの追加の制約を使用して、オブジェクトで許可されるキーをさらに制御できます。
場合によっては、値を文字列、数値、またはその他のものにする必要があります。 ここで、代替スキーマが役立ちます。 Joi.alternatives()
を使用して代替スキーマを定義できます。 any()
スキーマを継承しているため、required()
などの制約を使用できます。
使用可能なすべての制約の詳細なドキュメントについては、APIリファレンスを参照してください。
ステップ3—APIスキーマを作成する
Joiの制約とスキーマに慣れたら、APIルートの検証スキーマを作成できます。
プロジェクトルートディレクトリにschemas.js
という名前の新しいファイルを作成します。
nano schemas.js
Joiを要求することから始めます:
schemas.js
// load Joi module const Joi = require('joi');
people
エンドポイントとpersonDataSchema
/people
エンドポイントはpersonDataSchema
を使用します。 このシナリオでは、管理者が教師と生徒のアカウントを作成しています。 APIには、id
、type
、name
、および学生の場合はage
が必要です。
id
:UUID v4形式の文字列になります:
Joi.string().guid({version: 'uuidv4'})
type
:STUDENT
またはTEACHER
の文字列になります。 検証はどのような場合でも受け入れますが、uppercase()
を強制します。
Joi.string().valid('STUDENT', 'TEACHER').uppercase().required()
age
:6
より大きい値の整数または文字列になります。 また、文字列には「年」の短縮形式(「y」、「yr」、「yrs」など)を含めることもできます。
Joi.alternatives().try([ Joi.number().integer().greater(6).required(), Joi.string().replace(/^([7-9]|[1-9]\d+)(y|yr|yrs)?$/i, '$1').required() ]);
firstname
、lastname
、fullname
:アルファベット文字の文字列になります。 検証はどのような場合でも受け入れますが、uppercase()
を強制します。
firstname
およびlastname
の英字の文字列:
Joi.string().regex(/^[A-Z]+$/).uppercase()
スペースで区切られたfullname
:
Joi.string().regex(/^[A-Z]+ [A-Z]+$/i).uppercase()
fullname
を指定する場合は、firstname
およびlastname
を省略してください。 firstname
を指定する場合は、lastname
も指定する必要があります。 fullname
またはfirstname
のいずれかを指定する必要があります。
.xor('firstname', 'fullname') .and('firstname', 'lastname') .without('fullname', ['firstname', 'lastname'])
すべてをまとめると、peopleDataSchema
は次のようになります。
schemas.js
// ... const personID = Joi.string().guid({version: 'uuidv4'}); const name = Joi.string().regex(/^[A-Z]+$/).uppercase(); const ageSchema = Joi.alternatives().try([ Joi.number().integer().greater(6).required(), Joi.string().replace(/^([7-9]|[1-9]\d+)(y|yr|yrs)?$/i, '$1').required() ]); const personDataSchema = Joi.object().keys({ id: personID.required(), firstname: name, lastname: name, fullname: Joi.string().regex(/^[A-Z]+ [A-Z]+$/i).uppercase(), type: Joi.string().valid('STUDENT', 'TEACHER').uppercase().required(), age: Joi.when('type', { is: 'STUDENT', then: ageSchema.required(), otherwise: ageSchema }) }) .xor('firstname', 'fullname') .and('firstname', 'lastname') .without('fullname', ['firstname', 'lastname']);
/auth/edit
エンドポイントとauthDataSchema
/auth/edit
エンドポイントはauthDataSchema
を使用します。 このシナリオでは、教師がアカウントの電子メールとパスワードを更新しています。 APIには、id
、email
、password
、およびconfirmPassword
が必要です。
id
:personDataSchema
に対して以前に定義された検証を使用します。
email
:有効なメールアドレスになります。 検証はどのような場合でも受け入れますが、lowercase()
を強制します。
Joi.string().email().lowercase().required()
password
:少なくとも7
文字の文字列になります:
Joi.string().min(7).required().strict()
confirmPassword
:password
を参照する文字列になり、2つの一致を確認します。
Joi.string().valid(Joi.ref('password')).required().strict()
すべてをまとめると、authDataSchema
は次のようになります。
schemas.js
// ... const authDataSchema = Joi.object({ teacherId: personID.required(), email: Joi.string().email().lowercase().required(), password: Joi.string().min(7).required().strict(), confirmPassword: Joi.string().valid(Joi.ref('password')).required().strict() });
/fees/pay
エンドポイントとfeesDataSchema
/fees/pay
エンドポイントはfeesDataSchema
を使用します。 このシナリオでは、学生が金額を支払うためにクレジットカード情報を送信し、トランザクションのタイムスタンプも記録されます。 APIには、id
、amount
、cardNumber
、およびcompletedAt
が必要です。
id
:personDataSchema
に対して以前に定義された検証を使用します。
amount
:整数または浮動小数点数のいずれかになります。 値は、1
より大きい正の数である必要があります。 浮動小数点数が指定されている場合、精度は最大2
に切り捨てられます。
Joi.number().positive().greater(1).precision(2).required()
cardNumber
:有効な Luhn Algorithm 準拠の番号である文字列になります:
Joi.string().creditCard().required()
completedAt
:JavaScript形式の日付タイムスタンプになります:
Joi.date().timestamp().required()
すべてをまとめると、feesDataSchema
は次のようになります。
schemas.js
// ... const feesDataSchema = Joi.object({ studentId: personID.required(), amount: Joi.number().positive().greater(1).precision(2).required(), cardNumber: Joi.string().creditCard().required(), completedAt: Joi.date().timestamp().required() });
最後に、スキーマに関連付けられているエンドポイントを持つオブジェクトをエクスポートします。
schemas.js
// ... // export the schemas module.exports = { '/people': personDataSchema, '/auth/edit': authDataSchema, '/fees/pay': feesDataSchema };
これで、APIエンドポイントのスキーマが作成され、エンドポイントをキーとしてオブジェクトにエクスポートされました。
ステップ4—スキーマ検証ミドルウェアの作成
APIエンドポイントへのすべてのリクエストをインターセプトし、ルートハンドラーに制御を渡す前にリクエストデータを検証するミドルウェアを作成しましょう。
プロジェクトのルートディレクトリにmiddlewares
という名前の新しいフォルダを作成します。
mkdir middlewares
次に、その中にSchemaValidator.js
という名前の新しいファイルを作成します。
nano middlewares/SchemaValidator.js
このファイルには、スキーマ検証ミドルウェア用の次のコードが含まれている必要があります。
ミドルウェア/SchemaValidator.js
const _ = require('lodash'); const Joi = require('joi'); const Schemas = require('../schemas'); module.exports = (useJoiError = false) => { // useJoiError determines if we should respond with the base Joi error // boolean: defaults to false const _useJoiError = _.isBoolean(useJoiError) && useJoiError; // enabled HTTP methods for request data validation const _supportedMethods = ['post', 'put']; // Joi validation options const _validationOptions = { abortEarly: false, // abort after the last validation error allowUnknown: true, // allow unknown keys that will be ignored stripUnknown: true // remove unknown keys from the validated data }; // return the validation middleware return (req, res, next) => { const route = req.route.path; const method = req.method.toLowerCase(); if (_.includes(_supportedMethods, method) && _.has(Schemas, route)) { // get schema for the current route const _schema = _.get(Schemas, route); if (_schema) { // Validate req.body using the schema and validation options return Joi.validate(req.body, _schema, _validationOptions, (err, data) => { if (err) { // Joi Error const JoiError = { status: 'failed', error: { original: err._object, // fetch only message and type from each error details: _.map(err.details, ({message, type}) => ({ message: message.replace(/['"]/g, ''), type })) } }; // Custom Error const CustomError = { status: 'failed', error: 'Invalid request data. Please review request and try again.' }; // Send back the JSON error response res.status(422).json(_useJoiError ? JoiError : CustomError); } else { // Replace req.body with the data after Joi validation req.body = data; next(); } }); } } next(); }; };
ここでは、LodashをJoiおよびスキーマと一緒にミドルウェアモジュールにロードしました。 また、1つの引数を受け入れ、スキーマ検証ミドルウェアを返すファクトリ関数をエクスポートしています。
ファクトリ関数の引数はboolean
値であり、true
の場合、Joi検証エラーを使用する必要があることを示します。 それ以外の場合は、ミドルウェアのエラーにカスタムの一般的なエラーが使用されます。 指定されていないか、ブール値以外の値が指定されている場合、デフォルトでfalse
になります。
また、POST
およびPUT
要求のみを処理するようにミドルウェアを定義しました。 他のすべてのリクエストメソッドはミドルウェアによってスキップされます。 必要に応じて、リクエスト本文を受け取ることができるDELETE
などの他のメソッドを追加するように構成することもできます。
ミドルウェアは、前に定義したSchemas
オブジェクトの現在のルートキーと一致するスキーマを使用して、要求データを検証します。 検証は、Joi.validate()
メソッドを使用して、次の署名で行われます。
data
:この場合はreq.body
を検証するためのデータ。schema
:データを検証するためのスキーマ。options
:検証オプションを指定するobject
。 使用した検証オプションは次のとおりです。callback
:検証後に呼び出されるコールバックfunction
。 2つの引数が必要です。 1つ目は、検証エラーがあった場合はJoiValidationError
オブジェクト、エラーがなかった場合はnull
です。 2番目の引数は出力データです。
最後に、Joi.validate()
のコールバック関数で、エラーがある場合は422
HTTPステータスコードを含むJSON応答としてフォーマットされたエラーを返すか、req.body
を単に上書きします。出力データを検証してから、制御を次のミドルウェアに渡します。
これで、ルートでミドルウェアを使用できます。
nano routes.js
routes.js
ファイルを次のように変更します。
ルート.js
const express = require('express'); const router = express.Router(); const SchemaValidator = require('./middlewares/SchemaValidator'); const validateRequest = SchemaValidator(true); // generic route handler const genericHandler = (req, res, next) => { res.json({ status: 'success', data: req.body }); }; // create a new teacher or student router.post('/people', validateRequest, genericHandler); // change auth credentials for teachers router.post('/auth/edit', validateRequest, genericHandler); // accept fee payments for students router.post('/fees/pay', validateRequest, genericHandler); module.exports = router;
アプリを実行して、アプリケーションをテストしてみましょう。
npm start
これらは、エンドポイントのテストに使用できるサンプルテストデータです。 好きなように編集できます。
注: UUID v4文字列を生成するには、ノードUUIDモジュールまたはオンラインUUIDジェネレーターを使用できます。
/people
エンドポイント
このシナリオでは、管理者が12歳のJohnDoeという名前の新しい学生をシステムに入力しています。
{ "id": "a967f52a-6aa5-401d-b760-35eef7c68b32", "type": "Student", "firstname": "John", "lastname": "Doe", "age": "12yrs" }
例POST /people
成功応答:
Output{ "status": "success", "data": { "id": "a967f52a-6aa5-401d-b760-35eef7c68b32", "type": "STUDENT", "firstname": "JOHN", "lastname": "DOE", "age": "12" } }
この失敗したシナリオでは、管理者は必須のage
フィールドに値を指定していません。
Output{ "status": "failed", "error": { "original": { "id": "a967f52a-6aa5-401d-b760-35eef7c68b32", "type": "Student", "fullname": "John Doe", }, "details": [ { "message": "age is required", "type": "any.required" } ] } }
/auth/edit
エンドポイント
このシナリオでは、教師がメールアドレスとパスワードを更新しています。
{ "teacherId": "e3464323-22c1-4e31-9ac5-9bde207d61d2", "email": "[email protected]", "password": "password", "confirmPassword": "password" }
例POST /auth/edit
成功応答:
Output{ "status": "success", "data": { "teacherId": "e3464323-22c1-4e31-9ac5-9bde207d61d2", "email": "[email protected]", "password": "password", "confirmPassword": "password" } }
この失敗したシナリオでは、教師が無効な電子メールアドレスと誤った確認パスワードを提供しました。
Output{ "status": "failed", "error": { "original": { "teacherId": "e3464323-22c1-4e31-9ac5-9bde207d61d2", "email": "email_address", "password": "password", "confirmPassword": "Password" }, "details": [ { "message": "email must be a valid email", "type": "string.email" }, { "message": "confirmPassword must be of [ref:password]", "type": "any.allowOnly" } ] } }
/fees/pay
エンドポイント
このシナリオでは、学生はクレジットカードで料金を支払い、トランザクションのタイムスタンプを記録しています。
注:テストの目的で、有効なクレジットカード番号として4242424242424242
を使用してください。 この番号は、Stripeなどのサービスによってテスト目的で指定されています。
{ "studentId": "c77b8a6e-9d26-428a-9df1-e852473f886f", "amount": 134.9875, "cardNumber": "4242424242424242", "completedAt": 1512064288409 }
例POST /fees/pay
成功応答:
Output{ "status": "success", "data": { "studentId": "c77b8a6e-9d26-428a-9df1-e852473f886f", "amount": 134.99, "cardNumber": "4242424242424242", "completedAt": "2017-11-30T17:51:28.409Z" } }
この失敗したシナリオでは、学生は無効なクレジットカード番号を提供しました。
Output{ "status": "failed", "error": { "original": { "studentId": "c77b8a6e-9d26-428a-9df1-e852473f886f", "amount": 134.9875, "cardNumber": "5678901234567890", "completedAt": 1512064288409 }, "details": [ { "message": "cardNumber must be a credit card", "type": "string.creditCard" } ] } }
さまざまな値を使用してアプリケーションのテストを完了し、検証の成功と失敗を確認できます。
結論
このチュートリアルでは、Joiを使用してデータのコレクションを検証するためのスキーマを作成し、HTTPリクエストパイプラインでカスタムスキーマ検証ミドルウェアを使用してリクエストデータの検証を処理しました。
一貫性のあるデータがあると、アプリケーションでデータを参照するときに、信頼性が高く期待どおりに動作することが保証されます。
このチュートリアルの完全なコードサンプルについては、GitHubのjoi-schema-validation-sourcecodeリポジトリを確認してください。