Promiseベースのエラー処理のためのExpress.jsルートの使用
公開: 2022-03-11Express.jsのタグラインは真実です。これは「Node.js用の高速で、ピニオン化されていない、最小限のWebフレームワーク」です。 現在のJavaScriptのベストプラクティスでpromiseの使用を規定しているにもかかわらず、Express.jsはデフォルトでpromiseベースのルートハンドラーをサポートしていません。
多くのExpress.jsチュートリアルではその詳細が省略されているため、開発者はルートごとに結果送信コードとエラー処理コードをコピーして貼り付ける習慣があり、技術的負債が発生します。 このアンチパターン(およびそのフォールアウト)は、今日取り上げる手法で回避できます。これは、数百のルートを持つアプリで正常に使用した手法です。
Express.jsルートの典型的なアーキテクチャ
ユーザーモデルのルートがいくつかあるExpress.jsチュートリアルアプリケーションから始めましょう。
実際のプロジェクトでは、関連データをMongoDBなどのデータベースに保存します。 ただし、私たちの目的では、データストレージの詳細は重要ではないため、簡単にするためにそれらをモックアウトします。 単純化しないのは、優れたプロジェクト構造です。これは、プロジェクトの成功の半分の鍵です。
Yeomanは、一般的にはるかに優れたプロジェクトスケルトンを生成できますが、必要なものについては、express-generatorを使用してプロジェクトスケルトンを作成し、不要な部分を削除して、次のようにします。
bin start.js node_modules routes users.js services userService.js app.js package-lock.json package.json目標に関係のない残りのファイルの行を減らしました。
Express.jsのメインアプリケーションファイル./app.jsは次のとおりです。
const createError = require('http-errors'); const express = require('express'); const cookieParser = require('cookie-parser'); const usersRouter = require('./routes/users'); const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use('/users', usersRouter); app.use(function(req, res, next) { next(createError(404)); }); app.use(function(err, req, res, next) { res.status(err.status || 500); res.send(err); }); module.exports = app; ここでは、Express.jsアプリを作成し、JSONの使用、URLエンコード、およびCookieの解析をサポートする基本的なミドルウェアを追加します。 次に、 /usersのusersRouterを追加します。 最後に、ルートが見つからない場合の対処方法と、後で変更するエラーの処理方法を指定します。
サーバー自体を起動するスクリプトは/bin/start.jsです。
const app = require('../app'); const http = require('http'); const port = process.env.PORT || '3000'; const server = http.createServer(app); server.listen(port); /package.jsonも必要最低限のものです。
{ "name": "express-promises-example", "version": "0.0.0", "private": true, "scripts": { "start": "node ./bin/start.js" }, "dependencies": { "cookie-parser": "~1.4.4", "express": "~4.16.1", "http-errors": "~1.6.3" } } /routes/users.jsで典型的なユーザールーターの実装を使用してみましょう:
const express = require('express'); const router = express.Router(); const userService = require('../services/userService'); router.get('/', function(req, res) { userService.getAll() .then(result => res.status(200).send(result)) .catch(err => res.status(500).send(err)); }); router.get('/:id', function(req, res) { userService.getById(req.params.id) .then(result => res.status(200).send(result)) .catch(err => res.status(500).send(err)); }); module.exports = router; これには2つのルートがあります/はすべてのユーザーを取得し、 /:idはIDで1人のユーザーを取得します。 また、/ services / /services/userService.jsを使用します。これには、このデータを取得するためのpromiseベースのメソッドがあります。
const users = [ {id: '1', fullName: 'User The First'}, {id: '2', fullName: 'User The Second'} ]; const getAll = () => Promise.resolve(users); const getById = (id) => Promise.resolve(users.find(u => u.id == id)); module.exports = { getById, getAll }; ここでは、実際のDBコネクタまたはORM(MongooseやSequelizeなど)の使用を避け、 Promise.resolve(...)を使用したデータフェッチを模倣しています。
Express.jsのルーティングの問題
ルートハンドラーを見ると、各サービス呼び出しが重複した.then(...)および.catch(...)コールバックを使用して、データまたはエラーをクライアントに送り返していることがわかります。
一見、これは深刻ではないように思われるかもしれません。 いくつかの基本的な実際の要件を追加しましょう。特定のエラーのみを表示し、一般的な500レベルのエラーを省略する必要があります。 また、このロジックを適用するかどうかは、環境に基づいている必要があります。 それで、サンプルプロジェクトが2つのルートから、200のルートを持つ実際のプロジェクトに成長するとどのようになりますか?
アプローチ1:効用関数
たぶん、 resolveとrejectを処理するための個別のユーティリティ関数を作成し、それらをExpress.jsルートのどこにでも適用します。
// some response handlers in /utils const handleResponse = (res, data) => res.status(200).send(data); const handleError = (res, err) => res.status(500).send(err); // routes/users.js router.get('/', function(req, res) { userService.getAll() .then(data => handleResponse(res, data)) .catch(err => handleError(res, err)); }); router.get('/:id', function(req, res) { userService.getById(req.params.id) .then(data => handleResponse(res, data)) .catch(err => handleError(res, err)); }); 見栄えが良い:データとエラーの送信の実装を繰り返していません。 ただし、これらのハンドラーをすべてのルートにインポートし、 then()およびcatch() )に渡される各promiseに追加する必要があります。
アプローチ2:ミドルウェア
別の解決策は、promiseに関するExpress.jsのベストプラクティスを使用することです。エラー送信ロジックをExpress.jsエラーミドルウェア( app.jsに追加)に移動し、 nextコールバックを使用して非同期エラーを渡します。 基本的なエラーミドルウェアのセットアップでは、単純な無名関数を使用します。
app.use(function(err, req, res, next) { res.status(err.status || 500); res.send(err); }); Express.jsは、関数のシグネチャに4つの入力引数があるため、これはエラー用であることを理解しています。 (これは、すべての関数オブジェクトが、関数が期待するパラメーターの数を記述する.lengthプロパティを持っているという事実を利用しています。)
nextを介してエラーを渡すと、次のようになります。
// some response handlers in /utils const handleResponse = (res, data) => res.status(200).send(data); // routes/users.js router.get('/', function(req, res, next) { userService.getAll() .then(data => handleResponse(res, data)) .catch(next); }); router.get('/:id', function(req, res, next) { userService.getById(req.params.id) .then(data => handleResponse(res, data)) .catch(next); }); 公式のベストプラクティスガイドを使用している場合でも、 handleResponse()関数を使用して解決し、 next関数を渡して拒否するには、すべてのルートハンドラーでJSプロミスが必要です。
より良いアプローチでそれを単純化してみましょう。
アプローチ3:プロミスベースのミドルウェア
JavaScriptの最大の特徴の1つは、その動的な性質です。 実行時に任意のオブジェクトに任意のフィールドを追加できます。 これを使用して、Express.jsの結果オブジェクトを拡張します。 Express.jsミドルウェア関数はそうするのに便利な場所です。
私たちのpromiseMiddleware()関数
Express.jsルートをよりエレガントに構造化する柔軟性を提供するpromiseミドルウェアを作成しましょう。 新しいファイル/middleware/promise.jsが必要になります:

