🔧 カスタムサービスの作成
このガイドでは、NodeBlocks SDKを使用して完全なカスタムサービスを作成する方法を説明します。すべての主要なパターンとコンポーネントを実証するレビュサービスを構築します。このシンプルな例では、ユーザーが製品レビューを作成および取得できるようになります。
📦 必要なパッケージ: この例では
ramda
とneverthrow
パッケージを使用します。必ずインストールしてください:npm install ramda neverthrow
🏗️ サービスアーキテクチャ
サービスを構築するため、以下のコアコンポーネントを実装します:
- スキーマ - データ検証とTypeScript型
- ハンドラー - ビジネスロジック関数
- ルート - HTTPエンドポイント定義
- 機能 - スキーマ + ルートの合成
- サービス - すべてを接続するファクトリ関数
1️⃣ スキーマ定義
まず、src/schemas
ディレクトリ内に review.ts
ファイルを作成します。
ここでは、必須フィールド productId
(文字列)と rating
(1-5の数値)、およびオプションの comment
フィールドを含むスキーマを定義します。
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による取得、およびデータ形式の正規化を処理します。
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つのルートを作成します。
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エンドポイントを作成します。このステップでは、スキーマ検証ロジックをルートにリンクします
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で使用できる完全なサービスを返します。
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に接続し、実際に実行可能なアプリケーションを作成します。
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エンドポイントを追加してレビューを変更および削除