🔧 カスタムサービスの作成
このガイドでは、Nodeblocks SDKを使用して完全なカスタムサービスを作成する方法を順を追って説明します。Review Service を構築し、すべての主要なパターンとコンポーネントを示します。このシンプルな例では、ユーザーが製品レビューを作成・取得できます。
📦 必要なパッケージ: この例は直接Express、SDK、
ramda、neverthrow、およびMongoDB型をインポートします。インストールしてください:npm install @nodeblocks/backend-sdk express ramda neverthrow mongodbnpm install --save-dev @types/express @types/node @types/ramda⚙️ モジュール設定: bootstrap例はトップレベルの
awaitを使用します。ESMモジュールとして実行するか(例えばtsconfig.jsonで"module": "NodeNext"、package.jsonで"type": "module")、bootstrapコードをasync function main()に移動してください。
🏗️ サービスアーキテクチャ
サービスを構築するために、以下の主要なコンポーネントを実装します:
- スキーマ - データバリデーションとTypeScript型
- ブロック - ビジネスロジック関数
- ルート - HTTPエンドポイント定義
- フィーチャー - 合成されたスキーマ + ルート
- サービス - すべてを接続するファクトリ関数
1️⃣ スキーマの定義
まず、src/schemas ディレクトリ内に review.ts ファイルを作成します。
ここで必須フィールド productId(文字列)、identityId(文字列)、および 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 getReviewSchema = withSchema({
parameters: [{...reviewIdPathParameter}],
});
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 {}
/**
* データベースに新しいレビューを作成します。
* 作成されたレビューのIDを文字列として返します。
*/
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>, 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'));
}
}
/**
* '_id' を削除して単一のレビューを正規化します。
*/
export function normalizeReview<T extends Record<string, unknown>>({_id, ...object}: T): Result<Omit<T, '_id'>, Error> {
return ok(object);
}
/**
* '_id' を削除してレビュー配列を正規化します。
*/
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';
import {
createReview,
getReviewById,
findReviews,
normalizeReview,
normalizeReviews,
ReviewNotFoundBlockError,
ReviewBlockError,
} from '../blocks/review';
const {compose, flatMapAsync, lift, withLogging, withRoute, applyPayloadArgs, orThrow} = primitives;
const {isAuthenticated, isSelf} = validators;
export const createReviewRoute = withRoute({
method: 'POST',
path: '/reviews',
validators: [
isAuthenticated(),
// identityId は POST /reviews のリクエスト本文から来ます
isSelf(['params', 'requestBody', 'identityId']),
],
handler: compose(
withLogging(
applyPayloadArgs(
createReview,
[
['context', 'db', 'reviews'],
['params', 'requestBody'],
],
'createdReview',
),
),
// createReview は context.data.createdReview 下に文字列IDを保存
flatMapAsync(
withLogging(
applyPayloadArgs(
getReviewById,
[
['context', 'db', 'reviews'],
['context', 'data', 'createdReview'],
],
'review',
),
),
),
flatMapAsync(applyPayloadArgs(normalizeReview, [['context', 'data', 'review']], 'normalizedReview')),
lift(
withLogging(
orThrow(
[
[ReviewNotFoundBlockError, 404],
[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(
[
[ReviewNotFoundBlockError, 404],
[ReviewBlockError, 500],
],
[['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, getReviewSchema} from '../schemas/review';
const {compose} = primitives;
export const createReviewFeature = compose(createReviewSchema, createReviewRoute);
export const getReviewFeature = compose(getReviewSchema, getReviewRoute);
export const listReviewsFeature = listReviewsRoute;
5️⃣ サービスの作成
src/services ディレクトリ内に review.ts ファイルを作成します。
サービスはすべてを結合するファクトリ関数です。データベース接続と設定を取り、完全なExpressルーターを返します。
import {partial} from 'ramda';
import {primitives} from '@nodeblocks/backend-sdk';
import {createReviewFeature, getReviewFeature, listReviewsFeature} from '../features/review';
const {compose, defService} = primitives;
export const reviewService: primitives.Service = (dataStores, configuration) =>
defService(
partial(compose(createReviewFeature, getReviewFeature, listReviewsFeature), [{dataStores, configuration}]),
);
6️⃣ サービスの使用
最後に、src ディレクトリ内の index.ts ファイルを作成または更新します。
ここでReview Serviceを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/?authSource=admin', 'dev', 'user', 'password');
express()
.use(
reviewService(await connectToDatabase('reviews'), {
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
}),
)
.use(nodeBlocksErrorMiddleware())
.listen(8089, () => console.log('Server running'));
ノート:
POST /reviewsはisAuthenticated()とisSelf(...)を使用するので、作成リクエストにはボディと一致するidentityIdを持つ有効なユーザーアクセストークンが必要です。この例では提供されたauthSecretsでトークンを検証しますが、発行はしません。保護されたエンドポイントをテストする前に、services.authService(必要なidentitiesデータストアおよび適用可能な場合onetimetokens)をマウントするか、別の信頼できるJWT発行者を使用してください。
🧪 サービスのテスト
# レビューを作成(必須フィールドのみ)
curl -X POST http://localhost:8089/reviews \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access-token>' \
-d '{"productId":"123","identityId":"6dcdd50a-e0e6-445d-82e1-3da35bc2d149","rating":5}'
# オプションのコメント付きでレビューを作成
curl -X POST http://localhost:8089/reviews \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access-token>' \
-d '{"productId":"123","identityId":"6dcdd50a-e0e6-445d-82e1-3da35bc2d149","rating":5,"comment":"Great product!"}'
# レビューを取得
curl "http://localhost:8089/reviews/<reviewId>"
# レビューの一覧
curl http://localhost:8089/reviews
期待されるレスポンス:
{
"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"
}
🔧 トラブルシューティング
HTMLなのにJSONエラーではない?
APIがJSONではなくHTMLエラーページを返す場合、エラーミドルウェアを見逃している可能性が高いです:
const connectToDatabase = withMongo('mongodb://localhost:27017/?authSource=admin', 'dev', 'user', 'password');
const reviewServiceConfig = {
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
};
// ❌ エラーミドルウェアが不足
express()
.use(reviewService(await connectToDatabase('reviews'), reviewServiceConfig))
.listen(8089, () => console.log('Server running'));
// ✅ エラーミドルウェア付きで正しい
express()
.use(reviewService(await connectToDatabase('reviews'), reviewServiceConfig))
.use(nodeBlocksErrorMiddleware()) // この行を追加!
.listen(8089, () => console.log('Server running'));
エラーミドルウェアは最後に設定必须
エラーミドルウェアはすべてのサービスとルートの後に追加する必要があります:
const connectToDatabase = withMongo('mongodb://localhost:27017/?authSource=admin', 'dev', 'user', 'password');
const reviewServiceConfig = {
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
};
// ✅ 正しい順序
express()
.use(reviewService(await connectToDatabase('reviews'), reviewServiceConfig))
.use(nodeBlocksErrorMiddleware()) // 最後に!
.listen(8089, () => console.log('Server running'));
➡️ 次のステップ
今、Review Serviceにもっと機能を追加することで実習できます。以下のレビューサービスに必要なエンドポイントの実装を試してください:
- 更新と削除操作 - レビューの変更と削除のためにPATCHとDELETEエンドポイントをを追加
- 合成セットアップ - このサービスを認証およびプロフィールフィーチャーと組み合わせる (合成サービスの作成)
- カスタムストレージ - MongoDBをカスタムアダプターに交換する (カスタムDataStoreの使用)