SmartNode.jsフォームの検証

公開: 2022-03-11

APIで実行する基本的なタスクの1つは、データの検証です。 この記事では、適切にフォーマットされたデータを返す方法で、データに防弾検証を追加する方法を紹介します。

Node.jsでカスタムデータ検証を行うのは簡単でも迅速でもありません。 あらゆる種類のデータをカバーするために作成する必要のある機能はたくさんあります。 ExpressとKoaの両方でいくつかのNode.jsフォームデータライブラリを試しましたが、プロジェクトのニーズを満たしたことがありませんでした。 ライブラリの拡張に問題があり、ライブラリが複雑なデータ構造や非同期検証で機能しないという問題がありました。

Datalizeを使用したNode.jsでのフォーム検証

そのため、私は最終的に、datalizeと呼ばれる独自の小さいながらも強力なフォーム検証ライブラリを作成することにしました。 拡張可能であるため、どのプロジェクトでも使用でき、要件に合わせてカスタマイズできます。 リクエストの本文、クエリ、またはパラメータを検証します。 また、 asyncフィルターや、配列ネストされたオブジェクトなどの複雑なJSON構造もサポートしています。

設定

Datalizeはnpmを介してインストールできます:

 npm install --save datalize

リクエストの本文を解析するには、別のライブラリを使用する必要があります。 まだ使用していない場合は、Koaの場合はkoa-body、Expressの場合はbody-parserをお勧めします。

このチュートリアルは、すでに設定されているHTTP APIサーバーに適用するか、次の単純なKoaHTTPサーバーコードを使用できます。

 const Koa = require('koa'); const bodyParser = require('koa-body'); const app = new Koa(); const router = new (require('koa-router'))(); // helper for returning errors in routes app.context.error = function(code, obj) { this.status = code; this.body = obj; }; // add koa-body middleware to parse JSON and form-data body app.use(bodyParser({ enableTypes: ['json', 'form'], multipart: true, formidable: { maxFileSize: 32 * 1024 * 1024, } })); // Routes... // connect defined routes as middleware to Koa app.use(router.routes()); // our app will listen on port 3000 app.listen(3000); console.log(' API listening on 3000');

ただし、これは本番環境のセットアップではありません(ロギングを使用し、承認を適用し、エラーを処理する必要があります)が、これらの数行のコードは、これから説明する例では問題なく機能します。

注:すべてのコード例はKoaを使用していますが、データ検証コードはExpressでも機能します。 datalizeライブラリには、Expressフォーム検証を実装するための例もあります。

基本的なNode.jsフォーム検証の例

KoaまたはExpressWebサーバーと、データベースに複数のフィールドを持つユーザーを作成するエンドポイントがAPIにあるとします。 一部のフィールドは必須であり、一部のフィールドは特定の値のみを持つことができるか、タイプを修正するためにフォーマットする必要があります。

次のような単純なロジックを記述できます。

 /** * @api {post} / Create a user * ... */ router.post('/', (ctx) => { const data = ctx.request.body; const errors = {}; if (!String(data.name).trim()) { errors.name = ['Name is required']; } if (!(/^[\-0-9a-zA-Z\.\+_]+@[\-0-9a-zA-Z\.\+_]+\.[a-zA-Z]{2,}$/).test(String(data.email))) { errors.email = ['Email is not valid.']; } if (Object.keys(errors).length) { return ctx.error(400, {errors}); } const user = await User.create({ name: data.name, email: data.email, }); ctx.body = user.toJSON(); });

次に、このコードを書き直して、datalizeを使用してこのリクエストを検証しましょう。

 const datalize = require('datalize'); const field = datalize.field; /** * @api {post} / Create a user * ... */ router.post('/', datalize([ field('name').trim().required(), field('email').required().email(), ]), (ctx) => { if (!ctx.form.isValid) { return ctx.error(400, {errors: ctx.form.errors}); } const user = await User.create(ctx.form); ctx.body = user.toJSON(); });

短く、きれいで、とても読みやすいです。 datalizeを使用すると、フィールドのリストを指定して、必要な数のルール(入力が無効な場合にエラーをスローする関数)またはフィルター(入力をフォーマットする関数)にチェーンすることができます。

ルールとフィルターは、定義されているのと同じ順序で実行されるため、最初に空白の文字列をトリミングしてから、値があるかどうかを確認する場合は、 .trim()の前に.required() )を定義する必要があります。

