🔧 カスタムサービスの作成
このガイドでは、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' },
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 による取得、データ形式の正規化を処理します。
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つのルートを作成します。
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 エンドポイントを作成します。このステップでは、スキーマ検証ロジックをルートにリンクします
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 }
)
);
6️⃣ サービスの使用
最後に、src ディレクトリ内の index.ts ファイルを作成または更新します。
ここで、レビューサービスを Express と MongoDB に接続し、実際に動作するアプリケーションを作成します。
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 エンドポイントを追加