REST API(Node.jsのExpressで実装)のエラーハンドリングを共通化してみる
はじめに
Express で REST API を実装する時、そのエラーハンドリングを以下のように実装する事はできます。
router.get("/", (req, res) => {
try {
// do something
} catch (error) {
res.status(error.status || 500).json({
message: error.message,
status_code: res.statusCode,
path: `${req.method}:${req.originalUrl}`,
});
}
});
ただこれだと毎回 catch ブロック内を実装しなければならず、以下のようなデメリット・懸念があります。
・DRYではなくなり、同じような実装を何度もしてしまう
・個別にエラー処理を実装する事で、仮にREST APIのエラー時のスキーマ定義を共通化している場合、一部だけ定義と不一致な実装をしてしまう
そこで、今回は REST API の実装する際に、いちいち上記のように catch ブロックでエラーに関する実装をしなくてもいいように共通化するという事をやってみたのでそれについてみていきたいと思います。これにより、エラーハンドリングの処理を一括変更できるといったメリットも享受できます。
また、上記の『do somethig』内で何かビジネスロジック上でエラーになるようなパターンがある(例えば DB に既に登録済みなので 409 を返したいなど)の時に、共通のエラーオブジェクトを throw できるようにする方法についても見ていきたいと思います。
REST API のエラーハンドリングを実装する
結論
今回は Express の middleware の仕組みと、ライブラリmake-errorを使って実装した。
// error-response.js
export default () => {
return (req, res, next) => {
res.error = (error) => {
console.log(error);
if (error.status) res.status(error.status);
if (!res.statusCode) res.status(500);
res.json({
message: error.message,
status_code: res.statusCode,
path: `${req.method}:${req.originalUrl}`,
});
};
next();
};
};
// custom-error.js
import { BaseError } from "make-error";
export default class CustomError extends BaseError {
constructor(status, msg) {
super(msg);
this.status = status;
}
}
上記を利用する事で、router の実装は以下のように書ける。
import express, { Router } from "express";
import errorResponse from "./middleware/error-response";
import CustomError from "./middleware/custom-error";
const app = express();
const router = Router();
// 省略...
app.use(errorResponse());
app.use("/api/v1", router);
router.get("/", (req, res) => {
try {
// do something
throw new CustomError(409, "Already resgistred");
} catch (error) {
res.error(error);
}
});
## 一部省略している
[root@localhost node-express]# tree -I 'node_modules|.git' -a
.
├── babel.config.js
├── package.json
├── src
│ ├── middleware
│ │ ├── custom-error.js
│ │ └── error-response.js
│ └── index.js
├── webpack.config.js
└── yarn.lock
以下で詳細を見ていく。
error-response.js について
"if (error.status) res.status(error.status);" と "if (!res.statusCode) res.status(500);" は、まず error オブジェクト内に status プロパティがあればそれを HTTP レスポンスの status にセットしている。仮に、HTTP レスポンスの status が未定義であれば 500 をセットしている。
あとは、エラーの JSON を返している。
※"res.statuCode" は Express の公式にも書かれているように、Node の http モジュールのstatusCodeと同じで、Express の res.status()メソッドはこの statuCode を書き換えるメソッド。
※Express の middleware を使った res.error()の実装方法はExpress の error handling を理解し、middleware で実装してみたを参照)。
custome-error.js について
実装としては、公式にあるものとほぼ同じで、エラーオブジェクトに追加で status プロパティを設けたいので、その status だけ独自に追加している。
※status をエラーオブジェクトに追加している理由だが、今回 REST API の実装をするにあたり、request/response の validation はexpress-openapi-validatorで実装しているのだが、この express-openapi-validator のエラーオブジェクトがutil.tsに定義されているようなオブジェクトとして返され、これに status が含まれているのでそれに合わせる形で、この custome-error でも status プロパティを定義している。
このような custom-error.js を用意しておく事で、以下のように共通のエラー処理に渡せるエラーを発生させることができるようになる。
router.get("/", (req, res) => {
try {
// do something
throw new CustomError(409, "Already resgistred");
} catch (error) {
res.error(error);
}
});
この実装ができた所で curl で API を呼び出してみると、以下の通りちゃんと意図したエラーの JSON が返ってくる事が分かる。
[root@localhost node-express]# curl http://localhost:3000/api/v1
{"message":"Already exits","status_code":409,"path":"GET:/api/v1"}
まとめとして
今回は REST API を実装する際のエラーハンドリグの実装を共通化する方法について考えてみた。上記のように共通化する事で実装工数が減るだけでなく、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/