Datalizeは、指定したフィールドのみを使用してオブジェクト(より広いコンテキストオブジェクトで.formとして使用可能)を作成するため、それらを再度リストする必要はありません。 .form.isValidプロパティは、検証が成功したかどうかを示します。

自動エラー処理

すべてのリクエストでフォームが有効かどうかを確認したくない場合は、データが検証に合格しなかった場合にリクエストをキャンセルするグローバルミドルウェアを追加できます。

これを行うには、このコードをブートストラップファイルに追加して、Koa/Expressアプリインスタンスを作成します。

 const datalize = require('datalize'); // set datalize to throw an error if validation fails datalize.set('autoValidate', true); // only Koa // add to very beginning of Koa middleware chain app.use(async (ctx, next) => { try { await next(); } catch (err) { if (err instanceof datalize.Error) { ctx.status = 400; ctx.body = err.toJSON(); } else { ctx.status = 500; ctx.body = 'Internal server error'; } } }); // only Express // add to very end of Express middleware chain app.use(function(err, req, res, next) { if (err instanceof datalize.Error) { res.status(400).send(err.toJSON()); } else { res.send(500).send('Internal server error'); } });

また、datalizeが自動的に行うため、データが有効かどうかを確認する必要はありません。 データが無効な場合は、無効なフィールドのリストを含むフォーマットされたエラーメッセージが返されます。

クエリの検証

はい、クエリパラメータを非常に簡単に検証することもできますPOSTリクエストでのみ使用する必要はありません。 .query()ヘルパーメソッドを使用するだけです。唯一の違いは、データが.formではなく.dataオブジェクトに格納されることです。

 const datalize = require('datalize'); const field = datalize.field; /** * @api {get} / List users * ... */ router.post('/', datalize.query([ field('keywords').trim(), field('page').default(1).number(), field('perPage').required().select([10, 30, 50]), ]), (ctx) => { const limit = ctx.data.perPage; const where = { }; if (ctx.data.keywords) { where.name = {[Op.like]: ctx.data.keywords + '%'}; } const users = await User.findAll({ where, limit, offset: (ctx.data.page - 1) * limit, }); ctx.body = users; });

パラメータ検証用のヘルパーメソッド.params()もあります。 ルーターの.post()メソッドで2つのdatalizeミドルウェアを渡すことにより、クエリデータとフォームデータを一緒に検証できます。

その他のフィルター、配列、およびネストされたオブジェクト

