メインコンテンツまでスキップ
バージョン: 0.14.0 (最新)

⚙️ ハンドラー

ハンドラーは、完全なルートペイロードparamscontext、任意の logger)を受け取り、API 操作のビジネスロジックを実装する関数です。操作ハンドラーは通常 Result 値を返しますが、個別の引数を受け取るのではなくペイロードを直接読み書きします。ターミネーターはそれらの結果を消費し、最終的な応答形状を返します。

ハンドラー登録 API はありません。ハンドラーは SDK の handlers モジュールにエクスポートされる関数であり、ルートチェーンで直接合成されます。

移行に関する注意: SDK は applyPayloadArgs で接続するパラメーター駆動ブロックへ移行中です。authenticationprofile の多くのハンドラーには @deprecated migrate this to block のマークがあります。新しいルートではブロックを優先してください。ハンドラーは現在も categoryattributesinvitationorder、および chatorganizationproductauthentication の一部で利用されています。


🔍 ハンドラーとブロックの比較

観点ブロックハンドラー
入力抽出済みのパラメーターのみ完全な 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)
  1. 定義src/handlers/<domain>.ts でハンドラー関数をエクスポートします。
  2. 接続src/routes/<domain>.ts のルートハンドラーへ合成します。
  3. 検証src/features/<domain>.ts のスキーマとルートを組み合わせます。
  4. 提供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 };
};

ターミネーターパターン:

  1. result.isErr() を確認し、result.errorthrow します。
  2. result.value.context.data からデータを読み取ります。
  3. 最終的な整形済みオブジェクトまたは { statusCode, data } を返します。Result は返しません。

新しいルートでは、カスタムターミネーターより lift(orThrow(...)) を優先してください。ルート »およびブロック »を参照してください。


🔌 WebSocket ハンドラー

SDK は primitives に、RxJS の Subject を返す関数型 WsRouteHandlerAsyncWsRouteHandler を定義しています。第 2 引数に WebSocketServer を渡すと、defService は WebSocket ルートをサポートします。

実際には、SDK はハンドラーではなくブロックを使って WebSocket ストリーミングを実装しています。チャットメッセージストリームのルートは、blocks/chat/message.tsstreamChatMessagesResult<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、任意のドライバー(mailServicefileStorageDriver、OAuth ドライバー、findAddressDriver)、authenticateconfiguration が含まれます。[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))
),
});

チェーンフロー:

  1. バリデーター — ハンドラー処理の前に isAuthenticatedcheckIdentityType が実行されます。
  2. createCategoryparams.requestBody を読み取り、DB に挿入し、mergeData 経由で categoryId を保存します。
  3. getCategoryByIdcontext.data.categoryId を読み取り、カテゴリを取得し、mergeData 経由で category を保存します。
  4. normalizeCategoryTerminator — 内部フィールドを除去し、HTTP 応答を返します。

Result を返す非同期ハンドラーの間には flatMapAsync を使用します。


📦 SDK ハンドラーモジュール

ハンドラーは src/handlers/index.ts に対応する SDK の handlers 名前空間で、ドメインごとに整理されています。

モジュール説明参照
Attributes属性管理ハンドラー属性ハンドラー »
Authentication認証ハンドラー(多くは非推奨。ブロックへ移行中)認証ハンドラー »
Categoryカテゴリ CRUD ハンドラーカテゴリハンドラー »
Chatチャットチャンネル、メッセージ、サブスクリプションハンドラーチャットハンドラー »
Invitation招待管理ハンドラー招待ハンドラー »
Order注文管理ハンドラー注文ハンドラー »
Organization組織ハンドラー組織ハンドラー »
Product製品管理ハンドラー製品ハンドラー »
Profileプロファイルハンドラー(多くは非推奨。ブロックへ移行中)SDK のみ — ドキュメントは準備中
UtilsmergeData ユーティリティすべてのハンドラーと applyPayloadArgs で使用

🛠️ 関連ユーティリティ


➡️ 次へ

データの形状についてはスキーマ »を、ルートでのハンドラーとブロックの合成についてはルート »を確認してください。