@themainstack/communication 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,364 @@
1
+ # Detailed Integration Example: fee-service ↔ payment-service
2
+
3
+ This document shows exactly how to integrate two services using `@themainstack/communication`.
4
+
5
+ ---
6
+
7
+ ## Part 1: Fee Service (The Server)
8
+
9
+ ### 1.1 Install the Package
10
+
11
+ ```bash
12
+ cd fee-service
13
+ yarn add @themainstack/communication
14
+ ```
15
+
16
+ ### 1.2 Your Existing Business Logic
17
+
18
+ ```typescript
19
+ // fee-service/src/fee/fees.service.ts
20
+ import { Injectable } from '@nestjs/common';
21
+
22
+ @Injectable()
23
+ export class FeesService {
24
+ async calculateFeesForPlan(
25
+ planSlug: string,
26
+ currency: string,
27
+ amount: number,
28
+ type: string,
29
+ ) {
30
+ // Your existing business logic...
31
+ const feePercentage = planSlug === 'starter' ? 2.5 : 1.5;
32
+ const fee = amount * (feePercentage / 100);
33
+
34
+ return {
35
+ dollarAmount: amount,
36
+ dollarTransactionFee: fee,
37
+ localTransactionFee: fee * 400, // Example: NGN rate
38
+ localCurrency: currency,
39
+ localAmount: (amount - fee).toString(),
40
+ exchangeRate: 400,
41
+ appliedFees: [{
42
+ id: 'fee_001',
43
+ type: 'withdrawal',
44
+ displayName: 'Withdrawal Fee',
45
+ amount: fee,
46
+ amountType: 'percent',
47
+ }],
48
+ appliedFeeIds: ['fee_001'],
49
+ };
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### 1.3 Create the gRPC Bootstrap File
55
+
56
+ ```typescript
57
+ // fee-service/src/grpc/grpc-bootstrap.ts
58
+ import { GrpcServerFactory } from '@themainstack/communication';
59
+ import { FeesService } from '../fee/fees.service';
60
+
61
+ const feesService = new FeesService();
62
+
63
+ export async function startGrpcServer() {
64
+ const server = await GrpcServerFactory.createServer({
65
+ packageName: 'fee.v1',
66
+ serviceName: 'FeeService',
67
+ port: parseInt(process.env.GRPC_PORT || '50053'),
68
+ protoOutputDir: './src/grpc',
69
+ }, [
70
+ {
71
+ name: 'CalculateWithdrawalFee',
72
+ handler: async (request: any) => {
73
+ // Call your existing service
74
+ const result = await feesService.calculateFeesForPlan(
75
+ 'starter', // Default to starter plan
76
+ request.currency,
77
+ request.amount,
78
+ 'withdrawal',
79
+ );
80
+
81
+ // Map to proto response format
82
+ return {
83
+ dollar_amount: result.dollarAmount,
84
+ dollar_transaction_fee: result.dollarTransactionFee,
85
+ local_transaction_fee: result.localTransactionFee,
86
+ local_currency: result.localCurrency,
87
+ local_amount: result.localAmount,
88
+ exchange_rate: result.exchangeRate,
89
+ applied_fees: result.appliedFees.map(f => ({
90
+ id: f.id,
91
+ type: f.type,
92
+ display_name: f.displayName,
93
+ amount: f.amount,
94
+ amount_type: f.amountType,
95
+ })),
96
+ applied_fee_ids: result.appliedFeeIds,
97
+ };
98
+ },
99
+ // Sample data for proto generation
100
+ requestSample: () => ({
101
+ merchantId: '',
102
+ amount: 0,
103
+ currency: '',
104
+ payoutMethod: '',
105
+ }),
106
+ responseSample: () => ({
107
+ dollarAmount: 0,
108
+ dollarTransactionFee: 0,
109
+ localTransactionFee: 0,
110
+ localCurrency: '',
111
+ localAmount: '',
112
+ exchangeRate: 0,
113
+ appliedFees: [{
114
+ id: '',
115
+ type: '',
116
+ displayName: '',
117
+ amount: 0,
118
+ amountType: '',
119
+ }],
120
+ appliedFeeIds: [''],
121
+ }),
122
+ },
123
+ ]);
124
+
125
+ await server.start();
126
+ return server;
127
+ }
128
+ ```
129
+
130
+ ### 1.4 Integrate with main.ts
131
+
132
+ ```typescript
133
+ // fee-service/src/main.ts
134
+ import { NestFactory } from '@nestjs/core';
135
+ import { AppModule } from './app.module';
136
+ import { startGrpcServer } from './grpc/grpc-bootstrap';
137
+
138
+ async function bootstrap() {
139
+ // Start HTTP server (existing)
140
+ const app = await NestFactory.create(AppModule);
141
+ const httpPort = process.env.PORT || 40400;
142
+ await app.listen(httpPort);
143
+ console.log(`HTTP server on port ${httpPort}`);
144
+
145
+ // Start gRPC server (new)
146
+ await startGrpcServer();
147
+ }
148
+
149
+ bootstrap();
150
+ ```
151
+
152
+ ### 1.5 Result
153
+
154
+ When you start fee-service, it will:
155
+ 1. Auto-generate `./src/grpc/fee.proto`
156
+ 2. Start HTTP on port 40400
157
+ 3. Start gRPC on port 50053
158
+
159
+ ---
160
+
161
+ ## Part 2: Payment Service (The Client)
162
+
163
+ ### 2.1 Install the Package
164
+
165
+ ```bash
166
+ cd payment-service
167
+ yarn add @themainstack/communication
168
+ ```
169
+
170
+ ### 2.2 Copy the Proto File
171
+
172
+ Copy the generated proto from fee-service:
173
+ ```bash
174
+ mkdir -p src/payouts/protos
175
+ cp ../fee-service/src/grpc/fee.proto src/payouts/protos/
176
+ ```
177
+
178
+ ### 2.3 Create the Fee Client
179
+
180
+ ```typescript
181
+ // payment-service/src/payouts/clients/fee.client.ts
182
+ import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
183
+ import { GrpcClientFactory, handleGrpcError } from '@themainstack/communication';
184
+ import { join } from 'path';
185
+
186
+ export interface FeeCalculation {
187
+ dollarAmount: number;
188
+ dollarTransactionFee: number;
189
+ localTransactionFee: number;
190
+ localCurrency: string;
191
+ localAmount: string;
192
+ exchangeRate: number;
193
+ appliedFees: any[];
194
+ appliedFeeIds: string[];
195
+ }
196
+
197
+ @Injectable()
198
+ export class FeeClient implements OnModuleInit {
199
+ private readonly logger = new Logger(FeeClient.name);
200
+ private client: any;
201
+
202
+ onModuleInit() {
203
+ const FEE_SERVICE_URL = process.env.FEE_SERVICE_GRPC_URL || 'localhost:50053';
204
+
205
+ this.client = GrpcClientFactory.createClient({
206
+ serviceName: 'FeeService',
207
+ packageName: 'fee.v1',
208
+ protoPath: join(__dirname, '../protos/fee.proto'),
209
+ url: FEE_SERVICE_URL,
210
+ });
211
+
212
+ this.logger.log(`Fee gRPC Client connected to ${FEE_SERVICE_URL}`);
213
+ }
214
+
215
+ async calculateWithdrawalFee(
216
+ merchantId: string,
217
+ amount: number,
218
+ currency: string,
219
+ payoutMethod: string,
220
+ ): Promise<FeeCalculation> {
221
+ return new Promise((resolve, reject) => {
222
+ this.client.CalculateWithdrawalFee(
223
+ {
224
+ merchant_id: merchantId,
225
+ amount,
226
+ currency,
227
+ payout_method: payoutMethod,
228
+ },
229
+ (err: any, response: any) => {
230
+ if (err) {
231
+ try {
232
+ handleGrpcError(err);
233
+ } catch (normalizedError: any) {
234
+ this.logger.error(`Fee calculation failed: ${normalizedError.message}`);
235
+ reject(normalizedError);
236
+ }
237
+ return;
238
+ }
239
+
240
+ // Map snake_case response to camelCase
241
+ resolve({
242
+ dollarAmount: response.dollar_amount,
243
+ dollarTransactionFee: response.dollar_transaction_fee,
244
+ localTransactionFee: response.local_transaction_fee,
245
+ localCurrency: response.local_currency,
246
+ localAmount: response.local_amount,
247
+ exchangeRate: response.exchange_rate,
248
+ appliedFees: response.applied_fees || [],
249
+ appliedFeeIds: response.applied_fee_ids || [],
250
+ });
251
+ }
252
+ );
253
+ });
254
+ }
255
+ }
256
+ ```
257
+
258
+ ### 2.4 Register in Module
259
+
260
+ ```typescript
261
+ // payment-service/src/payouts/payouts.module.ts
262
+ import { Module } from '@nestjs/common';
263
+ import { PayoutsService } from './payouts.service';
264
+ import { FeeClient } from './clients/fee.client';
265
+
266
+ @Module({
267
+ providers: [PayoutsService, FeeClient],
268
+ exports: [PayoutsService],
269
+ })
270
+ export class PayoutsModule {}
271
+ ```
272
+
273
+ ### 2.5 Use in Your Service
274
+
275
+ ```typescript
276
+ // payment-service/src/payouts/payouts.service.ts
277
+ import { Injectable } from '@nestjs/common';
278
+ import { FeeClient } from './clients/fee.client';
279
+
280
+ @Injectable()
281
+ export class PayoutsService {
282
+ constructor(private readonly feeClient: FeeClient) {}
283
+
284
+ async processPayout(merchantId: string, amount: number, currency: string) {
285
+ // Call fee-service via gRPC
286
+ const feeCalculation = await this.feeClient.calculateWithdrawalFee(
287
+ merchantId,
288
+ amount,
289
+ currency,
290
+ 'bank_transfer',
291
+ );
292
+
293
+ console.log('Fee:', feeCalculation.dollarTransactionFee);
294
+ console.log('Net Amount:', amount - feeCalculation.dollarTransactionFee);
295
+
296
+ // Continue with payout logic...
297
+ return {
298
+ grossAmount: amount,
299
+ fee: feeCalculation.dollarTransactionFee,
300
+ netAmount: amount - feeCalculation.dollarTransactionFee,
301
+ };
302
+ }
303
+ }
304
+ ```
305
+
306
+ ---
307
+
308
+ ## Part 3: Environment Configuration
309
+
310
+ ### fee-service/.env
311
+ ```env
312
+ PORT=40400
313
+ GRPC_PORT=50053
314
+ ```
315
+
316
+ ### payment-service/.env
317
+ ```env
318
+ PORT=40401
319
+ FEE_SERVICE_GRPC_URL=localhost:50053
320
+ # In Docker/K8s: FEE_SERVICE_GRPC_URL=fee-service:50053
321
+ ```
322
+
323
+ ---
324
+
325
+ ## Part 4: Testing the Integration
326
+
327
+ ### Start fee-service
328
+ ```bash
329
+ cd fee-service
330
+ yarn start:dev
331
+ # Output:
332
+ # HTTP server on port 40400
333
+ # 🚀 gRPC Server running on 0.0.0.0:50053
334
+ ```
335
+
336
+ ### Start payment-service
337
+ ```bash
338
+ cd payment-service
339
+ yarn start:dev
340
+ # Output:
341
+ # Fee gRPC Client connected to localhost:50053
342
+ ```
343
+
344
+ ### Make a Request
345
+ ```bash
346
+ curl -X POST http://localhost:40401/payouts/process \
347
+ -H "Content-Type: application/json" \
348
+ -d '{"merchantId": "m123", "amount": 1000, "currency": "USD"}'
349
+
350
+ # Response:
351
+ # { "grossAmount": 1000, "fee": 25, "netAmount": 975 }
352
+ ```
353
+
354
+ ---
355
+
356
+ ## Summary
357
+
358
+ | Step | fee-service | payment-service |
359
+ |------|-------------|-----------------|
360
+ | 1 | Install package | Install package |
361
+ | 2 | Write business function | Copy proto file |
362
+ | 3 | Use `GrpcServerFactory` | Use `GrpcClientFactory` |
363
+ | 4 | Add to main.ts | Create client class |
364
+ | 5 | Proto auto-generated! | Call client methods |
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Full Demo: gRPC Server + Client using @themainstack/communication
3
+ *
4
+ * This demonstrates the complete workflow:
5
+ * 1. Define a function you want to expose
6
+ * 2. Create a gRPC server (proto auto-generated)
7
+ * 3. Create a client to call the server
8
+ */
9
+
10
+ import { GrpcServerFactory, GrpcClientFactory, handleGrpcError } from '../src';
11
+ import * as path from 'path';
12
+
13
+ // 1: Define your business logic function
14
+
15
+ interface FeeRequest {
16
+ merchantId: string;
17
+ amount: number;
18
+ currency: string;
19
+ }
20
+
21
+ interface FeeResponse {
22
+ originalAmount: number;
23
+ fee: number;
24
+ totalAmount: number;
25
+ currency: string;
26
+ feePercentage: number;
27
+ }
28
+
29
+ // Your existing business function
30
+ async function calculateFee(request: FeeRequest): Promise<FeeResponse> {
31
+ const feePercentage = 2.5; // 2.5% fee
32
+ const fee = request.amount * (feePercentage / 100);
33
+
34
+ return {
35
+ originalAmount: request.amount,
36
+ fee: fee,
37
+ totalAmount: request.amount + fee,
38
+ currency: request.currency,
39
+ feePercentage: feePercentage,
40
+ };
41
+ }
42
+
43
+ // 2: Create and start the gRPC server
44
+
45
+ async function startServer() {
46
+ console.log('Starting gRPC Server...\n');
47
+
48
+ const server = await GrpcServerFactory.createServer(
49
+ {
50
+ packageName: 'calculator.v1',
51
+ serviceName: 'CalculatorService',
52
+ port: 50055,
53
+ protoOutputDir: path.join(__dirname, 'grpc'),
54
+ },
55
+ [
56
+ {
57
+ name: 'CalculateFee',
58
+ handler: calculateFee, // Your existing function!
59
+ requestSample: () => ({ merchantId: '', amount: 0, currency: '' }),
60
+ responseSample: () => ({
61
+ originalAmount: 0,
62
+ fee: 0,
63
+ totalAmount: 0,
64
+ currency: '',
65
+ feePercentage: 0
66
+ }),
67
+ },
68
+ ]
69
+ );
70
+
71
+ await server.start();
72
+ return server;
73
+ }
74
+
75
+ // 3: Create a client and call the server
76
+
77
+ async function callServer() {
78
+ console.log('\nCreating gRPC Client...\n');
79
+
80
+ // Wait a moment for server to be ready
81
+ await new Promise(resolve => setTimeout(resolve, 500));
82
+
83
+ const client = GrpcClientFactory.createClient<any>({
84
+ serviceName: 'CalculatorService',
85
+ packageName: 'calculator.v1',
86
+ protoPath: path.join(__dirname, 'grpc', 'calculator.proto'),
87
+ url: 'localhost:50055',
88
+ });
89
+
90
+ // Make a gRPC call
91
+ return new Promise<FeeResponse>((resolve, reject) => {
92
+ client.CalculateFee(
93
+ { merchantId: 'merchant_123', amount: 1000, currency: 'USD' },
94
+ (err: any, response: any) => {
95
+ if (err) {
96
+ try {
97
+ handleGrpcError(err);
98
+ } catch (e) {
99
+ reject(e);
100
+ }
101
+ return;
102
+ }
103
+ resolve(response);
104
+ }
105
+ );
106
+ });
107
+ }
108
+
109
+ // RUN THE DEMO
110
+ async function main() {
111
+ console.log('------------------------------------------------------------');
112
+ console.log(' @themainstack/communication - Full gRPC Demo');
113
+ console.log('------------------------------------------------------------');
114
+ console.log('');
115
+
116
+ // Start server
117
+ const server = await startServer();
118
+
119
+ try {
120
+ // Call the server
121
+ const result = await callServer();
122
+
123
+ console.log('gRPC Call Successful!\n');
124
+ console.log('Request: { merchantId: "merchant_123", amount: 1000, currency: "USD" }');
125
+ console.log('Response:', JSON.stringify(result, null, 2));
126
+ console.log('');
127
+ } catch (error) {
128
+ console.error('Error:', error);
129
+ } finally {
130
+ // Stop server
131
+ await server.stop();
132
+ }
133
+
134
+ console.log('------------------------------------------------------------');
135
+ console.log(' Demo Complete!');
136
+ console.log('------------------------------------------------------------');
137
+ }
138
+
139
+ main().catch(console.error);
@@ -0,0 +1,22 @@
1
+ syntax = "proto3";
2
+
3
+ package calculator.v1;
4
+
5
+ // CalculatorService - Auto-generated gRPC service
6
+ service CalculatorService {
7
+ rpc CalculateFee (CalculateFeeRequest) returns (CalculateFeeResponse) {}
8
+ }
9
+
10
+ message CalculateFeeResponse {
11
+ int32 original_amount = 1;
12
+ int32 fee = 2;
13
+ int32 total_amount = 3;
14
+ string currency = 4;
15
+ int32 fee_percentage = 5;
16
+ }
17
+
18
+ message CalculateFeeRequest {
19
+ string merchant_id = 1;
20
+ int32 amount = 2;
21
+ string currency = 3;
22
+ }
@@ -0,0 +1,60 @@
1
+ syntax = "proto3";
2
+
3
+ package fee.v1;
4
+
5
+ // FeeService - Auto-generated gRPC service
6
+ service FeeService {
7
+ rpc CalculateWithdrawalFee (CalculateWithdrawalFeeRequest) returns (CalculateWithdrawalFeeResponse) {}
8
+ }
9
+
10
+ message CalculateWithdrawalFeeResponse {
11
+ double dollar_amount = 1;
12
+ double dollar_transaction_fee = 2;
13
+ int32 local_transaction_fee = 3;
14
+ string local_currency = 4;
15
+ string local_amount = 5;
16
+ double exchange_rate = 6;
17
+ repeated AppliedFee applied_fees = 7;
18
+ repeated string applied_fee_ids = 8;
19
+ Tax tax = 9;
20
+ }
21
+
22
+ message Tax {
23
+ string id = 1;
24
+ double dollar_tax = 2;
25
+ int32 local_tax = 3;
26
+ int32 gateway_recorded_tax = 4;
27
+ int32 surplus_on_gateway_recorded_tax = 5;
28
+ repeated Breakdown breakdown = 6;
29
+ }
30
+
31
+ message Breakdown {
32
+ string country = 1;
33
+ int32 flat_amount = 2;
34
+ string percentage_decimal = 3;
35
+ string rate_type = 4;
36
+ string state = 5;
37
+ string tax_type = 6;
38
+ string taxability_reason = 7;
39
+ }
40
+
41
+ message AppliedFee {
42
+ string id = 1;
43
+ string type = 2;
44
+ string display_name = 3;
45
+ double amount = 4;
46
+ string amount_type = 5;
47
+ int32 extra = 6;
48
+ double value = 7;
49
+ double value_usd = 8;
50
+ bool is_mainstack = 9;
51
+ bool is_processor = 10;
52
+ bool is_deposit = 11;
53
+ }
54
+
55
+ message CalculateWithdrawalFeeRequest {
56
+ string merchant_id = 1;
57
+ double amount = 2;
58
+ string currency = 3;
59
+ string payout_method = 4;
60
+ }
@@ -0,0 +1,17 @@
1
+ syntax = "proto3";
2
+
3
+ package hero.v1;
4
+
5
+ service HeroService {
6
+ rpc GetHero (HeroRequest) returns (HeroResponse) {}
7
+ }
8
+
9
+ message HeroRequest {
10
+ int32 id = 1;
11
+ }
12
+
13
+ message HeroResponse {
14
+ int32 id = 1;
15
+ string name = 2;
16
+ string power = 3;
17
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@themainstack/communication",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "description": "Unified gRPC framework for inter-service communication - auto-generates protos, creates servers, and provides type-safe clients",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc -p tsconfig.json",
10
+ "prepublishOnly": "npm run build",
11
+ "test": "npx vitest --run"
12
+ },
13
+ "author": "Mainstack Engineering",
14
+ "license": "MIT",
15
+ "devDependencies": {
16
+ "typescript": "^5.3.3",
17
+ "vitest": "^4.0.7"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "dependencies": {
23
+ "@grpc/grpc-js": "^1.14.3",
24
+ "@grpc/proto-loader": "^0.8.0"
25
+ }
26
+ }
@@ -0,0 +1,48 @@
1
+ import * as grpc from '@grpc/grpc-js';
2
+ import * as protoLoader from '@grpc/proto-loader';
3
+ import { GrpcClientOptions } from './types';
4
+
5
+ export class GrpcClientFactory {
6
+ static createClient<T extends grpc.Client>(options: GrpcClientOptions): T {
7
+ const packageDefinition = protoLoader.loadSync(options.protoPath, {
8
+ keepCase: true,
9
+ longs: String,
10
+ enums: String,
11
+ defaults: true,
12
+ oneofs: true,
13
+ ...options.loaderOptions,
14
+ });
15
+
16
+ const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
17
+
18
+ let serviceDef: any = protoDescriptor;
19
+
20
+ // Default to empty string (root) if not provided
21
+ const packageName = options.packageName || '';
22
+
23
+ // Only traverse if packageName is non-empty
24
+ if (packageName) {
25
+ const pkgNameParts = packageName.split('.');
26
+ for (const part of pkgNameParts) {
27
+ if (serviceDef[part]) {
28
+ serviceDef = serviceDef[part];
29
+ } else {
30
+ throw new Error(`Package part '${part}' not found in proto definition`);
31
+ }
32
+ }
33
+ }
34
+
35
+ if (!serviceDef[options.serviceName]) {
36
+ const location = packageName ? `package '${packageName}'` : 'root definition';
37
+ throw new Error(`Service '${options.serviceName}' not found in ${location}`);
38
+ }
39
+
40
+ const ServiceClient = serviceDef[options.serviceName];
41
+
42
+ return new ServiceClient(
43
+ options.url,
44
+ options.credentials || grpc.credentials.createInsecure(),
45
+ options.channelOptions
46
+ ) as T;
47
+ }
48
+ }
@@ -0,0 +1,31 @@
1
+ import { ServiceError, status } from '@grpc/grpc-js';
2
+
3
+ export class GrpcError extends Error {
4
+ public code: status;
5
+ public details: string;
6
+ public metadata: any;
7
+
8
+ constructor(code: status, message: string, details?: string, metadata?: any) {
9
+ super(message);
10
+ this.name = 'GrpcError';
11
+ this.code = code;
12
+ this.details = details || message;
13
+ this.metadata = metadata;
14
+ }
15
+ }
16
+
17
+ export const handleGrpcError = (err: any): never => {
18
+ if (err && (err.code !== undefined || err.message)) {
19
+ const error = err as ServiceError;
20
+ // Map gRPC status codes to more friendly errors if needed
21
+ // For now, we wrap it in our GrpcError
22
+ throw new GrpcError(
23
+ error.code ?? status.UNKNOWN,
24
+ error.message || 'Unknown gRPC error',
25
+ error.details,
26
+ error.metadata
27
+ );
28
+ }
29
+
30
+ throw new GrpcError(status.UNKNOWN, 'An unexpected error occurred', JSON.stringify(err));
31
+ };