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

🔗 コンポジットサービスの作成

このガイドでは、複数の Nodeblocks サービスを1つのコンポジットサービスに結合する方法を説明します。認証とユーザー管理機能を1つのアプリケーションに統合した コンポジット認証 + ユーザーサービス を構築します。このパターンは、サービス間でコンテキストを共有したり、統一された API を作成したりする場合に便利です。

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

npm install ramda

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

コンポジットサービスパターンにより、以下が可能になります:

  1. 複数のサービスを結合 - 認証とユーザー管理を統合
  2. コンテキストの共有 - 同じデータベース接続と設定を使用
  3. 統一されたミドルウェア - 複数のサービスから1つの Express ミドルウェアを作成
  4. 簡素化されたデプロイ - 複数の関連サービスを1つのアプリケーションとしてデプロイ

1️⃣ コンポーネントの理解

コンポジットサービスを構築する前に、結合する内容を理解しましょう:

認証サービスの機能

  • 認証情報の登録 - メール/パスワードでのユーザー登録
  • 認証情報でのログイン - ユーザー認証とトークン生成

ユーザーサービスの機能

  • ユーザーの作成 - 新しいユーザープロフィールを作成
  • ユーザー機能の取得 - ユーザー情報を取得
  • ユーザー機能の編集 - ユーザープロフィールを更新
  • ユーザー機能の削除 - ユーザーアカウントを削除
  • ユーザー機能のロック - ユーザーアカウントを無効化

2️⃣ サービスミドルウェアの作成

複数のサービス機能を統一されたミドルウェアに結合する compositeService.ts ファイルを作成します:

src/compositeService.ts
import express from 'express';
import { partial } from 'ramda';
import cors from 'cors';

import {
middlewares,
features,
primitives,
drivers,
routes,
} from '@nodeblocks/backend-sdk';

const { withMongo } = drivers;

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

const { nodeBlocksErrorMiddleware } = middlewares;
const { defService, compose, withSchema } = primitives;
const {
registerCredentialsFeature,
loginWithCredentialsFeature,
getUserFeatures,
deleteUserFeatures,
editUserFeatures,
findUsersFeatures,
} = features;

const { createUserRoute } = routes;

export const userSchema: primitives.SchemaDefinition = {
$schema: 'http://json-schema.org/draft-07/schema#',
additionalProperties: false,
properties: {
email: { type: 'string' },
name: { type: 'string' },
status: { type: 'string' },
age: { type: 'number' },
},
type: 'object',
};

export const createUserSchema = withSchema({
requestBody: {
content: {
'application/json': {
schema: {
...userSchema,
required: ['email', 'name', 'status', 'age'],
},
},
},
required: true,
},
});

const createUserFeature = compose(createUserSchema, createUserRoute);

const authServiceMiddleware = compose(
registerCredentialsFeature,
loginWithCredentialsFeature
);
const userServiceMiddleware = compose(
createUserFeature,
getUserFeatures,
editUserFeatures,
deleteUserFeatures,
findUsersFeatures
);

const dataStores = {
...(await connectToDatabase('identities')),
...(await connectToDatabase('users')),
...(await connectToDatabase('organizations')),
...(await connectToDatabase('products')),
};
const configuration = {
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
maxFailedLoginAttempts: 5,
accessTokenExpireTime: '2h',
refreshTokenExpireTime: '2d',
identity: {
typeIds: {
admin: '100',
guest: 'guest',
regular: 'regular',
},
},
};
const context = { dataStores, configuration };

const appMiddleware = compose(authServiceMiddleware, userServiceMiddleware);

