メインコンテンツまでスキップ
バージョン: 🚧 Canary

🔗 Creating a Composite Service

This guide demonstrates how to combine multiple Nodeblocks services into a single composite service. We'll build a Composite Auth + User Service that combines authentication and user management functionality in one application. This pattern is useful when you want to share context between services or create a unified API.

📦 Required Package: This example uses the ramda package. Make sure to install it:

npm install ramda

🏗️ Service Architecture

The composite service pattern allows you to:

  1. Combine multiple services - Merge authentication and user management
  2. Share context - Use the same database connections and configuration
  3. Unified middleware - Create a single Express middleware from multiple services
  4. Simplified deployment - Deploy multiple related services as one application

1️⃣ Understand the Components

Before building the composite service, let's understand what we're combining:

Authentication Service Features

  • Register credentials - User registration with email/password
  • Login with credentials - User authentication and token generation

User Service Features

  • Create user - Create new user profiles
  • Get user features - Retrieve user information
  • Edit user features - Update user profiles
  • Delete user features - Remove user accounts
  • Lock user features - Disable user accounts

2️⃣ Create Service Middleware

Create a compositeService.ts file that combines multiple service features into unified middleware:

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

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

const client = getMongoClient('mongodb://localhost:27017', 'dev');

const { nodeBlocksErrorMiddleware } = middlewares;
const { defService, compose, withSchema } = primitives;
const {
registerCredentialsFeature,
loginWithCredentialsFeature,
// createUserFeature,
getUserFeatures,
deleteUserFeatures,
lockUserFeatures,
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,
lockUserFeatures,
findUsersFeatures
);

const dataStores = {
identity: client.collection('identity'),
users: client.collection('users'),
};
const configuration = {
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
maxFailedLoginAttempts: 5,
accessTokenExpireTime: '2h',
refreshTokenExpireTime: '2d',
};
const context = { dataStores, configuration };

const appMiddleware = compose(authServiceMiddleware, userServiceMiddleware);

express()
.use(
cors({
origin: ['*'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['*'],
})
)
// you can define services under single namespace, e.g. /api/v1/auth, /api/v1/user, etc.
.use('/api', defService(partial(appMiddleware, context)))
.use(nodeBlocksErrorMiddleware())
.listen(8089, () => console.log('Server running'));

3️⃣ Understanding the Pattern

Service Composition

The key to composite services is the compose function, which combines multiple features:

// Individual service middleware
const authServiceMiddleware = compose(
registerCredentialsFeature,
loginWithCredentialsFeature
);

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

// Combined middleware
const appMiddleware = compose(authServiceMiddleware, userServiceMiddleware);

Shared Context

All services share the same context object, which includes shared resources like:

  • Database collections for both auth and user services
  • Configuration settings for authentication and tokens
  • External service clients like email providers
  • Any other shared dependencies needed across services
const context = {
dataStores: {
identity: client.collection('identity'), // For auth service
users: client.collection('users'), // For user service
},
configuration: {
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret'
},
accessTokenExpireTime: '2h',
refreshTokenExpireTime: '2d'
}
mailService: sendGridClient
};

Partial Application

The partial function from Ramda pre-applies the context to the middleware:

defService(partial(appMiddleware, [context]))

This creates a service factory that's ready to be used by Express.


4️⃣ API Endpoints

Your composite service will expose the following endpoints:

Authentication Endpoints

  • POST /api/auth/register - Register a new user
  • POST /api/auth/login - Login with credentials
  • POST /api/auth/logout - Login with credentials

User Management Endpoints

  • POST /api/users - Create a new user
  • GET /api/users/:userId - Get user by ID
  • GET /api/users - List all users
  • PATCH /api/users/:userId - Update user
  • DELETE /api/users/:userId - Delete user
  • POST /api/users/:userId/lock - Lock user account
  • POST /api/users/:userId/unlock - Lock user account

5️⃣ Testing the Composite Service

# Register a new user
curl -X POST http://localhost:8089/api/auth/register \
-H 'Content-Type: application/json' \
-d '{
"email": "user@example.com",
"password": "securepassword123"
}'

# Login with credentials
curl -X POST http://localhost:8089/api/auth/login \
-H 'Content-Type: application/json' \
-d '{
"email": "user@example.com",
"password": "securepassword123"
}'

# Create a user profile
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"
}'

# Get user by ID
curl -X GET http://localhost:8089/api/users/USER_ID \
-H 'Authorization: Bearer <access-token>'

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

# Lock user account
curl -X POST http://localhost:8089/api/users/USER_ID/lock \
-H 'Authorization: Bearer <access-token>'

# Delete user
curl -X DELETE http://localhost:8089/api/users/USER_ID \
-H 'Authorization: Bearer <access-token>'

6️⃣ Environment Configuration

For production, you should externalize your configuration:

src/config.ts
export const config = {
database: {
url: process.env.MONGODB_URL || 'mongodb://localhost:27017',
name: process.env.DB_NAME || 'dev'
},
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')
}
};

Then update your composite service:

src/compositeService.ts
import { config } from './config';

const client = new MongoClient(config.database.url).db(config.database.name);

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

// ... rest of the service

️📐 Best Practices

1. Organize by Domain

Group related features together:

// ✅ Good: Logical grouping
const authServiceMiddleware = compose(
registerCredentialsFeature,
loginWithCredentialsFeature
);

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

// ❌ Avoid: Mixed concerns
const mixedMiddleware = compose(
registerCredentialsFeature,
createUserFeature,
loginWithCredentialsFeature,
getUserFeatures
);

➡️ Next Steps

Now you can extend your composite service by:

  • Adding more services - Include product, order, or notification services
  • Implementing middleware - Add logging, rate limiting, or CORS
  • Adding custom features - Create domain-specific business logic
  • Implementing microservices - Split into separate services when needed

🔗 See Also