🎭 ハンドラーユーティリティ
ハンドラーラッパーは、任意のハンドラー関数に適用できる横断的関心事を提供します。これらのユーティリティは、コアビジネスロジックを変更することなく、ロギング、ページネーション、その他のミドルウェアのような機能を追加します。
🎯 概要
ハンドラーラッパーは、同じインターフェースを維持しながら、追加機能をハンドラーに付与するユーティリティです。デコレーターパターンに従い、複数の機能を追加するために一緒に構成できます。
主な機能
- 非侵襲的: ハンドラー署名を変更せずに機能を追加
- 合成可能: 同じハンドラーに複数のラッパーを組み合わせる
- 設定可能: 各ラッパーの柔軟な設定オプション
- 型安全: 適切な型指定による完全なTypeScriptサポート
📝 関数ロギング
withLogging
任意の関数をラップして、柔軟なパラメータ順序による包括的なロギング機能を提供します。
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging } = primitives;
// デフォルト設定でのシンプルなロギング
const loggedHandler = withLogging(createUserHandler);
// ログレベルを指定
const debugHandler = withLogging(createUserHandler, 'debug');
// カスタムロガーを指定
const customLoggedHandler = withLogging(createUserHandler, customLogger);
// ロガーとレベル両方を指定(任意の順序)
const fullLoggedHandler = withLogging(createUserHandler, customLogger, 'trace');
ログレベル
// 利用可能なログレベル
withLogging(handler, 'trace'); // 最も詳細
withLogging(handler, 'debug'); // 開発情報
withLogging(handler, 'info'); // 一般情報
withLogging(handler, 'warn'); // 警告
withLogging(handler, 'error'); // エラー
withLogging(handler, 'fatal'); // 致命的エラー
ログ出力
すべてのログレベルが構造化ログを生成し、以下の情報を含みます:
- 関数名
- サニタイズされた入力引数
- サニタイズされた結果/戻り値
- 非同期関数のPromise検出
{
"args": [{"params": {"requestBody": {"name": "John"}}}],
"functionName": "createUserHandler",
"msg": "Function: createUserHandler",
"result": {"user": {"id": "123", "name": "John"}}
}
スマートサニタイズ
withLogging
関数は、機密オブジェクトを自動的にサニタイズします:
- データベース接続 →
🗄️ [Database]
- 設定オブジェクト →
⚙️ [Config]
- メールサービス →
📧 [MailService]
- リクエスト/レスポンス →
📤 [Request]
/📥 [Response]
- ロガーインスタンス →
📝 [Logger]
📄 自動ページネーション
withPagination
ラッパーが実装されています。MongoDBスタイルのfind
呼び出しを拡張し、requestQuery
からpage
とlimit
を適用し、下流のハンドラー/ターミネータのためにページネーションメタデータをペイロードに添付します。
withPagination
データベースバックエンドのルートハンドラーに自動ページネーションを追加し、特にMongoDBコレクションに有用です。
import { primitives } from '@nodeblocks/backend-sdk';
const { withPagination } = primitives;
// すべての製品を返す基本ハンドラー
const getAllProducts = async (payload: RouteHandlerPayload) => {
const products = await payload.context.db.products.find({}).toArray();
return ok(mergeData(payload, { products }));
};
// ハンドラーにページネーションを追加
const getPaginatedProducts = withPagination(getAllProducts);
ページネーションパラメータ
ラッパーはリクエストクエリから以下のパラメータを自動的に抽出します:
パラメータ | 型 | デフォルト | 説明 |
---|---|---|---|
page | number | 1 | ページ番号(1ベースのインデックス) |
limit | number | 10 | ページあたりの項目数 |
レスポンス構造
withPagination
はページネーションメタデータをpayload.paginationResult
としてペイロードに保存します。必要に応じてターミネータを使用してHTTPレスポンスに含めてください。
// データ + ページネーションを返すターミネータの例
export const normalizeProductsListTerminator = (
result: Result<RouteHandlerPayload, NodeblocksError>
) => {
if (result.isErr()) throw result.error;
const payload = result.value;
const { data, pagination } = payload.paginationResult || {};
return { data: data ?? payload.context.data.products, pagination };
};
使用例
// リクエスト: GET /api/products?page=2&limit=20
// 現在のレスポンス(ページネーションメタデータなし):
[
{ "id": "prod-21", "name": "Product 21" },
{ "id": "prod-22", "name": "Product 22" }
]
// 将来のレスポンス(ページネーションメタデータ付き):
{
"data": [
{ "id": "prod-21", "name": "Product 21" },
{ "id": "prod-22", "name": "Product 22" }
],
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
}
}
🔧 高度な使用法
ラッパーの組み合わせ
同じハンドラーに複数のラッパーを組み合わせることができます:
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging, withPagination } = primitives;
// ロギングとページネーションの両方を追加
const enhancedHandler = withLogging(
withPagination(getAllProducts),
'debug'
);
// または任意の順序で構成
const alternativeHandler = withPagination(
withLogging(getAllProducts, 'info')
);
条件付きラッピング
環境または設定に基づいて条件付きでラッパーを適用:
const createHandler = (config: Config) => {
let handler = getAllProducts;
// 有効な場合はページネーションを追加
if (config.enablePagination) {
handler = withPagination(handler);
}
// 環境に基づいてロギングを追加
if (config.environment === 'development') {
handler = withLogging(handler, 'debug');
} else if (config.environment === 'production') {
handler = withLogging(handler, 'info');
}
return handler;
};
カスタムロガー統合
import { createLogger } from 'winston';
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging } = primitives;
// カスタムロガーを作成
const customLogger = createLogger({
level: 'debug',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'app.log' }),
new winston.transports.Console()
]
});
// 特定のレベルでカスタムロガーを使用
const loggedHandler = withLogging(
createUserHandler,
customLogger,
'debug'
);
設定
withPagination
は現在オプションを受け付けません。requestQuery
からpage
とlimit
を読み取り、skip
を内部的に計算します。
📝 実践例
ログ付きユーザー作成
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging } = primitives;
const createUserHandler = async (payload: RouteHandlerPayload) => {
const { name, email } = payload.params.requestBody;
// ビジネスロジックはここ
const user = await userService.create({ name, email });
return ok(mergeData(payload, { user }));
};
// 包括的なロギングを追加
const loggedCreateUser = withLogging(createUserHandler, 'debug');
// ルートでの使用
const createUserRoute = withRoute({
method: 'POST',
path: '/users',
handler: loggedCreateUser,
});
ページネーション付き製品リスト
import { primitives } from '@nodeblocks/backend-sdk';
const { withPagination } = primitives;
const getAllProducts = async (payload: RouteHandlerPayload) => {
const products = await payload.context.db.products
.find({})
.sort({ createdAt: -1 })
.toArray();
return ok(mergeData(payload, { products }));
};
// ページネーションを追加(現在は配列を直接返す)
const getPaginatedProducts = withPagination(getAllProducts);
// ルートでの使用
const getProductsRoute = withRoute({
method: 'GET',
path: '/products',
handler: getPaginatedProducts,
});
ロギングとページネーションの組み合わせ
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging, withPagination } = primitives;
const getProductsHandler = compose(
withLogging(withPagination(findProducts), 'debug'),
lift(normalizeProductsList)
);
// これは以下の処理を行うパイプラインを作成します:
// 1. データベースクエリにページネーションを適用
// 2. ページネーションされた結果をログ
// 3. 製品リスト形式を正規化
📐 ベストプラクティス
1. 適切なログレベルを使用
// ✅ 良い例: 異なる操作に適切なログレベルを使用
const userPipeline = compose(
withLogging(validateUser, 'debug'), // 検証用デバッグ
withLogging(saveUser, 'info'), // データベース操作用情報
withLogging(formatResponse, 'trace') // フォーマット用トレース
);
// ❌ 避けるべき: すべてに同じレベルを使用
const badPipeline = compose(
withLogging(validateUser, 'info'),
withLogging(saveUser, 'info'),
withLogging(formatResponse, 'info')
);
2. 適切なレベルでラッパーを適用
// ✅ 良い例: データベース操作にページネーションを適用
const getProducts = withPagination(
async (payload) => {
const products = await payload.context.db.products.find({}).toArray();
return ok(mergeData(payload, { products }));
}
);
// ❌ 避けるべき: すでに処理されたデータにページネーションを適用
const badGetProducts = async (payload) => {
const products = await payload.context.db.products.find({}).toArray();
return withPagination(() => ok(mergeData(payload, { products })));
};
3. 戦略的にロギングを使用
// ✅ 良い例: 適切なレベルでログ
const userPipeline = compose(
withLogging(validateUser, 'debug'), // 検証用デバッグ
withLogging(saveUser, 'info'), // データベース操作用情報
withLogging(formatResponse, 'trace') // フォーマット用トレース
);
// ✅ 良い例: 条件付きロギング
const getLoggedHandler = (isDevelopment: boolean) => {
const logLevel = isDevelopment ? 'debug' : 'info';
return withLogging(handler, logLevel);
};
4. ビジネスロジックとラッパーを合成
// ✅ 良い例: 関心事を明確に分離
const businessLogic = compose(
validateInput,
processData,
formatResponse
);
const enhancedHandler = withLogging(
withPagination(businessLogic),
'debug'
);
// ❌ 避けるべき: ラッパーロジックとビジネスロジックを混在
const mixedHandler = async (payload) => {
// ビジネスロジックにロギングロジックが混在
console.log('Starting handler');
const result = await businessLogic(payload);
console.log('Handler completed');
return result;
};
🔗 関連項目
- 合成ユーティリティ - 関数合成とエラーハンドリング
- ハンドラーコンポーネント - 基本ハンドラー概念
- ルートコンポーネント - ルート定義
- エラーハンドリング - Result型とエラーパターン