express()
.use(
cors({
origin: ['*'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['*'],
})
)
// 単一の名前空間の下でサービスを定義できます(例: /api/v1/auth, /api/v1/user など)
.use('/api', defService(partial(appMiddleware, [context])))
.use(nodeBlocksErrorMiddleware())
.listen(8089, () => console.log('Server running'));

3️⃣ パターンの理解

サービス構成

コンポジットサービスの鍵は、複数の機能を結合する compose 関数です:

// 個別のサービスミドルウェア
const authServiceMiddleware = compose(
registerCredentialsFeature,
loginWithCredentialsFeature
);

const userServiceMiddleware = compose(
createUserFeature,
getUserFeatures,
editUserFeatures,
deleteUserFeatures,
);

// 結合されたミドルウェア
const appMiddleware = compose(authServiceMiddleware, userServiceMiddleware);

共有コンテキスト

すべてのサービスは同じコンテキストオブジェクトを共有します。これには以下の共有リソースが含まれます:

  • 認証とユーザーサービスの両方のデータベースコレクション
  • 認証とトークンの設定
  • メールプロバイダーなどの外部サービスクライアント
  • サービス間で必要なその他の共有依存関係
const context = {
dataStores: {
...(await connectToDatabase('identities')),
...(await connectToDatabase('users')),
...(await connectToDatabase('organizations')),
...(await connectToDatabase('products')),
},
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
maxFailedLoginAttempts: 5,
accessTokenExpireTime: '2h',
refreshTokenExpireTime: '2d',
identity: {
typeIds: {
admin: '100',
guest: 'guest',
regular: 'regular',
},
},
mailService: sendGridClient
};

部分適用

Ramda の partial 関数は、ミドルウェアにコンテキストを事前適用します:

defService(partial(appMiddleware, [context]))

これにより、Express で使用できる準備が整ったサービスファクトリーが作成されます。


4️⃣ API エンドポイント

コンポジットサービスは以下のエンドポイントを公開します:

認証エンドポイント

  • POST /api/auth/register - 新しいユーザーを登録
  • POST /api/auth/login - 認証情報でログイン
  • POST /api/auth/logout - ログアウト

ユーザー管理エンドポイント

  • POST /api/users - 新しいユーザーを作成
  • GET /api/users/:userId - ID でユーザーを取得
  • PATCH /api/users/:userId - ユーザーを更新
  • DELETE /api/users/:userId - ユーザーを削除

5️⃣ コンポジットサービスのテスト

# 新しいユーザーを登録
curl -X POST http://localhost:8089/api/auth/register \
-H 'Content-Type: application/json' \
-d '{
"email": "user@example.com",
"password": "securepassword123"
}'

# 認証情報でログイン
curl -X POST http://localhost:8089/api/auth/login \
-H 'Content-Type: application/json' \
-d '{
"email": "user@example.com",
"password": "securepassword123"
}'

# ユーザープロフィールを作成
curl -X POST http://localhost:8089/api/users \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access-token>' \
-d '{
"name": "John Doe",
"email": "john@example.com",
"status": "active"
}'

# ID でユーザーを取得
curl -X GET http://localhost:8089/api/users/USER_ID \
-H 'Authorization: Bearer <access-token>'

# ユーザーを更新
curl -X PATCH http://localhost:8089/api/users/USER_ID \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access-token>' \
-d '{
"name": "John Smith"
}'

# ユーザーを削除
curl -X DELETE http://localhost:8089/api/users/USER_ID \
-H 'Authorization: Bearer <access-token>'

6️⃣ 環境設定

本番環境では、設定を外部化する必要があります:

src/config.ts
export const config = {
database: {
url: process.env.MONGODB_URL || 'mongodb://localhost:27017',
name: process.env.DB_NAME || 'dev',
user: 'user',
password: 'password'
},
auth: {
encSecret: process.env.AUTH_ENC_SECRET || 'your-encryption-secret',
signSecret: process.env.AUTH_SIGN_SECRET || 'your-signing-secret',
maxFailedAttempts: parseInt(process.env.MAX_FAILED_ATTEMPTS || '5'),
accessTokenExpireTime: process.env.ACCESS_TOKEN_EXPIRE || '2h',
refreshTokenExpireTime: process.env.REFRESH_TOKEN_EXPIRE || '2d'
},
server: {
port: parseInt(process.env.PORT || '8089')
}
};

次に、コンポジットサービスを更新します:

src/compositeService.ts
import { config } from './config';
import { withMongo } from '@nodeblocks/backend-sdk/drivers';

const connectToDatabase = withMongo(
config.database.url,
config.database.name,
config.database.user || 'user',
config.database.password || 'password'
);

const context = {
dataStores: {
...(await connectToDatabase('identity')),
...(await connectToDatabase('users')),
},
configuration: {
authSecrets: {
authEncSecret: config.auth.encSecret,
authSignSecret: config.auth.signSecret
},
maxFailedLoginAttempts: config.auth.maxFailedAttempts,
accessTokenExpireTime: config.auth.accessTokenExpireTime,
refreshTokenExpireTime: config.auth.refreshTokenExpireTime
}
};

// ... サービスの残りの部分

️📐 ベストプラクティス

1. ドメイン別に整理

関連する機能をグループ化します:

// ✅ 良い: 論理的なグループ化
const authServiceMiddleware = compose(
registerCredentialsFeature,
loginWithCredentialsFeature
);

const userServiceMiddleware = compose(
createUserFeature,
getUserFeatures,
editUserFeatures,
deleteUserFeatures,
);

// ❌ 避ける: 混在した関心事
const mixedMiddleware = compose(
registerCredentialsFeature,
createUserFeature,
loginWithCredentialsFeature,
getUserFeatures
);

➡️ 次のステップ

コンポジットサービスを拡張できます:

  • さらに多くのサービスを追加 - 製品、注文、または通知サービスを含める
  • ミドルウェアの実装 - ロギング、レート制限、または CORS を追加
  • カスタム機能の追加 - ドメイン固有のビジネスロジックを作成
  • マイクロサービスの実装 - 必要に応じて別々のサービスに分割

🔗 関連ドキュメント