🔍 バリデータ
バリデータは、スキーマ検証を超えた追加の検証を実行する関数です。ルートハンドラーの前に実行され、ビジネスロジック、リソースの存在、認証などをチェックできます。
🔍 バリデータとは何ですか?
バリデータは、ハンドラーの前にリクエストパイプラインで実行される述語関数です。以下のことができます:
- ビジネスロジックの検証(例:「このリソースは存在しますか?」)
- 認証のチェック(例:「ユーザーは認可されていますか?」)
- パラメータの検証(例:「これは有効なUUIDですか?」)
- 制約の適用(例:「ユーザーに権限がありますか?」)
┌─────────────┐
│ リクエスト │
├─────────────┤
│ スキーマ │ ← JSONスキーマ検証
│ バリデータ │ ← カスタムビジネスロジック
│ ハンドラー │ ← ルートロジック
└─────────────┘
主要な特徴:
- 非同期関数 - データベースクエリを実行可能
- 失敗時にスロー - 一貫したエラーレスポンスのために
NodeblocksError
を使用 - 合成可能 - 複数のバリデータをチェーン可能
- コンテキスト対応 - データベース、リクエストパラメータ、コンテキストにアクセス可能
📋 利用可能なバリデータ
汎用バリデータ
重要: レガシーパラメータバリデータ
以下のパラメータバリデータは現在レガシーインターフェースを使用しており、後で非推奨となり削除されます:
requireParam
isUUID
isNumber
これらは標準の
RouteHandlerPayload
を受け入れません。payload.params.requestParams
を抽出し、小さなラッパー経由で手動で渡す必要があります:// レガシーパラメータバリデータのラッパーパターン
({ params: { requestParams } }) => requireParam('userId')(requestParams)
({ params: { requestParams } }) => isUUID('userId')(requestParams)
({ params: { requestParams } }) => isNumber('limit')(requestParams)このレガシーインターフェースのため、これらのバリデータは
compose()
やsome()
などの合成ヘルパーに直接渡すことができません。これらのヘルパーでは、標準バリデータまたは通常のペイロード署名を持つカスタムバリデータを使用してください。
requireParam(key: string)
必須パラメータが存在し、nullでないことを確認します。
import { validators } from '@nodeblocks/backend-sdk';
const { requireParam } = validators;
const getUserRoute = withRoute({
method: 'GET',
path: '/users/:userId',
validators: [({params: { requestParams }}) => requireParam('userId')(requestParams)],
handler: getUserHandler,
});
動作:
- ✅ パラメータが存在し、nullでない場合に通過
- ❌ パラメータが欠落またはnullの場合にエラーをスロー
- エラー:
Missing required parameter: userId
isUUID(key: string)
パラメータが有効なUUID v4形式に従っていることを検証します。
import { validators } from '@nodeblocks/backend-sdk';
const { isUUID } = validators;
const getCategoryRoute = withRoute({
method: 'GET',
path: '/categories/:categoryId',
validators: [({params: { requestParams }}) => isUUID('categoryId')(requestParams)],
handler: getCategoryHandler,
});
動作:
- ✅ パラメータが有効なUUID v4形式の場合に通過
- ❌ パラメータが存在するが無効なUUIDの場合にエラーをスロー
- エラー:
Invalid UUID format for parameter: categoryId
- 形式:
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
isNumber(key: string)
パラメータが有効な数値に変換可能であることを検証します。
import { validators } from '@nodeblocks/backend-sdk';
const { isNumber } = validators;
const getProductsRoute = withRoute({
method: 'GET',
path: '/products',
validators: [({params: { requestParams }}) => isNumber('limit')(requestParams)],
handler: getProductsHandler,
});
動作:
- ✅ パラメータが数値に変換可能な場合に通過
- ❌ パラメータが存在するが数値として解析できない場合にエラーをスロー
- エラー:
Parameter limit must be a number
some(...validators: Validator[])
複数のバリデータを実行し、少なくとも1つが成功した場合に通過します。条件付き検証シナリオに役立ちます。
import { validators } from '@nodeblocks/backend-sdk';
const { some } = validators;
const flexibleUserRoute = withRoute({
method: 'GET',
path: '/users/:identifier',
validators: [
// ここでは標準バリデータを使用してください。requireParam/isUUID/isNumberのような
// レガシーパラメータバリデータは`some()`で直接使用できません。
some(validateHasUserId, validateHasEmail)
],
handler: getUserHandler,
});
動作:
- ✅ 少なくとも1つのバリデータが成功した場合に通過
- ❌ すべてのバリデータが失敗した場合にエラーをスロー
- ユースケース: 代替検証パス(例:ユーザーはIDまたはメールで識別可能)
認証バリデータ
verifyAuthentication
ルートを保護するための高階認証バリデータ。
import { validators } from '@nodeblocks/backend-sdk';
const { verifyAuthentication } = validators;
const getSecretRoute = withRoute({
method: 'GET',
path: '/secret',
validators: [verifyAuthentication(authenticateFunction)],
handler: getSecretHandler,
});
動作:
- ✅ 認証が成功した場合に通過
- ❌ 認証が失敗した場合にエラーをスロー
- 使用法: 認証関数が提供される必要があります
認証関数
認証関数はトークンを検証し、トークン情報を返す関数です。リクエストが適切に認証されているかをチェックするためにバリデータによって使用されます。
getBearerTokenInfo
(デフォルト)
AuthorizationヘッダーからのBearerトークンを検証するデフォルトの認証関数。
import { validators, utils } from '@nodeblocks/backend-sdk';
const { validateResourceAccess } = validators;
const { getBearerTokenInfo } = utils;
// デフォルト使用法(getBearerTokenInfoが自動的に使用されます)
const protectedRoute = withRoute({
method: 'GET',
path: '/protected',
validators: [
validateResourceAccess(['admin']) // デフォルトでgetBearerTokenInfoを使用
],
handler: protectedHandler,
});
// 明示的な使用法
const explicitRoute = withRoute({
method: 'GET',
path: '/protected',
validators: [
validateResourceAccess(['admin'], getBearerTokenInfo)
],
handler: protectedHandler,
});
動作原理:
Authorization: Bearer <token>
ヘッダーからトークンを抽出- JWTトークン署名を復号化および検証
- トークンタイプを検証(ユーザーアクセストークンまたはアプリアクセストークン)
- ユーザートークンに対してセキュリティチェックを実行(フィンガープリント、IP、ユーザーエージェント)
- 認可決定のためのトークン情報を返す
サポートされるトークンタイプ:
- User Access Tokens:
userId
を含み、セキュリティ検証が必要 - App Access Tokens:
appId
を含み、セキュリティチェックをバイパス
getCookieTokenInfo
Cookieベースのトークン用の代替認証関数。
import { validators } from '@nodeblocks/backend-sdk';
const { getCookieTokenInfo, validateResourceAccess } = validators;
const cookieRoute = withRoute({
method: 'GET',
path: '/cookie-protected',
validators: [
validateResourceAccess(['admin'], getCookieTokenInfo)
],
handler: protectedHandler,
});
ユースケース:
- Cookieベース認証を使用するWebアプリケーション
- 異なるセキュリティ検証ルール(IPチェックが無効)
- サーバー側セッション管理
カスタム認証関数
特定のユースケースのためにカスタム認証関数を作成できます:
import { primitives, validators } from '@nodeblocks/backend-sdk';
const { Authenticator, RouteHandlerPayload, TokenInfo, NodeblocksError } = primitives;
const { validateResourceAccess } = validators;
const customAuth: Authenticator = async (payload: RouteHandlerPayload): Promise<TokenInfo> => {
const { context } = payload;
const { request } = context;
// カスタムトークン抽出ロジック
const customToken = request.headers['x-custom-token'];
if (!customToken) {
throw new NodeblocksError(401, 'Custom token required');
}
// カスタム検証ロジック
// ... ここに検証コードを記述
// トークン情報を返す
return {
accessType: 'user',
userId: 'user-123',
type: 'access'
};
};
// カスタム認証を使用した使用法
const customRoute = withRoute({
method: 'GET',
path: '/custom-protected',
validators: [
validateResourceAccess(['admin'], customAuth)
],
handler: protectedHandler,
});
認証関数シグネチャ:
type Authenticator = (payload: RouteHandlerPayload) => Promise<TokenInfo>;
ドメイン固有のバリデータ
カテゴリサービス
doesCategoryExist
操作を実行する前に、カテゴリがデータベースに存在することを検証します。
import { validators } from '@nodeblocks/backend-sdk';
const { doesCategoryExist } = validators;
const updateCategoryRoute = withRoute({
method: 'PATCH',
path: '/categories/:categoryId',
validators: [doesCategoryExist],
handler: updateCategoryHandler,
});
動作:
- ✅ カテゴリがデータベースに存在する場合に通過
- ❌ カテゴリが見つからない場合に
NodeblocksError
(404)をスロー - エラー:
Category does not exist
(404ステータス)
アクセス制御バリデータ
validateResourceAccess
許可されたサブジェクトとトークン情報に基づいてリソースアクセスを検証します。保護されたリソースのロールベースアクセス制御を実装します。
import { validators } from '@nodeblocks/backend-sdk';
const { validateResourceAccess } = validators;
const adminRoute = withRoute({
method: 'GET',
path: '/admin/users',
validators: [
validateResourceAccess(['admin'])
],
handler: getUsersHandler,
});
const selfAccessRoute = withRoute({
method: 'GET',
path: '/users/:userId',
validators: [
validateResourceAccess(['self', 'admin'])
],
handler: getUserHandler,
});
設定:
{
allowedSubjects: string[] // 許可されたユーザータイプ/サブジェクトの配列
}
サポートされるサブジェクト:
'admin'
- 管理者アクセス'user'
- 通常ユーザーアクセス'guest'
- ゲストアクセス'self'
- ユーザーは自分のリソースのみアクセス可能
動作:
- ✅ ユーザーに適切な権限がある場合に通過
- ❌ 無効なトークンの場合に
NodeblocksError
(401)をスロー - ❌ 認可されていないアクセスの場合に
NodeblocksError
(403)をスロー - エラー:
App token is not valid
(401)User token is not valid
(401)Identity not found
(401)User is not authorized to access this resource
(403)
validateChannelAccess
所有権とトークン情報に基づいてチャンネルアクセスを検証します。
import { validators } from '@nodeblocks/backend-sdk';
const { validateChannelAccess } = validators;
const channelRoute = withRoute({
method: 'PATCH',
path: '/channels/:channelId',
validators: [
validateChannelAccess(['owner'])
],
handler: updateChannelHandler,
});
サポートされるサブジェクト:
'owner'
- チャンネル所有者アクセス
動作:
- ✅ ユーザーがチャンネル所有者の場合に通過
- ❌ 無効なトークンの場合に
NodeblocksError
(401)をスロー - ❌ 認可されていないアクセスの場合に
NodeblocksError
(403)をスロー - ❌ チャンネルが見つからない場合に
NodeblocksError
(404)をスロー - エラー:
App token is not valid
(401)User token is not valid
(401)Channel not found
(404)Channel has no owner
(403)User is not authorized to access this channel
(403)
validateMessageAccess
所有権とトークン情報に基づいてメッセージアクセスを検証します。
import { validators } from '@nodeblocks/backend-sdk';
const { validateMessageAccess } = validators;
const messageRoute = withRoute({
method: 'DELETE',
path: '/messages/:messageId',
validators: [
validateMessageAccess(['owner'])
],
handler: deleteMessageHandler,
});
サポートされるサブジェクト:
'owner'
- メッセージ送信者アクセス
動作:
- ✅ ユーザーがメッセージ送信者の場合に通過
- ❌ 無効なトークンの場合に
NodeblocksError
(401)をスロー - ❌ 認可されていないアクセスの場合に
NodeblocksError
(403)をスロー - ❌ メッセージが見つからない場合に
NodeblocksError
(404)をスロー - エラー:
App token is not valid
(401)User token is not valid
(401)Message not found
(404)Message has no sender
(403)User is not authorized to access this message
(403)
validateOrderAccess
所有権とトークン情報に基づいてオーダーアクセスを検証します。
import { validators } from '@nodeblocks/backend-sdk';
const { validateOrderAccess } = validators;
const orderRoute = withRoute({
method: 'GET',
path: '/orders/:orderId',
validators: [
validateOrderAccess(['owner'])
],
handler: getOrderHandler,
});
サポートされるサブジェクト:
'owner'
- オーダー所有者アクセス
動作:
- ✅ ユーザーがオーダー所有者の場合に通過
- ❌ 無効なトークンの場合に
NodeblocksError
(401)をスロー - ❌ 認可されていないアクセスの場合に
NodeblocksError
(403)をスロー - ❌ オーダー見つからない場合に
NodeblocksError
(404)をスロー - エラー:
App token is not valid
(401)User token is not valid
(401)Order not found
(404)Order has no user
(403)User is not authorized to access this order
(403)
validateUserProfileAccess
ユーザータイプと所有権に基づいてユーザープロファイルアクセスを検証します。
import { validators } from '@nodeblocks/backend-sdk';
const { validateUserProfileAccess } = validators;
const profileRoute = withRoute({
method: 'GET',
path: '/users/:userId/profile',
validators: [
validateUserProfileAccess(['self', 'admin'])
],
handler: getUserProfileHandler,
});
サポートされるサブジェクト:
'admin'
- 管理者アクセス'user'
- 通常ユーザーアクセス'guest'
- ゲストアクセス'self'
- ユーザーは自分のプロファイルにアクセス可能
動作:
- ✅ ユーザーに適切な権限がある場合に通過
- ❌ 無効なトークンの場合に
NodeblocksError
(401)をスロー - ❌ 認可されていないアクセスの場合に
NodeblocksError
(403)をスロー - ❌ ユーザープロファイルが見つからない場合に
NodeblocksError
(404)をスロー - エラー:
App token is not valid
(401)User token is not valid
(401)Identity not found
(401)User profile not found
(404)Identity is not authorized to access this resource
(403)
validateOrganizationAccess
メンバーシップとロールに基づいて組織アクセスを検証します。
import { validators } from '@nodeblocks/backend-sdk';
const { validateOrganizationAccess } = validators;
const orgRoute = withRoute({
method: 'GET',
path: '/organizations/:organizationId',
validators: [
validateOrganizationAccess(['admin', 'member'])
],
handler: getOrganizationHandler,
});
サポートされるサブジェクト:
'admin'
- 組織管理者'member'
- 組織メンバー'owner'
- 組織所有者
動作:
- ✅ ユーザーが適切なロールを持つ組織メンバーの場合に通過
- ❌ 無効なトークンの場合に
NodeblocksError
(401)をスロー - ❌ 認可されていないアクセスの場合に
NodeblocksError
(403)をスロー - ❌ 組織が見つからない場合に
NodeblocksError
(404)をスロー - エラー:
App token is not valid
(401)User token is not valid
(401)Organization not found
(404)Organization has no users
(403)User does not belong to this organization
(403)User is not authorized to access this organization
(403)
🛠️ カスタムバリデータの作成
バリデータ関数シグネチャ
type Validator = (payload: RouteHandlerPayload) => Promise<void>;
基本的なバリデータの例
import { primitives } from '@nodeblocks/backend-sdk';
const { NodeblocksError } = primitives;
const validateUserExists = async (payload: RouteHandlerPayload) => {
const { context, params } = payload;
const userId = params.requestParams?.userId;
const user = await context.db.users.findOne({ id: userId });
if (!user) {
throw new NodeblocksError(404, 'User not found', 'USER_NOT_FOUND');
}
};
高度なバリデータの例
import { primitives } from '@nodeblocks/backend-sdk';
const { NodeblocksError, RouteHandlerPayload } = primitives;
const validateUserPermission = async (payload: RouteHandlerPayload) => {
const { context, params } = payload;
const { userId, organizationId } = params.requestParams;
// ユーザーが存在するかチェック
const user = await context.db.users.findOne({ id: userId });
if (!user) {
throw new NodeblocksError(404, 'User not found', 'USER_NOT_FOUND');
}
// ユーザーが組織に属しているかチェック
const membership = await context.db.memberships.findOne({
userId,
organizationId,
status: 'active'
});
if (!membership) {
throw new NodeblocksError(403, 'Access denied', 'ACCESS_DENIED');
}
};
ファクトリ関数パターン
再利用可能なバリデータファクトリを作成します:
const requireResourceExists = (collectionName: string, paramName: string) => {
return async (payload: RouteHandlerPayload) => {
const { context, params } = payload;
const resourceId = params.requestParams?.[paramName];
const resource = await context.db[collectionName].findOne({ id: resourceId });
if (!resource) {
throw new NodeblocksError(
404,
`${collectionName} not found`,
'RESOURCE_NOT_FOUND'
);
}
};
};
// 使用法
const validateProductExists = requireResourceExists('products', 'productId');
const validateOrderExists = requireResourceExists('orders', 'orderId');
🔗 ルートでのバリデータの使用
単一バリデータ
export const getCategoryRoute = withRoute({
method: 'GET',
path: '/categories/:categoryId',
validators: [doesCategoryExist],
handler: getCategoryHandler,
});
複数バリデータ
export const updateUserRoute = withRoute({
method: 'PATCH',
path: '/users/:userId',
validators: [
({params: { requestParams }}) => requireParam('userId')(requestParams),
({params: { requestParams }}) => isUUID('userId')(requestParams),
validateUserExists,
verifyAuthentication(authenticate)
],
handler: updateUserHandler,
});
認証 + 認可
export const protectedUserRoute = withRoute({
method: 'PATCH',
path: '/users/:userId',
validators: [
({params: { requestParams }}) => requireParam('userId')(requestParams),
({params: { requestParams }}) => isUUID('userId')(requestParams),
verifyAuthentication(authenticate),
validateResourceAccess(['admin', 'self'])
],
handler: updateUserHandler,
});
条件付きバリデータ
const validators = [
({params: { requestParams }}) => requireParam('userId')(requestParams),
({params: { requestParams }}) => isUUID('userId')(requestParams),
// 非公開ルートでのみ認証をチェック
...(isPublicRoute ? [] : [verifyAuthentication(authenticate)])
];
export const getUserRoute = withRoute({
method: 'GET',
path: '/users/:userId',
validators,
handler: getUserHandler,
});
代替検証パス
export const flexibleUserRoute = withRoute({
method: 'GET',
path: '/users/:identifier',
validators: [
// ここでは標準バリデータを使用してください。requireParam/isUUID/isNumberのような
// レガシーパラメータバリデータは`some()`で直接使用できません。
some(validateHasUserId, validateHasEmail),
validateUserExists
],
handler: getUserHandler,
});
🚨 エラーハンドリング
NodeblocksErrorの使用
一貫したエラーレスポンスのために常にNodeblocksError
を使用します:
import { primitives } from '@nodeblocks/backend-sdk';
const { NodeblocksError, RouteHandlerPayload } = primitives;
const validateResource = async (payload: RouteHandlerPayload) => {
// 検証ロジック
if (!isValid) {
throw new NodeblocksError(
400, // HTTPステータスコード
'Validation failed', // 人間が読めるメッセージ
'VALIDATION_ERROR' // クライアント処理のためのエラーコード
);
}
};
エラーレスポンス形式
バリデータがエラーをスローすると、クライアントは以下を受け取ります:
{
"error": {
"message": "Category does not exist"
}
}
📋 バリデータリファレンス
バリデータ | 目的 | パラメータ | 戻り値 | スロー |
---|---|---|---|---|
requireParam | パラメータの存在をチェック | key: string | リクエストパラメータを検証する関数 | null/欠落の場合にエラー |
isUUID | UUID形式を検証 | key: string | リクエストパラメータを検証する関数 | 無効なUUIDの場合にエラー |
isNumber | 数値形式を検証 | key: string | リクエストパラメータを検証する関数 | 数値でない場合にエラー |
some | 代替検証パス | ...validators: Validator[] | Validator | すべて失敗した場合にエラー |
doesCategoryExist | カテゴリが存在するかチェック | none | Validator | NodeblocksError (404) |
verifyAuthentication | 認証を検証 | authenticate function | Validator | 認証エラー |
validateResourceAccess | リソース権限をチェック | allowedSubjects: string[], authenticate function | Validator | NodeblocksError (401/403) |
validateChannelAccess | チャンネル所有権をチェック | allowedSubjects: string[], authenticate function | Validator | NodeblocksError (401/403/404) |
validateMessageAccess | メッセージ所有権をチェック | allowedSubjects: string[], authenticate function | Validator | NodeblocksError (401/403/404) |
validateOrderAccess | オーダー所有権をチェック | allowedSubjects: string[], authenticate function | Validator | NodeblocksError (401/403/404) |
validateUserProfileAccess | ユーザープロファイルアクセスをチェック | allowedSubjects: string[], authenticate function | Validator | NodeblocksError (401/403/404) |
validateOrganizationAccess | 組織アクセスをチェック | allowedSubjects: string[], authenticate function | Validator | NodeblocksError (401/403/404) |
📐 ベストプラクティス
1. Fail Fast
- バリデータはハンドラーの前に実行されるため、無効なリクエストに対して早期に失敗する
- 必要な場合を除き、バリデータで高価な操作を実行しない
2. 再利用可能なパターン
- 一般的な検証パターンのファクトリ関数を作成
- ビジネスロジックにドメイン固有のバリデータを使用
3. 明確なエラーメッセージ
- 説明的なエラーメッセージを提供
- クライアント側処理のために一貫したエラーコードを使用
4. データベース効率
- バリデータクエリにデータベースインデックスを使用
- 頻繁にアクセスされるデータにはキャッシュを検討
5. 合成
- 論理的にバリデータをチェーン(必須 → 形式 → ビジネスロジック)
- バリデータを単一の責任に集中させる
6. アクセス制御
- 保護されたリソースに適切なアクセス制御バリデータを使用
- 認証と認可バリデータを組み合わせる
- 柔軟な検証シナリオのために
some()
の使用を検討