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

🔧 カスタムサービスの作成

このガイドでは、NodeBlocks SDKを使用して完全なカスタムサービスを作成する方法を説明します。すべての主要なパターンとコンポーネントを実証するレビュサービスを構築します。このシンプルな例では、ユーザーが製品レビューを作成および取得できるようになります。

📦 必要なパッケージ: この例では ramdaneverthrow パッケージを使用します。必ずインストールしてください:

npm install ramda neverthrow

🏗️ サービスアーキテクチャ

サービスを構築するため、以下のコアコンポーネントを実装します:

  1. スキーマ - データ検証とTypeScript型
  2. ハンドラー - ビジネスロジック関数
  3. ルート - HTTPエンドポイント定義
  4. 機能 - スキーマ + ルートの合成
  5. サービス - すべてを接続するファクトリ関数

1️⃣ スキーマ定義

まず、src/schemas ディレクトリ内に review.ts ファイルを作成します。 ここでは、必須フィールド productId(文字列)と rating(1-5の数値)、およびオプションの comment フィールドを含むスキーマを定義します。

src/schemas/review.ts
import { primitives } from '@nodeblocks/backend-sdk';

const { withSchema } = primitives;

const reviewIdPathParameter: primitives.OpenAPIParameter = {
in: 'path', // パラメータの場所
name: 'reviewId', // パラメータ名
required: true, // 必須かどうか
schema: {
type: 'string', // 型
},
};

export const reviewSchema: primitives.SchemaDefinition = {
$schema: 'http://json-schema.org/draft-07/schema#',
additionalProperties: false, // 追加プロパティを許可しない
properties: {
productId: { type: 'string' }, // 製品ID
rating: { type: 'number', minimum: 1, maximum: 5 }, // 評価(1-5)
comment: { type: 'string' }, // コメント
},
type: 'object',
};

export const createReviewSchema = withSchema({
requestBody: {
content: {
'application/json': {
schema: { ...reviewSchema, required: ['productId', 'rating'] }, // 必須フィールド
},
},
required: true, // リクエストボディ必須
},
});

export const updateReviewSchema = withSchema({
parameters: [{ ...reviewIdPathParameter }], // パスパラメータ
requestBody: {
content: {
'application/json': {
schema: {
additionalProperties: false,
properties: {
comment: { type: 'string' }, // 更新可能なコメントフィールド
},
required: [], // 必須フィールドなし
type: 'object',
},
},
},
required: true,
},
});

2️⃣ ハンドラー作成

次に、src/handlers ディレクトリ内に review.ts ファイルを作成します。 レビュサービスのビジネスロジックを含むハンドラーを追加します。これらの関数は、レビューの作成、IDによる取得、およびデータ形式の正規化を処理します。

src/handlers/review.ts
import { Result, ok, err } from 'neverthrow';
import { primitives, handlers, utils } from '@nodeblocks/backend-sdk';

const { NodeblocksError } = primitives;
const { mergeData } = handlers;
const { createBaseEntity } = utils;

export const startTransaction = async (
payload: primitives.RouteHandlerPayload
) => {
const { context } = payload;
const session = await context.db.startSession(); // DBセッション開始
session.startTransaction(); // トランザクション開始
return ok(mergeData(payload, { session })); // ペイロードにセッションを追加
};

export const commitTransaction = async (session: any) => {
await session.commitTransaction(); // トランザクションコミット
await session.endSession(); // セッション終了
};

export const createReview: primitives.AsyncRouteHandler<
Result<primitives.RouteHandlerPayload, Error>
> = async payload => {
const { params, context } = payload;
const entity = createBaseEntity(params.requestBody || {}); // ベースエンティティ作成
const res = await context.db.reviews.insertOne(entity); // DBに挿入
if (!res.insertedId)
return err(new NodeblocksError(400, 'Failed to create review')); // 作成失敗
return ok(mergeData(payload, { reviewId: entity.id })); // 成功レスポンス
};