これまで、Node.jsフォームの検証で非常に単純なデータを使用してきました。 次に、配列、ネストされたオブジェクトなど、より複雑なフィールドを試してみましょう。

 const datalize = require('datalize'); const field = datalize.field; const DOMAIN_ERROR = "Email's domain does not have a valid MX (mail) entry in its DNS record"; /** * @api {post} / Create a user * ... */ router.post('/', datalize([ field('name').trim().required(), field('email').required().email().custom((value) => { return new Promise((resolve, reject) => { dns.resolve(value.split('@')[1], 'MX', function(err, addresses) { if (err || !addresses || !addresses.length) { return reject(new Error(DOMAIN_ERROR)); } resolve(); }); }); }), field('type').required().select(['admin', 'user']), field('languages').array().container([ field('id').required().id(), field('level').required().select(['beginner', 'intermediate', 'advanced']) ]), field('groups').array().id(), ]), async (ctx) => { const {languages, groups} = ctx.form; delete ctx.form.languages; delete ctx.form.groups; const user = await User.create(ctx.form); await UserGroup.bulkCreate(groups.map(groupId => ({ groupId, userId: user.id, }))); await UserLanguage.bulkCreate(languages.map(item => ({ languageId: item.id, userId: user.id, level: item.level, )); });

検証する必要のあるデータの組み込みルールがない場合は、 .custom()メソッドを使用してカスタムデータ検証ルールを作成し(素晴らしい名前ですね)、そこに必要なロジックを記述できます。 ネストされたオブジェクトの場合、 datalize()関数と同じ方法でフィールドのリストを指定できる.container()メソッドがあります。 コンテナーをコンテナー内にネストするか、値を配列に変換する.array()フィルターでコンテナーを補足することができます。 .array()フィルターがコンテナーなしで使用される場合、指定されたルールまたはフィルターが配列内のすべての値に適用されます。

したがって、 .array().select(['read', 'write'])は、配列内のすべての値が'read'または'write'のいずれかであるかどうかをチェックし、そうでない場合は、すべてのインデックスのリストを返します。エラー。 かなりかっこいいですね

PUT / PATCH

PUT / PATCH (またはPOST )を使用してデータを更新する場合、すべてのロジック、ルール、およびフィルターを書き直す必要はありません。 .optional().patch() )のような追加のフィルターを追加するだけで、リクエストで定義されていない場合、コンテキストオブジェクトからフィールドが削除されます。 ( .optional()は常にオプションになりますが、 .patch()はHTTPリクエストのメソッドがPATCHの場合にのみオプションになります。)この追加のフィルターを追加して、データベース内のデータの作成と更新の両方で機能するようにすることができます。

 const datalize = require('datalize'); const field = datalize.field; const userValidator = datalize([ field('name').patch().trim().required(), field('email').patch().required().email(), field('type').patch().required().select(['admin', 'user']), ]); const userEditMiddleware = async (ctx, next) => { const user = await User.findByPk(ctx.params.id); // cancel request here if user was not found if (!user) { throw new Error('User was not found.'); } // store user instance in the request so we can use it later ctx.user = user; return next(); }; /** * @api {post} / Create a user * ... */ router.post('/', userValidator, async (ctx) => { const user = await User.create(ctx.form); ctx.body = user.toJSON(); }); /** * @api {put} / Update a user * ... */ router.put('/:id', userEditMiddleware, userValidator, async (ctx) => { await ctx.user.update(ctx.form); ctx.body = ctx.user.toJSON(); }); /** * @api {patch} / Patch a user * ... */ router.patch('/:id', userEditMiddleware, userValidator, async (ctx) => { if (!Object.keys(ctx.form).length) { return ctx.error(400, {message: 'Nothing to update.'}); } await ctx.user.update(ctx.form); ctx.body = ctx.user.toJSON(); });

2つの単純なミドルウェアを使用すると、すべてのPOST / PUT / PATCHメソッドのほとんどのロジックを記述できます。 userEditMiddleware()関数は、編集するレコードが存在するかどうかを確認し、存在しない場合はエラーをスローします。 次に、 userValidator()がすべてのエンドポイントの検証を行います。 最後に、 .patch()フィルターは、フィールドが定義されておらず、リクエストのメソッドがPATCHである場合、 .formオブジェクトからすべてのフィールドを削除します。

Node.jsフォーム検証エクストラ

カスタムフィルターでは、他のフィールドの値を取得し、それに基づいて検証を実行できます。 すべてカスタム関数のコールバックパラメータで提供されるため、リクエストやユーザー情報など、コンテキストオブジェクトから任意のデータを取得することもできます。

ライブラリは基本的なルールとフィルターのセットをカバーしていますが、任意のフィールドで使用できるカスタムグローバルフィルターを登録できるため、同じコードを何度も記述する必要はありません。

 const datalize = require('datalize'); const Field = datalize.Field; Field.prototype.date = function(format = 'YYYY-MM-DD') { return this.add(function(value) { const date = value ? moment(value, format) : null; if (!date || !date.isValid()) { throw new Error('%s is not a valid date.'); } return date.format(format); }); }; Field.prototype.dateTime = function(format = 'YYYY-MM-DD HH:mm') { return this.date(format); };

これらの2つのカスタムフィルターを使用すると、フィールドを.date()または.dateTime()フィルターでチェーンして、日付入力を検証できます。

datalizeを使用してファイルを検証することもできます.size() .file() .mime() mime()、. size()などのファイル専用の特別なフィルターがあるため、ファイルを個別に処理する必要はありません。

今すぐより良いAPIの作成を開始

小規模なAPIと大規模なAPIの両方で、すでにいくつかの本番プロジェクトでNode.jsフォームの検証にdatalizeを使用しています。 それは私がそれらをより読みやすくそして維持しやすくしながら、時間通りにそしてより少ないストレスで素晴らしいプロジェクトを提供するのを助けました。 あるプロジェクトでは、Socket.IOの周りに単純なラッパーを記述して、WebSocketメッセージのデータを検証するために使用しました。使用法は、Koaでルートを定義するのとほとんど同じでした。 十分な関心があれば、そのためのチュートリアルも書くかもしれません。

このチュートリアルが、セキュリティの問題や内部サーバーエラーのない完全に検証されたデータを使用して、Node.jsでより優れたAPIを構築するのに役立つことを願っています。 そして最も重要なことは、JavaScriptを使用してフォームを検証するための追加の関数を作成するために投資しなければならない時間を大幅に節約できることを願っています。