APIの境界でデータを検証し、すべてのエラー処理を四つの引数を持つ一つのミドルウェアに集約しましょう。
このレッスンを Kodokon で開く入ってくるあらゆるデータは、そうでないと証明されるまで敵対的です。JSONのボディ、URLのパラメーター、ヘッダー、すべてです。検証はアプリケーションの境界、つまりコントローラーの手前に属します。そうすれば内側のレイヤーは、保証されたデータの上で動けます。次に、二種類のエラーを区別しましょう。運用上のエラー(無効な入力、存在しないリソース、競合)は、想定内であり4xxに翻訳されます。そしてプログラミングのエラー(バグ)は、クライアント側には不透明な500を、サーバー側には完全なスタックトレースを生み出さなければなりません。
export class HttpError extends Error {
constructor(status, message, details = undefined) {
super(message);
this.status = status;
this.details = details;
}
}検証ミドルウェアは、コントローラーの手前でリクエストを横取りします。下の手書きのバージョンは原理を示すものです。本番では、宣言的なスキーマ(zodが標準になりました)が、加えて型推論と構造化されたメッセージを提供します。トレードオフは、一つの依存とわずかな実行時コストで、その代わりに集約された、組み合わせ可能で、決して忘れようのないルールが手に入ることです。
import { HttpError } from './http-error.js';
export function validateUser(req, res, next) {
const { email, name } = req.body ?? {};
const errors = [];
if (typeof email !== 'string' || !email.includes('@')) {
errors.push('email: invalid format');
}
if (typeof name !== 'string' || name.trim().length < 2) {
errors.push('name: 2 characters minimum');
}
if (errors.length > 0) {
return next(new HttpError(400, 'Invalid body', errors));
}
next();
}Expressはエラーミドルウェアを、その四つの引数のシグネチャ(err, req, res, next)によって見分けます。すべてのルートのあとにapp.use(errorHandler)で最後に登録されると、それはAPIの唯一のエラーの出口になります。均質なレスポンス形式、一箇所でのロギング、内部情報の漏洩なし、です。一方でコントローラーは、ただnext(err)を呼ぶだけです。
export function errorHandler(err, req, res, next) {
if (res.headersSent) {
return next(err);
}
const status = err.status ?? 500;
const body = { error: err.message };
if (err.details) {
body.details = err.details;
}
if (status >= 500) {
console.error(err);
body.error = 'Internal server error';
}
res.status(status).json(body);
}