export const getReviewById: primitives.AsyncRouteHandler<
Result<primitives.RouteHandlerPayload, Error>
> = async payload => {
const { context, params } = payload;
const id = context.data?.reviewId || params.requestParams?.reviewId; // ID取得
const review = await context.db.reviews.findOne({ id }); // DBから検索
if (!review) return err(new NodeblocksError(404, 'Review not found')); // 見つからない場合
return ok(mergeData(payload, { review })); // 成功レスポンス
};

export const normalizeReviewTerminator = (
result: Result<primitives.RouteHandlerPayload, Error>
) => {
if (result.isErr()) {
throw result.error; // エラーが発生した場合throw
}
const payload = result.value;
const { context } = payload;

if (!context.data?.review) {
throw new NodeblocksError(
500,
'Unknown error normalizing review', // レビューの正規化中に不明なエラー
'normalizeReview'
);
}
const { _id, ...review } = context.data.review; // MongoDBの_idを除去
return review;
};
export const normalizeReviewListTerminator = (
result: Result<primitives.RouteHandlerPayload, Error>
) => {
if (result.isErr()) {
throw result.error;
}
const payload = result.value;
const { context } = payload;

const reviews = context.data.reviews.map(
(rawReview: { _id: string; [key: string]: unknown }) => {
const { _id, ...review } = rawReview; // 各レビューから_idを除去
return review;
}
);
return reviews;
};
export const listReviews: primitives.AsyncRouteHandler<
Result<primitives.RouteHandlerPayload, Error>
> = async payload => {
const { context } = payload;
const reviews = await context.db.reviews.find({}).toArray(); // 全レビュー取得
return ok(mergeData(payload, { reviews })); // 成功レスポンス
};

3️⃣ ルート定義

src/routes ディレクトリ内に review.ts ファイルを作成します。 ルートはHTTPエンドポイントを定義し、ハンドラーに接続します。レビューの作成用と取得用の3つのルートを作成します。

src/routes/review.ts
import { primitives } from '@nodeblocks/backend-sdk';
const { compose, flatMapAsync, lift, withLogging, withRoute } = primitives;
import {
createReview, // レビュー作成ハンドラー
getReviewById, // IDでレビュー取得ハンドラー
normalizeReviewTerminator, // レビュー正規化ターミネーター
normalizeReviewListTerminator, // レビューリスト正規化ターミネーター
listReviews, // レビューリスト取得ハンドラー
} from '../handlers/review';

export const createReviewRoute = withRoute({
method: 'POST', // HTTPメソッド
path: '/reviews', // エンドポイントパス
validators: [], // バリデーション(なし)
handler: compose(
createReview, // 1. レビュー作成
flatMapAsync(getReviewById), // 2. 非同期でレビュー取得
lift(normalizeReviewTerminator) // 3. 結果正規化
),
});

export const getReviewRoute = withRoute({
method: 'GET', // HTTPメソッド
path: '/reviews/:reviewId', // パスパラメータ付きパス
validators: [], // バリデーション(なし)
handler: compose(getReviewById, lift(normalizeReviewTerminator)), // 取得して正規化
});

export const listReviewsRoute = withRoute({
method: 'GET', // HTTPメソッド
path: '/reviews', // エンドポイントパス
validators: [], // バリデーション(なし)
handler: compose(
withLogging(listReviews, 'debug'), // デバッグログ付きでリスト取得
lift(normalizeReviewListTerminator) // 結果正規化
),
});

4️⃣ 機能合成

src/features ディレクトリ内に review.ts ファイルを作成します。 機能はスキーマとルートを組み合わせ、完全なAPIエンドポイントを作成します。このステップでは、スキーマ検証ロジックをルートにリンクします

src/features/review.ts
import { primitives } from '@nodeblocks/backend-sdk';
import {
createReviewRoute, // レビュー作成ルート
getReviewRoute, // レビュー取得ルート
listReviewsRoute, // レビューリスト取得ルート
} from '../routes/review';
import { createReviewSchema } from '../schemas/review'; // レビュー作成スキーマ

