メインコンテンツまでスキップ
バージョン: 0.4.2

🔍 バリデーター

バリデーターは、スキーマ検証を超えた追加の検証を実行する関数です。ルートハンドラーの前に実行され、ビジネスロジック、リソースの存在、認証などをチェックできます。


🔍 バリデーターとは何ですか?

バリデーターは、ハンドラーの前にリクエストパイプラインで実行される述語関数です。以下のことができます:

  • ビジネスロジックの検証(例:「このリソースは存在するか?」)
  • 認証のチェック(例:「ユーザーは認証されているか?」)
  • パラメータの検証(例:「これは有効なUUIDか?」)
  • 制約の強制(例:「ユーザーは権限を持っているか?」)
┌─────────────┐
│ Request │
├─────────────┤
│ Schema │ ← JSON Schema検証
│ Validators │ ← カスタムビジネスロジック
│ Handler │ ← ルートロジック
└─────────────┘

主要な特徴:

  • 非同期関数 - データベースクエリを実行可能
  • 失敗時にスロー - 一貫したエラーレスポンスにNodeblocksErrorを使用
  • 構成可能 - 複数のバリデーターをチェーン
  • コンテキスト認識 - データベース、リクエストパラメータ、コンテキストにアクセス

📋 利用可能なバリデーター

汎用バリデーター

requireParam(key: string)

必須パラメータが存在しnullでないことを確認します。

import { validators } from '@nodeblocks/backend-sdk';

const { requireParam } = validators;

const getUserRoute = withRoute({
method: 'GET',
path: '/users/:userId',
validators: [requireParam('userId')],
handler: getUserHandler,
});

動作:

  • ✅ パラメータが存在しnullでない場合は通過
  • ❌ パラメータが欠けているかnullの場合はエラーをスロー
  • エラー: 必須パラメータが不足しています: userId

isUUID(key: string)

パラメータが有効なUUID v4形式に従うことを検証します。

import { validators } from '@nodeblocks/backend-sdk';

const { isUUID } = validators;

const getCategoryRoute = withRoute({
method: 'GET',
path: '/categories/:categoryId',
validators: [isUUID('categoryId')],
handler: getCategoryHandler,
});

動作:

  • ✅ 有効なUUID形式の場合は通過
  • ❌ 無効なUUID形式の場合はエラーをスロー
  • エラー: 無効なUUID形式: categoryId

isNumber(key: string)

パラメータが有効な数値であることを検証します。

import { validators } from '@nodeblocks/backend-sdk';

const { isNumber } = validators;

const getPaginatedRoute = withRoute({
method: 'GET',
path: '/items?page=:page',
validators: [isNumber('page')],
handler: getPaginatedHandler,
});

動作:

  • ✅ 有効な数値の場合は通過
  • ❌ 無効な数値の場合はエラーをスロー
  • エラー: 数値が必要です: page

🔐 認証バリデーター

verifyAuthentication(authenticator)

リクエストが有効な認証トークンを含むことを検証します。

import { validators } from '@nodeblocks/backend-sdk';

const { verifyAuthentication, getBearerTokenInfo } = validators;

const protectedRoute = withRoute({
method: 'GET',
path: '/protected',
validators: [verifyAuthentication(getBearerTokenInfo)],
handler: protectedHandler,
});

動作:

  • ✅ 有効なBearerトークンで認証された場合は通過
  • ❌ 認証トークンが無効または欠けている場合はエラーをスロー
  • エラー: 認証が必要です

validateResourceAccess(roles, authenticator)

ユーザーがリソースにアクセスするための適切な役割を持つことを検証します。

import { validators } from '@nodeblocks/backend-sdk';

const { validateResourceAccess, getBearerTokenInfo } = validators;

const adminRoute = withRoute({
method: 'DELETE',
path: '/admin/users/:userId',
validators: [
verifyAuthentication(getBearerTokenInfo),
validateResourceAccess(['admin'], getBearerTokenInfo)
],
handler: deleteUserHandler,
});

サポートされる役割:

  • 'admin' - 管理者レベルアクセス
  • 'self' - ユーザー自身のリソースへのアクセス
  • 'owner' - リソース所有者アクセス

🎯 カスタムバリデーターの作成

基本的なカスタムバリデーター

import { NodeblocksError } from '@nodeblocks/backend-sdk';

export const validateEmailDomain = (allowedDomains: string[]) =>
(payload: RouteHandlerPayload) => {
const { requestBody } = payload.params;
const email = requestBody.email;

if (!email) {
throw new NodeblocksError(400, 'メールアドレスが必要です');
}

const domain = email.split('@')[1];
if (!allowedDomains.includes(domain)) {
throw new NodeblocksError(400, `許可されていないメールドメインです: ${domain}`);
}
};

