メインコンテンツまでスキップ
バージョン: 0.9.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' },
identityId: { type: 'string' },
rating: { type: 'number', minimum: 1, maximum: 5 },
comment: { type: 'string' },
},
type: 'object',
};

export const createReviewSchema = withSchema({
requestBody: {
content: {
'application/json': {
schema: { ...reviewSchema, required: ['productId', 'identityId', '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/blocks ディレクトリ内に review.ts ファイルを作成します。
レビューサービスのビジネスロジックを含むブロックを追加します。これらの関数は、レビューの作成、ID による取得、データ形式の正規化を処理します。

src/blocks/review.ts
import { Result, ok, err } from 'neverthrow';
import { Collection, WithId, Document } from 'mongodb';
import { utils, primitives } from '@nodeblocks/backend-sdk';

const { createBaseEntity } = utils;
const { BlockError, hasValue } = primitives;

export class ReviewBlockError extends BlockError {}
export class ReviewNotFoundBlockError extends ReviewBlockError {}

/**
* データベースに新しいレビューを作成します。
*/
export async function createReview(
reviewsCollection: Collection,
reviewData: Record<string, unknown>
): Promise<Result<string, ReviewBlockError>> {
try {
const entity = createBaseEntity(reviewData);
const result = await reviewsCollection.insertOne(entity);

if (!result.insertedId) {
return err(new ReviewBlockError('Failed to create review'));
}

return ok(entity.id);
} catch (error) {
return err(new ReviewBlockError('Failed to create review'));
}
}

/**
* ID でデータベースからレビューを取得します。
*/
export async function getReviewById(
reviewsCollection: Collection,
reviewId: string
): Promise<Result<WithId<Document> | null, ReviewBlockError>> {
try {
const result = await reviewsCollection.findOne({ id: String(reviewId) });
if (!hasValue(result)) {
return err(new ReviewNotFoundBlockError('Review not found'));
}
return ok(result);
} catch (error) {
return err(new ReviewBlockError('Failed to get review'));
}
}

/**
* オプションのフィルタリングで複数のレビューを検索します。
*/
export async function findReviews(
reviewsCollection: Collection,
filter: Record<string, unknown> = {}
): Promise<Result<WithId<Document>[], ReviewBlockError>> {
try {
const reviews = await reviewsCollection.find(filter).toArray();
return ok(reviews);
} catch (error) {
return err(new ReviewBlockError('Failed to find reviews'));
}
}

/**
* MongoDB 固有のフィールドを削除して単一のレビューを正規化します。
*/
export function normalizeReview<T extends Record<string, unknown>>({
_id,
...object
}: T): Result<Omit<T, '_id'>, Error> {
return ok(object);
}

/**
* MongoDB 固有のフィールドを削除してレビューの配列を正規化します。
*/
export function normalizeReviews<T extends Record<string, unknown>>(
objects: T[]
): Result<Omit<T, '_id'>[], Error> {
return ok(objects.map(({ _id, ...object }) => object));
}


3️⃣ ルートの定義

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

src/routes/review.ts
import { primitives, validators } from '@nodeblocks/backend-sdk';
const {
compose,
flatMapAsync,
lift,
withLogging,
withRoute,
applyPayloadArgs,
orThrow,
} = primitives;
import {
createReview,
getReviewById,
findReviews,
normalizeReview,
normalizeReviews,
ReviewNotFoundBlockError,
ReviewBlockError,
} from '../blocks/review';

const { isAuthenticated, isSelf } = validators;

export const createReviewRoute = withRoute({
method: 'POST',
path: '/reviews',
validators: [
isAuthenticated(),
isSelf(['params', 'requestParams', 'identityId']),
],
handler: compose(
withLogging(
applyPayloadArgs(
createReview,
[
['context', 'db', 'reviews'],
['params', 'requestBody'],
],
'createdReview'
)
),
flatMapAsync(
withLogging(
applyPayloadArgs(
getReviewById,
[
['context', 'db', 'reviews'],
['context', 'data', 'createdReview', 'id'],
],
'review'
)
)
),
flatMapAsync(
applyPayloadArgs(
normalizeReview,
[['context', 'data', 'review']],
'normalizedReview'
)
),
lift(
withLogging(
orThrow(
[[ReviewBlockError, 500]],
[['context', 'data', 'normalizedReview'], 200]
)
)
)
),
});

export const getReviewRoute = withRoute({
method: 'GET',
path: '/reviews/:reviewId',
validators: [],
handler: compose(
withLogging(
applyPayloadArgs(
getReviewById,
[
['context', 'db', 'reviews'],
['params', 'requestParams', 'reviewId'],
],
'review'
)
),
flatMapAsync(
applyPayloadArgs(
normalizeReview,
[['context', 'data', 'review']],
'normalizedReview'
)
),
lift(
withLogging(
orThrow(
[
[ReviewBlockError, 500],
[ReviewNotFoundBlockError, 404],
],
[['context', 'data', 'normalizedReview'], 200]
)
)
)
),
});

export const listReviewsRoute = withRoute({
method: 'GET',
path: '/reviews',
validators: [],
handler: compose(
withLogging(
applyPayloadArgs(
findReviews,
[
['context', 'db', 'reviews'],
[], // 今のところ空のフィルター
],
'reviews'
)
),
flatMapAsync(
applyPayloadArgs(
normalizeReviews,
[['context', 'data', 'reviews']],
'normalizedReviews'
)
),
lift(
withLogging(
orThrow(
[[ReviewBlockError, 500]],
[['context', 'data', 'normalizedReviews'], 200]
)
)
)
),
});

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 }
)
);


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 { withMongo } = drivers;
const { nodeBlocksErrorMiddleware } = middlewares;

const connectToDatabase = withMongo('mongodb://localhost:27017', 'dev', 'user', 'password');

express()
.use(reviewService(await connectToDatabase('reviews'), {})) // {} - 空の設定
.use(nodeBlocksErrorMiddleware())
.listen(8089, () => console.log('Server running'));

🧪 サービスのテスト

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

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

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

期待されるレスポンス:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"identityId": "6dcdd50a-e0e6-445d-82e1-3da35bc2d149",
"productId": "123",
"rating": 5,
"comment": "Great product!",
"createdAt": "2024-01-01T12:00:00.000Z",
"updatedAt": "2024-01-01T12:00:00.000Z"
}

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

JSON エラーではなく HTML が返される?

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

const connectToDatabase = withMongo('mongodb://localhost:27017', 'dev', 'user', 'password');

// ❌ エラーミドルウェアが不足
express()
.use(reviewService(await connectToDatabase('reviews'), {}))
.listen(8089, () => console.log('Server running'));

// ✅ エラーミドルウェア付きで正しい
express()
.use(reviewService(await connectToDatabase('reviews'), {}))
.use(nodeBlocksErrorMiddleware()) // この行を追加!
.listen(8089, () => console.log('Server running'));

エラーミドルウェアは最後に配置する必要がある

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

const connectToDatabase = withMongo('mongodb://localhost:27017', 'dev', 'user', 'password');

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

➡️ 次のステップ

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

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