const handleResponse = (res, data) => res.status(200).send(data); const handleError = (res, err = {}) => res.status(err.status || 500).send({error: err.message}); module.exports = function promiseMiddleware() { return (req,res,next) => { res.promise = (p) => { let promiseToResolve; if (p.then && p.catch) { promiseToResolve = p; } else if (typeof p === 'function') { promiseToResolve = Promise.resolve().then(() => p()); } else { promiseToResolve = Promise.resolve(p); } return promiseToResolve .then((data) => handleResponse(res, data)) .catch((e) => handleError(res, e)); }; return next(); }; } app.jsで、ミドルウェアをExpress.js appオブジェクト全体に適用し、デフォルトのエラー動作を更新しましょう。
const promiseMiddleware = require('./middlewares/promise'); //... app.use(promiseMiddleware()); //... app.use(function(req, res, next) { res.promise(Promise.reject(createError(404))); }); app.use(function(err, req, res, next) { res.promise(Promise.reject(err)); }); エラーミドルウェアを省略しないことに注意してください。 これは、コードに存在する可能性のあるすべての同期エラーの重要なエラーハンドラーです。 ただし、エラー送信ロジックを繰り返す代わりに、エラーミドルウェアはres.promise()に送信されるres.promise() Promise.reject()呼び出しを介して、同期エラーを同じ中央のhandleError()関数に渡すようになりました。
これは、次のような同期エラーを処理するのに役立ちます。
router.get('/someRoute', function(req, res){ throw new Error('This is synchronous error!'); }); 最後に、/ routers / users.jsにある新しい/routes/users.js res.promise()を使用してみましょう。
const express = require('express'); const router = express.Router(); const userService = require('../services/userService'); router.get('/', function(req, res) { res.promise(userService.getAll()); }); router.get('/:id', function(req, res) { res.promise(() => userService.getById(req.params.id)); }); module.exports = router; .promise()のさまざまな使用法に注意してください。関数またはpromiseを渡すことができます。 関数を渡すと、promiseがないメソッドで役立ちます。 .promise()は、それが関数であることを確認し、promiseでラップします。
実際にクライアントにエラーを送信する方がよいのはどこですか? これは、コード編成に関する良い質問です。 これは、エラーミドルウェア(エラーで動作することになっているため)またはpromiseミドルウェア(応答オブジェクトとの相互作用がすでにあるため)で行うことができます。 すべての応答操作をPromiseミドルウェアの1つの場所に保持することにしましたが、独自のコードを整理するのは各開発者の責任です。
技術的には、 res.promise()はオプションです
res.promise()を追加しましたが、これを使用することに縛られていません。必要なときに、応答オブジェクトを直接操作することができます。 これが役立つ2つのケースを見てみましょう。リダイレクトとストリームパイピングです。
特殊なケース1:リダイレクト
ユーザーを別のURLにリダイレクトするとします。 userService.jsに関数userService.js getUserProfilePicUrl()を追加しましょう:
const getUserProfilePicUrl = (id) => Promise.resolve(`/img/${id}`); そして今度は、直接応答操作を使用して、 async / awaitスタイルでユーザールーターで使用しましょう。
router.get('/:id/profilePic', async function (req, res) { try { const url = await userService.getUserProfilePicUrl(req.params.id); res.redirect(url); } catch (e) { res.promise(Promise.reject(e)); } }); エラー処理にres.promise()を使用したため、 async / awaitを使用してリダイレクトを実行し、(最も重要なことですが)エラーを渡すための中心的な場所が1つあることに注意してください。
特別な場合2:ストリーム配管
プロファイル画像ルートと同様に、ストリームのパイピングは、応答オブジェクトを直接操作する必要があるもう1つの状況です。
現在リダイレクトしているURLへのリクエストを処理するために、一般的な画像を返すルートを追加しましょう。
まず、新しい/assets/imgサブフォルダーにprofilePic.jpgを追加する必要があります。 (実際のプロジェクトでは、AWS S3のようなクラウドストレージを使用しますが、配管メカニズムは同じです。)
/img/profilePic/:idリクエストに応答して、この画像をパイプ処理してみましょう。 そのための新しいルーターを/routes/img.jsに作成する必要があります。
const express = require('express'); const router = express.Router(); const fs = require('fs'); const path = require('path'); router.get('/:id', function(req, res) { /* Note that we create a path to the file based on the current working * directory, not the router file location. */ const fileStream = fs.createReadStream( path.join(process.cwd(), './assets/img/profilePic.png') ); fileStream.pipe(res); }); module.exports = router; 次に、 app.jsに新しい/imgルーターを追加します。
app.use('/users', require('./routes/users')); app.use('/img', require('./routes/img')); リダイレクトの場合と比較して、1つの違いが目立つ可能性があります/imgルーターでres.promise()を使用していません。 これは、エラーが渡された既にパイプされた応答オブジェクトの動作が、ストリームの途中でエラーが発生した場合とは異なるためです。
Express.js開発者は、Express.jsアプリケーションでストリームを操作するときに注意を払う必要があり、エラーが発生するタイミングに応じて異なる方法でエラーを処理します。 パイピングの前にエラーを処理する必要があります( res.promise()はそこで役立ちます)。また、 .on('error')ハンドラーに基づく)も処理する必要がありますが、詳細はこの記事の範囲を超えています。
res.promise()
res.promise()を呼び出す場合と同様に、私たちが行っている方法での実装に縛られることはありません。 promiseMiddleware.jsを拡張して、 res.promise()のいくつかのオプションを受け入れ、呼び出し元が応答ステータスコード、コンテンツタイプ、またはプロジェクトで必要となる可能性のあるその他のものを指定できるようにすることができます。 ツールを形作り、ニーズに最も合うようにコードを整理するのは開発者の責任です。
Express.jsのエラー処理は最新のPromiseベースのコーディングに適合
ここで紹介するアプローチでは、当初よりも洗練されたルートハンドラーが可能になり、結果とエラーを処理する単一のポイントが可能になりますres.promise(...)の外部で発生した場合でも、 app.jsでのエラー処理のおかげです。 それでも、使用を強制されることはなく、必要に応じてエッジケースを処理できます。
これらの例の完全なコードはGitHubで入手できます。 そこから、開発者は必要に応じてカスタムロジックをhandleResponse()関数に追加できます。たとえば、データが利用できない場合は、応答ステータスを200ではなく204に変更できます。
ただし、エラーに対する追加の制御ははるかに便利です。 このアプローチは、これらの機能を本番環境に簡潔に実装するのに役立ちました。
- すべてのエラーを一貫して
{error: {message}}としてフォーマットします - ステータスが提供されていない場合は一般的なメッセージを送信し、そうでない場合は特定のメッセージを渡します
- 環境が
error.stack(またはtestなど)の場合は、devフィールドに入力します - データベースインデックスエラー(つまり、一意のインデックスフィールドを持つエンティティがすでに存在する)を処理し、意味のあるユーザーエラーで適切に応答します
このExpress.jsルートロジックは、サービスに触れることなく、すべて1つの場所にありました。これにより、コードの保守と拡張がはるかに簡単になりました。 これは、シンプルでありながらエレガントなソリューションがプロジェクト構造を大幅に改善できる方法です。
Toptal Engineeringブログでさらに読む:
- Node.jsエラー処理システムを構築する方法