const { compose } = primitives;

export const createReviewFeature = compose(
createReviewSchema, // スキーマ検証を適用
createReviewRoute // ルートに接続
);
export const getReviewFeature = getReviewRoute; // 取得機能(スキーマ不要)
export const listReviewsFeature = listReviewsRoute; // リスト取得機能(スキーマ不要)

5️⃣ サービス作成

src/services ディレクトリ内に review.ts ファイルを作成します。 サービスはすべてをまとめるファクトリ関数です。データベース接続を受け取り、Expressで使用できる完全なサービスを返します。

src/services/review.ts
import { partial } from 'lodash';  // 部分適用用
import { primitives } from '@nodeblocks/backend-sdk';
import {
createReviewFeature, // レビュー作成機能
getReviewFeature, // レビュー取得機能
listReviewsFeature, // レビューリスト取得機能
} from '../features/review';

const { compose, defService } = primitives;

export const reviewService: primitives.Service = db =>
defService(
partial(
compose(getReviewFeature, createReviewFeature, listReviewsFeature), // 機能を合成
{ dataStores: db } // DB接続をコンテキストとして渡す
)
);


6️⃣ サービス使用

最後に、src ディレクトリ内の index.ts ファイルを作成または更新します。 ここでレビュサービスをExpressとMongoDBに接続し、実際に実行可能なアプリケーションを作成します。

src/index.ts
import express from 'express';
import { middlewares, drivers } from '@nodeblocks/backend-sdk';
import { reviewService } from './services/review';

const { getMongoClient } = drivers;
const { nodeBlocksErrorMiddleware } = middlewares;

const client = getMongoClient('mongodb://localhost:27017', 'dev'); // MongoDB接続

express()
.use(reviewService({ reviews: client.collection('reviews') }, {})) // サービス使用、空設定
.use(nodeBlocksErrorMiddleware()) // エラーミドルウェア
.listen(8089, () => console.log('Server running')); // サーバー起動

🧪 Testing the Service

# レビュー作成(必須フィールドのみ)
curl -X POST http://localhost:8089/reviews \
-H 'Content-Type: application/json' \
-d '{"productId":"123","rating":5}'

# オプションコメント付きでレビュー作成
curl -X POST http://localhost:8089/reviews \
-H 'Content-Type: application/json' \
-d '{"productId":"123","rating":5,"comment":"Great product!"}'

# レビュー取得
curl http://localhost:8089/reviews/[reviewId]

期待されるレスポンス:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"productId": "123",
"rating": 5,
"comment": "Great product!",
"createdAt": "2024-01-01T12:00:00.000Z",
"updatedAt": "2024-01-01T12:00:00.000Z"
}

🔧 トラブルシューティング

HTMLエラーページがJSONの代わりに返される場合

APIがJSONの代わりにHTMLエラーページを返す場合、エラーミドルウェアを忘れている可能性があります:

// ❌ エラーミドルウェアなし
express()
.use(reviewService({ reviews: client.collection('reviews') }, {}))
.listen(8089, () => console.log('Server running'));

// ✅ エラーミドルウェアあり
express()
.use(reviewService({ reviews: client.collection('reviews') }, {}))
.use(nodeBlocksErrorMiddleware()) // この行を追加!
.listen(8089, () => console.log('Server running'));

エラーミドルウェアは最後に配置

エラーミドルウェアはすべてのサービスとルートの後に追加する必要があります:

// ✅ 正しい順序
express()
.use(reviewService({ reviews: client.collection('reviews') }, {}))
.use(nodeBlocksErrorMiddleware()) // 最後に!
.listen(8089, () => console.log('Server running'));

➡️ 次のステップ

これでレビュサービスに機能を追加して練習できます。レビュサービスに必要な以下のエンドポイントを実装してみてください:

  • 更新および削除操作 - PATCHおよびDELETEエンドポイントを追加してレビューを変更および削除