ノード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などのツールをダウンロードしてインストールすることをお勧めします。
このチュートリアルは、Nodev14.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 body-parser@1.18.2<6> express@4.16.2 joi@13.0.2 lodash@4.17.4 morgan@1.9.0<^>
名前の付いた新しいファイルを作成します app.js
プロジェクトのルートディレクトリで、Expressアプリを設定します。
- nano app.js
これがアプリケーションのスターターセットアップです。
まず、 express
, morgan
、 と body-parser
:
// load app dependencies
const express = require('express');
const logger = require('morgan');
const bodyParser = require('body-parser');
次に、を初期化します app
:
// ...
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
アプリのリクエストパイプラインへのミドルウェア:
// ...
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}`) });
これらのミドルウェアは、現在のHTTPリクエストの本文をフェッチして解析します。 application/json
と application/x-www-form-urlencoded
リクエストを作成し、 req.body
リクエストのルート処理ミドルウェアの。
それから加えて Routes
:
// ...
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"
およびリクエストのデータ:
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
:
// ...
// 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
スクリプトに scripts
あなたのセクション package.json
ファイル:
- nano 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.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
リクエスト本文から。 そしてそれは定義します schema
ジョイのルールで email
, phone
、 と birthday
.
の制約 email
含む:
- 有効なメール文字列である必要があります
- 必須です
の制約 phone
含む:
- 次の形式の数字を含む文字列である必要があります
XXX-XXX-XXXX
- 必須です
の制約 birthday
含む:
- ISO8601形式の有効な日付である必要があります
- 2004年1月1日以降にすることはできません
- 必須ではありません
次に、検証の合格と不合格を処理します。
// ...
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
失敗すると、ステータスが次の422エラーが生成されます。 "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を要求することから始めます:
// load Joi module
const Joi = require('joi');
people
エンドポイントと personDataSchema
The /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
これに似ています:
// ...
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
The /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
これに似ています:
// ...
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
The /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
これに似ています:
// ...
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()
});
最後に、スキーマに関連付けられているエンドポイントを持つオブジェクトをエクスポートします。
// ...
// 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
このファイルには、スキーマ検証ミドルウェア用の次のコードが含まれている必要があります。
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つの引数が必要です。 最初はジョイですValidationError
検証エラーがあった場合はオブジェクトまたはnull
エラーがない場合。 2番目の引数は出力データです。
最後に、のコールバック関数で Joi.validate()
フォーマットされたエラーをJSON応答として返します。 422
エラーがある場合、または単に上書きする場合のHTTPステータスコード req.body
検証出力データを使用して、制御を次のミドルウェアに渡します。
これで、ルートでミドルウェアを使用できます。
- nano routes.js
を変更します routes.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リポジトリを確認してください。