🔗 Creating a Composite Service
This guide demonstrates how to combine multiple Nodeblocks services into a single composite service. We'll build a Composite Auth + Profile Service that combines authentication and profile management functionality in one application. This pattern is useful when you want to share context between services or create a unified API.
📦 Required Packages: This example imports Express, the SDK, Ramda, and
cookie-parser. Make sure to install them:npm install express @nodeblocks/backend-sdk ramda cookie-parsernpm install --save-dev @types/node @types/express @types/ramda @types/cookie-parser⚙️ Module setup: This example uses top-level
await. Run it as an ESM module (for example withmodule: "NodeNext"intsconfig.jsonand"type": "module"inpackage.json) or move the bootstrap code into anasync function main().🖼️ Avatar support: The profile routes in this sample normalize avatar URLs, so the example also uses
createFileStorageDriverfrom the SDK.🔐 Google Cloud credentials:
createFileStorageDriveruses Google Cloud Storage under the hood, so your environment still needs valid Google Cloud credentials for signed URL generation.
🏗️ Service Architecture
The composite service pattern allows you to:
- Combine multiple services - Merge authentication and profile management
- Share context - Use the same datastore configuration and request context
- Unified middleware - Create a single Express middleware from multiple services
- 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 - Identity registration with email/password
- Login with credentials - Authentication and token generation
- Logout - Invalidate the current session
- This guide intentionally excludes invitation, MFA, one-time-token, email verification, password reset, and OAuth flows.
Profile Service Features
- Create profile - Create a new profile for an identity
- Get profile - Retrieve a profile by ID
- Edit profile - Update profile fields
- Delete profile - Remove a profile
- Find profiles - List and filter profiles
- This guide intentionally excludes avatar upload URLs, identity lookup by identity ID, and follow/like flows.
2️⃣ Create Service Middleware
Create a compositeService.ts file that combines multiple service features into unified middleware:
import cookieParser from 'cookie-parser';
import express from 'express';
import {partial} from 'ramda';
import {middlewares, features, primitives, drivers, utils} from '@nodeblocks/backend-sdk';
const {withMongo, createFileStorageDriver} = drivers;
const {getCookieTokenInfo} = utils;
const connectToDatabase = withMongo('mongodb://localhost:27017/?authSource=admin', 'dev', 'user', 'password');
const fileStorageDriver = createFileStorageDriver(process.env.GCP_PROJECT_ID!, process.env.GCP_BUCKET_NAME!);
const {nodeBlocksErrorMiddleware} = middlewares;
const {defService, compose} = primitives;
const {
registerCredentialsFeature,
loginWithCredentialsFeature,
logoutFeature,
createProfileFeature,
getProfileFeature,
editProfileFeature,
deleteProfileFeature,
findProfilesFeature,
} = features;
const authServiceMiddleware = compose(registerCredentialsFeature, loginWithCredentialsFeature, logoutFeature);
const profileServiceMiddleware = compose(
createProfileFeature,
getProfileFeature,
editProfileFeature,
deleteProfileFeature,
findProfilesFeature,
);
const dataStores = {
...(await connectToDatabase('identities')),
...(await connectToDatabase('profiles')),
};
const configuration = {
// Use 'cookie' only when the host app also registers cookie-parser.
authMode: (process.env.AUTH_MODE === 'cookie' ? 'cookie' : 'bearer') as 'bearer' | 'cookie',
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
maxFailedLoginAttempts: 5,
accessTokenSignOptions: {expiresIn: '2h'},
refreshTokenSignOptions: {expiresIn: '2d'},
identity: {
typeIds: {
admin: '100',
guest: '000',
regular: '001',
},
},
};
const context = {
dataStores,
configuration,
fileStorageDriver,
// Feature composition does not configure this automatically as profileService does.
...(configuration.authMode === 'cookie' && {
authenticate: getCookieTokenInfo,
}),
};
const appMiddleware = compose(authServiceMiddleware, profileServiceMiddleware);
express()
// you can define services under a single namespace, e.g. /api/auth, /api/profiles
.use(cookieParser())
.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, logoutFeature);
const profileServiceMiddleware = compose(
createProfileFeature,
getProfileFeature,
editProfileFeature,
deleteProfileFeature,
findProfilesFeature,
);
// Combined middleware
const appMiddleware = compose(authServiceMiddleware, profileServiceMiddleware);
Shared Context
All services share the same context object, which includes:
- Database collections for the auth and profile routes shown here (
identitiesandprofiles) - Nested
configurationfor authentication secrets and token options - A
fileStorageDriverfor avatar URL normalization in profile responses - An
authenticatefunction when you opt into cookie authentication
If you later enable invitation-token registration or MFA / one-time-token login, add invitations and/or onetimetokens to the auth datastores as well.
const context = {
dataStores: {
...(await connectToDatabase('identities')),
...(await connectToDatabase('profiles')),
},
configuration: {
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
maxFailedLoginAttempts: 5,
accessTokenSignOptions: {expiresIn: '2h'},
refreshTokenSignOptions: {expiresIn: '2d'},
identity: {
typeIds: {
admin: '100',
guest: '000',
regular: '001',
},
},
},
fileStorageDriver,
};
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. Always pass the context as a one-element array: [{ dataStores, configuration, ... }].
4️⃣ API Endpoints
Your composite service will expose the following endpoints:
The examples use bearer tokens. To use cookie authentication, set configuration.authMode to 'cookie', register cookie-parser in the host app, and set context.authenticate to getCookieTokenInfo as shown above.
Authentication Endpoints
POST /api/auth/register- Register a new identityPOST /api/auth/login- Login with credentialsPOST /api/auth/logout- Logout and invalidate the session- Requires authentication
Profile Management Endpoints
POST /api/profiles- Create a new profile- Requires authentication
- Admin or the identity owner may create the profile
GET /api/profiles/:profileId- Get profile by ID- Requires authentication
- Admin or the profile owner may read it
PATCH /api/profiles/:profileId- Update profile- Requires authentication
- Admin or the profile owner may update it
DELETE /api/profiles/:profileId- Delete profile- Requires authentication
- Admin or the profile owner may delete it
GET /api/profiles- List/filter profiles- Requires authentication
- Admin only
5️⃣ Testing the Composite Service
# Register a new identity
curl -X POST http://localhost:8089/api/auth/register \
-H 'Content-Type: application/json' \
-d '{
"email": "user@example.com",
"password": "securepass123"
}'
# Login with credentials
curl -X POST http://localhost:8089/api/auth/login \
-H 'Content-Type: application/json' \
-d '{
"email": "user@example.com",
"password": "securepass123"
}'
# Create a profile (requires identityId from registration/login)
curl -X POST http://localhost:8089/api/profiles \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access-token>' \
-d '{
"identityId": "6dcdd50a-e0e6-445d-82e1-3da35bc2d149",
"name": "John Doe"
}'
# Get profile by ID
curl -X GET http://localhost:8089/api/profiles/PROFILE_ID \
-H 'Authorization: Bearer <access-token>'
# Update profile
curl -X PATCH http://localhost:8089/api/profiles/PROFILE_ID \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access-token>' \
-d '{
"name": "John Smith"
}'
# Delete profile
curl -X DELETE http://localhost:8089/api/profiles/PROFILE_ID \
-H 'Authorization: Bearer <access-token>'
6️⃣ Environment Configuration
For production, you should externalize your configuration:
export const config = {
database: {
url: process.env.MONGODB_URI || 'mongodb://localhost:27017/?authSource=admin',
name: process.env.MONGODB_DB_NAME || 'dev',
user: process.env.DB_USER || 'user',
password: process.env.DB_PASSWORD || 'password',
},
storage: {
gcpProjectId: process.env.GCP_PROJECT_ID || 'your-gcp-project-id',
bucketName: process.env.GCP_BUCKET_NAME || 'your-bucket-name',
},
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', 10),
accessTokenExpiresIn: process.env.ACCESS_TOKEN_EXPIRE || '2h',
refreshTokenExpiresIn: process.env.REFRESH_TOKEN_EXPIRE || '2d',
},
server: {
port: parseInt(process.env.PORT || '8089', 10),
},
};
Then update your composite service:
import {drivers} from '@nodeblocks/backend-sdk';
import {config} from './config';
const {withMongo, createFileStorageDriver} = drivers;
const connectToDatabase = withMongo(
config.database.url,
config.database.name,
config.database.user,
config.database.password,
);
const fileStorageDriver = createFileStorageDriver(config.storage.gcpProjectId, config.storage.bucketName);
const context = {
dataStores: {
...(await connectToDatabase('identities')),
...(await connectToDatabase('profiles')),
},
configuration: {
authSecrets: {
authEncSecret: config.auth.encSecret,
authSignSecret: config.auth.signSecret,
},
maxFailedLoginAttempts: config.auth.maxFailedAttempts,
accessTokenSignOptions: {expiresIn: config.auth.accessTokenExpiresIn},
refreshTokenSignOptions: {expiresIn: config.auth.refreshTokenExpiresIn},
identity: {
typeIds: {
admin: '100',
guest: '000',
regular: '001',
},
},
},
fileStorageDriver,
};
// ... rest of the service
📐 Best Practices
1. Organize by Domain
Group related features together:
// ✅ Good: Logical grouping
const authServiceMiddleware = compose(registerCredentialsFeature, loginWithCredentialsFeature, logoutFeature);
const profileServiceMiddleware = compose(
createProfileFeature,
getProfileFeature,
editProfileFeature,
deleteProfileFeature,
findProfilesFeature,
);
// ❌ Avoid: Mixed concerns
const mixedMiddleware = compose(
registerCredentialsFeature,
createProfileFeature,
loginWithCredentialsFeature,
getProfileFeature,
);
➡️ Next Steps
Now you can extend your composite service by:
- Adding more services - Include product, order, or notification features
- Implementing middleware - Add logging, rate limiting, or CORS
- Adding custom features - Create domain-specific business logic
- Overriding schemas - Customize validation for built-in features
- Implementing microservices - Split into separate services when needed
🔗 See Also
- Creating a Custom Service - Learn to build individual services
- Overriding Existing Schemas - Customize built-in validation
- Profile Service - Full profile API reference
- Authentication Service - Full auth API reference
- Service Component - Understand service architecture
- Feature Component - Learn about feature composition
- Error Handling - Handle errors in composite services