⚙️ ハンドラー
ハンドラーは、完全なルートペイロード(params、context、任意の logger)を受け取り、API 操作のビジネスロジックを実装する関数です。操作ハンドラーは通常 Result 値を返しますが、個別の引数を受け取るのではなくペイロードを直接読み書きします。ターミネーターはそれらの結果を消費し、最終的な応答形状を返します。
ハンドラー登録 API はありません。ハンドラーは SDK の handlers モジュールにエクスポートされる関数であり、ルートチェーンで直接合成されます。
移行に関する注意: SDK は
applyPayloadArgsで接続するパラメーター駆動ブロックへ移行中です。authenticationとprofileの多くのハンドラーには@deprecated migrate this to blockのマークがあります。新しいルートではブロックを優先してください。ハンドラーは現在も category、attributes、invitation、order、および chat、organization、product、authentication の一部で利用されています。
🔍 ハンドラーとブロックの比較
| 観点 | ブロック | ハンドラー |
|---|---|---|
| 入力 | 抽出済みのパラメーターのみ | 完全な RouteHandlerPayload |
| 接続 | applyPayloadArgs(block, paths, key) | compose() に直接渡す |
| テスト容易性 | プレーンな引数を渡す | ペイロードフィクスチャが必要 |
Block: getIdentityById(db, identityId) → Result<identity, Error>
Handler: getCategoryById(payload) → Result<payload with context.data, Error>
新しいルートのパラメーター駆動ロジックにはブロックを使用してください。ペイロードへ直接アクセスする必要があるレガシールートの保守にはハンドラーを使用します。推奨パターンについてはブロック »を参照してください。
📋 ハンドラーのライフサイクル
handlers/*.ts → routes/*.ts → features/*.ts → defService()
(define) (compose) (schema + route) (serve)
- 定義 —
src/handlers/<domain>.tsでハンドラー関数をエクスポートします。 - 接続 —
src/routes/<domain>.tsのルートハンドラーへ合成します。 - 検証 —
src/features/<domain>.tsのスキーマとルートを組み合わせます。 - 提供 —
defService(feature)で機能をマウントします。
🧑💻 HTTP ハンドラーの定義
HTTP 操作ハンドラーはデータベース呼び出し、検証、変換などを行い、通常 Result<RouteHandlerPayload, Error> を返します。一般的なエラークラスは NodeblocksError です。
import { ok, err, Result } from 'neverthrow';
import { handlers, primitives, utils } from '@nodeblocks/backend-sdk';
const { mergeData } = handlers;
const { createBaseEntity } = utils;
// 非同期ハンドラー — handlers/category.ts のパターン
export const createCategory: primitives.AsyncRouteHandler<
Result<primitives.RouteHandlerPayload, Error>
> = async (payload) => {
const { params, context } = payload;
if (!params.requestBody || Object.keys(params.requestBody).length === 0) {
return err(
new primitives.NodeblocksError(400, 'Request body is required', 'createCategory')
);
}
const baseEntity = createBaseEntity(params.requestBody);
try {
const createdCategory = await context.db.categories.insertOne(baseEntity);
if (!createdCategory.insertedId) {
return err(
new primitives.NodeblocksError(400, 'Failed to create category', 'createCategory')
);
}
return ok(mergeData(payload, { categoryId: baseEntity.id }));
} catch {
return err(
new primitives.NodeblocksError(500, 'Failed to create category', 'createCategory')
);
}
};
ハンドラーは同期(RouteHandler)または非同期(AsyncRouteHandler)にできます。
mergeData
handlers からエクスポートされる mergeData は、Ramda の mergeDeepRight を使用して値を payload.context.data にマージします。既存のキーは保持されます。
return ok(mergeData(payload, { categoryId: baseEntity.id }));
// 後続ステップで読み取る値: context.data?.categoryId
flatMapAsync とともに使用するハンドラーは、戻り値を ok()(失敗時は err())でラップする必要があります。mergeData はブロックルートの applyPayloadArgs 内部でも使用されます。
mergeData の完全なドキュメントは合成ユーティリティ »を参照してください。
🏁 ターミネーターハンドラー
ターミネーターハンドラーはハンドラーチェーンの最終ステップです。前のハンドラーから Result を受け取り、エラー時にはスローし、Result ではないプレーンな応答データを返します。
正規化ターミネーター
整形済みの応答オブジェクトを返します。
import { primitives } from '@nodeblocks/backend-sdk';
import { Result } from 'neverthrow';
// handlers/category.ts のパターン — normalizeCategoryTerminator
export const normalizeCategoryTerminator = (
result: Result<primitives.RouteHandlerPayload, Error>
) => {
if (result.isErr()) {
throw result.error;
}
const { context } = result.value;
if (!context.data?.category) {
throw new primitives.NodeblocksError(
500,
'Unknown error normalizing category',
'normalizeCategoryTerminator'
);
}
const { _id, ...category } = context.data.category;
return category;
};
ステータスコードターミネーター
ステータスコードを含む構造化 HTTP 応答を返します。
// handlers/category.ts のパターン — deleteCategoryTerminator
export const deleteCategoryTerminator = (
result: Result<primitives.RouteHandlerPayload, Error>
): { statusCode: number } => {
if (result.isErr()) {
throw result.error;
}
if (!result.value.context.data?.deleteCategory) {
throw new primitives.NodeblocksError(
500,
'Unknown error deleting category',
'deleteCategoryTerminator'
);
}
return { statusCode: 204 };
};
ターミネーターパターン:
result.isErr()を確認し、result.errorをthrowします。result.value.context.dataからデータを読み取ります。- 最終的な整形済みオブジェクトまたは
{ statusCode, data }を返します。Resultは返しません。
新しいルートでは、カスタムターミネーターより lift(orThrow(...)) を優先してください。ルート »およびブロック »を参照してください。
🔌 WebSocket ハンドラー
SDK は primitives に、RxJS の Subject を返す関数型 WsRouteHandler と AsyncWsRouteHandler を定義しています。第 2 引数に WebSocketServer を渡すと、defService は WebSocket ルートをサポートします。
実際には、SDK はハンドラーではなくブロックを使って WebSocket ストリーミングを実装しています。チャットメッセージストリームのルートは、blocks/chat/message.ts の streamChatMessages(Result<Subject<T>, Error> を返す)を使用し、applyPayloadArgs で接続して lift(orThrow(...)) で終わります。
カスタム WebSocket ハンドラー用の型は次のとおりです。
type WsRouteHandler<T = any> = (wsPayload: WsRouteHandlerPayload) => Subject<T>;
type AsyncWsRouteHandler<T = any> = (wsPayload: WsRouteHandlerPayload) => Promise<Subject<T>>;
型定義の WsRouteHandlerPayload には params が含まれませんが、defService は接続時に params.requestQuery(WebSocket URL から解析)を注入します。
完全な WebSocket パターンについてはWebSocket サービスの作成 »を参照してください。
🔄 ペイロード構造
HTTP ハンドラーは primitives.RouteHandlerPayload を受け取ります。
type RouteHandlerPayload = {
params: {
requestBody?: Record<string, any>;
requestParams?: Record<string, any>;
requestQuery?: Record<string, any>;
};
context: ServiceContext;
logger?: Logger;
[prop: string]: unknown;
};
ServiceContext には、db、任意のドライバー(mailService、fileStorageDriver、OAuth ドライバー、findAddressDriver)、authenticate、configuration が含まれます。[entityName: string]: any インデックスにより、追加フィールドを利用できます。
defService は HTTP ルートの実行時に次を注入します。
context.request— Express リクエストcontext.response— Express レスポンスcontext.data— ハンドラーチェーン中にmergeData/applyPayloadArgsが蓄積するデータ
🧑💻 完全なハンドラーチェーン例
routes/category.ts より。ハンドラーをターミネーターと合成します。バリデーターはハンドラーチェーンの前に実行され、withLogging は可観測性のためにステップをラップします。
import { handlers, primitives, validators } from '@nodeblocks/backend-sdk';
const {
createCategory,
getCategoryById,
normalizeCategoryTerminator,
} = handlers;
const { compose, flatMapAsync, lift, withLogging, withRoute } = primitives;
const { isAuthenticated, checkIdentityType } = validators;
export const createCategoryRoute = withRoute({
method: 'POST',
path: '/categories',
validators: [isAuthenticated(), checkIdentityType(['admin'])],
handler: compose(
withLogging(createCategory),
flatMapAsync(withLogging(getCategoryById)),
lift(withLogging(normalizeCategoryTerminator))
),
});
チェーンフロー:
- バリデーター — ハンドラー処理の前に
isAuthenticated、checkIdentityTypeが実行されます。 createCategory—params.requestBodyを読み取り、DB に挿入し、mergeData経由でcategoryIdを保存します。getCategoryById—context.data.categoryIdを読み取り、カテゴリを取得し、mergeData経由でcategoryを保存します。normalizeCategoryTerminator— 内部フィールドを除去し、HTTP 応答を返します。
Result を返す非同期ハンドラーの間には flatMapAsync を使用します。
📦 SDK ハンドラーモジュール
ハンドラーは src/handlers/index.ts に対応する SDK の handlers 名前空間で、ドメインごとに整理されています。
| モジュール | 説明 | 参照 |
|---|---|---|
| Attributes | 属性管理ハンドラー | 属性ハンドラー » |
| Authentication | 認証ハンドラー(多くは非推奨。ブロックへ移行中) | 認証ハンドラー » |
| Category | カテゴリ CRUD ハンドラー | カテゴリハンドラー » |
| Chat | チャットチャンネル、メッセージ、サブスクリプションハンドラー | チャットハンドラー » |
| Invitation | 招待管理ハンドラー | 招待ハンドラー » |
| Order | 注文管理ハンドラー | 注文ハンドラー » |
| Organization | 組織ハンドラー | 組織ハンドラー » |
| Product | 製品管理ハンドラー | 製品ハンドラー » |
| Profile | プロファイルハンドラー(多くは非推奨。ブロックへ移行中) | SDK のみ — ドキュメントは準備中 |
| Utils | mergeData ユーティリティ | すべてのハンドラーと applyPayloadArgs で使用 |
🛠️ 関連ユーティリティ
- 合成ユーティリティ —
compose、flatMapAsync、lift、mergeData - エンティティユーティリティ —
createBaseEntityとエンティティ管理 - ブロック » —
applyPayloadArgsで接続するパラメーター駆動関数(新しいルートで推奨)