// 使用例
const registerRoute = withRoute({
method: 'POST',
path: '/register',
validators: [validateEmailDomain(['company.com', 'example.org'])],
handler: registerHandler,
});

非同期バリデーター

export const validateUserExists = (userIdParam: string) => 
async (payload: RouteHandlerPayload) => {
const { context, params } = payload;
const userId = params.requestParams[userIdParam];

const user = await context.db.users.findOne({ id: userId });
if (!user) {
throw new NodeblocksError(404, `ユーザーが見つかりません: ${userId}`);
}

// ユーザー情報をコンテキストに追加
payload.context.data = { ...payload.context.data, user };
};

// 使用例
const updateUserRoute = withRoute({
method: 'PATCH',
path: '/users/:userId',
validators: [
requireParam('userId'),
isUUID('userId'),
validateUserExists('userId')
],
handler: updateUserHandler,
});

複合バリデーター

export const validateBusinessHours = () => 
(payload: RouteHandlerPayload) => {
const now = new Date();
const hour = now.getHours();
const day = now.getDay();

// 週末(土曜日=6、日曜日=0)をチェック
if (day === 0 || day === 6) {
throw new NodeblocksError(400, 'このサービスは平日のみ利用可能です');
}

// 営業時間(9時-17時)をチェック
if (hour < 9 || hour >= 17) {
throw new NodeblocksError(400, 'このサービスは営業時間内(9:00-17:00)のみ利用可能です');
}
};

export const validateRateLimit = (maxRequests: number, windowMs: number) => {
const requests = new Map<string, number[]>();

return (payload: RouteHandlerPayload) => {
const clientIP = payload.context.request.ip;
const now = Date.now();

// クライアントのリクエスト履歴を取得
const clientRequests = requests.get(clientIP) || [];

// 時間窓外の古いリクエストを削除
const validRequests = clientRequests.filter(
timestamp => now - timestamp < windowMs
);

// レート制限をチェック
if (validRequests.length >= maxRequests) {
throw new NodeblocksError(429, 'レート制限に達しました。しばらくしてから再試行してください');
}

// 新しいリクエストを記録
validRequests.push(now);
requests.set(clientIP, validRequests);
};
};

🔧 バリデーターの組み合わせ

バリデーターチェーン

// 複数のバリデーターを順序よく実行
const createUserRoute = withRoute({
method: 'POST',
path: '/users',
validators: [
verifyAuthentication(getBearerTokenInfo),
validateResourceAccess(['admin'], getBearerTokenInfo),
validateEmailDomain(['company.com']),
validateBusinessHours(),
validateRateLimit(10, 60000) // 1分間に10リクエスト
],
handler: createUserHandler,
});

条件付きバリデーション

export const conditionalValidator = (condition: (payload: RouteHandlerPayload) => boolean, validator: Validator) =>
(payload: RouteHandlerPayload) => {
if (condition(payload)) {
return validator(payload);
}
};

// 使用例
const updateRoute = withRoute({
method: 'PATCH',
path: '/items/:id',
validators: [
conditionalValidator(
payload => payload.params.requestBody.status === 'published',
validateResourceAccess(['admin'], getBearerTokenInfo)
)
],
handler: updateHandler,
});

🎯 バリデーターのベストプラクティス

1. 早期失敗

// 良い - 早期に安価なチェックを実行
const validators = [
requireParam('userId'), // 高速
isUUID('userId'), // 高速
verifyAuthentication(getBearerTokenInfo), // 中程度
validateUserExists('userId') // 低速(DB呼び出し)
];

2. 意味のあるエラーメッセージ

// 良い - 具体的なエラーメッセージ
throw new NodeblocksError(400, 'メールアドレスは@company.comドメインである必要があります');

// 悪い - 曖昧なエラーメッセージ
throw new NodeblocksError(400, '無効な入力');

3. 適切なHTTPステータスコード

// 認証エラー
throw new NodeblocksError(401, '認証が必要です');

// 権限エラー
throw new NodeblocksError(403, 'このリソースへのアクセス権限がありません');

// リソースが見つからない
throw new NodeblocksError(404, 'ユーザーが見つかりません');

// バリデーションエラー
throw new NodeblocksError(400, '無効なメールアドレス形式です');

➡️ 次へ

ハンドラーについて学習して、バリデーション後にビジネスロジックがどのように実行されるかを理解しましょう!