nodejs-quickstart-structure 1.15.1 → 1.16.1

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +14 -5
  3. package/lib/modules/app-setup.js +3 -3
  4. package/lib/modules/config-files.js +2 -2
  5. package/lib/modules/kafka-setup.js +70 -24
  6. package/package.json +1 -1
  7. package/templates/clean-architecture/js/src/index.js.ejs +9 -6
  8. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +12 -11
  9. package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +17 -1
  10. package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +36 -0
  11. package/templates/clean-architecture/ts/src/config/swagger.ts.ejs +1 -1
  12. package/templates/clean-architecture/ts/src/index.ts.ejs +16 -16
  13. package/templates/clean-architecture/ts/src/infrastructure/log/logger.spec.ts.ejs +0 -1
  14. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +19 -0
  15. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +17 -0
  16. package/templates/common/Dockerfile +2 -0
  17. package/templates/common/README.md.ejs +24 -1
  18. package/templates/common/database/js/models/User.js.ejs +2 -1
  19. package/templates/common/database/ts/models/User.ts.ejs +4 -3
  20. package/templates/common/eslint.config.mjs.ejs +30 -3
  21. package/templates/common/health/js/healthRoute.js.ejs +5 -2
  22. package/templates/common/health/ts/healthRoute.ts.ejs +5 -2
  23. package/templates/common/jest.config.js.ejs +4 -1
  24. package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +30 -0
  25. package/templates/common/kafka/js/messaging/baseConsumer.spec.js.ejs +58 -0
  26. package/templates/common/kafka/js/messaging/userEventSchema.js.ejs +11 -0
  27. package/templates/common/kafka/js/messaging/userEventSchema.spec.js.ejs +27 -0
  28. package/templates/common/kafka/js/messaging/welcomeEmailConsumer.js.ejs +31 -0
  29. package/templates/common/kafka/js/messaging/welcomeEmailConsumer.spec.js.ejs +49 -0
  30. package/templates/common/kafka/js/services/kafkaService.js.ejs +77 -23
  31. package/templates/common/kafka/js/services/kafkaService.spec.js.ejs +53 -7
  32. package/templates/common/kafka/ts/messaging/baseConsumer.spec.ts.ejs +50 -0
  33. package/templates/common/kafka/ts/messaging/baseConsumer.ts.ejs +27 -0
  34. package/templates/common/kafka/ts/messaging/userEventSchema.spec.ts.ejs +51 -0
  35. package/templates/common/kafka/ts/messaging/userEventSchema.ts.ejs +11 -0
  36. package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.spec.ts.ejs +49 -0
  37. package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.ts.ejs +25 -0
  38. package/templates/common/kafka/ts/services/kafkaService.spec.ts.ejs +22 -2
  39. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +65 -12
  40. package/templates/common/package.json.ejs +6 -4
  41. package/templates/common/shutdown/ts/gracefulShutdown.ts.ejs +8 -11
  42. package/templates/mvc/js/src/controllers/userController.js.ejs +15 -0
  43. package/templates/mvc/js/src/controllers/userController.spec.js.ejs +39 -0
  44. package/templates/mvc/js/src/index.js.ejs +20 -15
  45. package/templates/mvc/ts/src/config/swagger.ts.ejs +1 -1
  46. package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +18 -0
  47. package/templates/mvc/ts/src/controllers/userController.ts.ejs +16 -0
  48. package/templates/mvc/ts/src/index.ts.ejs +16 -18
  49. package/templates/mvc/ts/src/utils/logger.spec.ts.ejs +0 -1
@@ -0,0 +1,50 @@
1
+ import { BaseConsumer } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/baseConsumer<% } else { %>@/messaging/baseConsumer<% } %>';
2
+ import logger from '<%= loggerPath %>';
3
+
4
+ jest.mock('<%= loggerPath %>');
5
+
6
+ class TestConsumer extends BaseConsumer {
7
+ constructor() {
8
+ super();
9
+ }
10
+ get topic() { return 'test-topic'; }
11
+ get groupId() { return 'test-group'; }
12
+ async handle(data: any) {
13
+ logger.info('Handled', data);
14
+ }
15
+ }
16
+
17
+ describe('BaseConsumer', () => {
18
+ let consumer: TestConsumer;
19
+
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+ consumer = new TestConsumer();
23
+ });
24
+
25
+ it('should throw error if instantiated directly', () => {
26
+ // @ts-expect-error: Abstract class cannot be instantiated
27
+ expect(() => new BaseConsumer()).toThrow("Abstract class 'BaseConsumer' cannot be instantiated.");
28
+ });
29
+
30
+ it('should process a valid message', async () => {
31
+ const message = { value: Buffer.from(JSON.stringify({ test: 'data' })) };
32
+ await consumer.onMessage({ message } as any);
33
+ expect(logger.info).toHaveBeenCalledWith('Handled', { test: 'data' });
34
+ });
35
+
36
+ it('should handle invalid JSON', async () => {
37
+ const message = { value: Buffer.from('invalid-json') };
38
+ await consumer.onMessage({ message } as any);
39
+ expect(logger.error).toHaveBeenCalledWith(
40
+ expect.stringContaining('[Kafka] Error processing message on topic test-topic:'),
41
+ expect.anything()
42
+ );
43
+ });
44
+
45
+ it('should skip empty messages', async () => {
46
+ const message = { value: null };
47
+ await consumer.onMessage({ message } as any);
48
+ expect(logger.info).not.toHaveBeenCalled();
49
+ });
50
+ });
@@ -0,0 +1,27 @@
1
+ import { EachMessagePayload } from 'kafkajs';
2
+ import logger from '<%= loggerPath %>';
3
+
4
+ export abstract class BaseConsumer {
5
+ abstract topic: string;
6
+ abstract groupId: string;
7
+
8
+ public constructor() {
9
+ if (new.target === BaseConsumer) {
10
+ throw new Error("Abstract class 'BaseConsumer' cannot be instantiated.");
11
+ }
12
+ }
13
+
14
+ async onMessage({ message }: EachMessagePayload) {
15
+ try {
16
+ const rawValue = message.value?.toString();
17
+ if (!rawValue) return;
18
+
19
+ const data = JSON.parse(rawValue);
20
+ await this.handle(data);
21
+ } catch (error) {
22
+ logger.error(`[Kafka] Error processing message on topic ${this.topic}:`, error);
23
+ }
24
+ }
25
+
26
+ abstract handle(data: unknown): Promise<void>;
27
+ }
@@ -0,0 +1,51 @@
1
+ import { UserEventSchema } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/schemas/userEventSchema<% } else { %>@/messaging/schemas/userEventSchema<% } %>';
2
+
3
+ describe('UserEventSchema', () => {
4
+ it('should validate a correct USER_CREATED event', () => {
5
+ const validEvent = {
6
+ action: 'USER_CREATED',
7
+ payload: {
8
+ id: 1,
9
+ email: 'test@example.com'
10
+ }
11
+ };
12
+ const result = UserEventSchema.safeParse(validEvent);
13
+ expect(result.success).toBe(true);
14
+ });
15
+
16
+ it('should validate a correct UPDATE_USER event', () => {
17
+ const validEvent = {
18
+ action: 'UPDATE_USER',
19
+ payload: {
20
+ id: 'abc-123',
21
+ email: 'test@example.com'
22
+ }
23
+ };
24
+ const result = UserEventSchema.safeParse(validEvent);
25
+ expect(result.success).toBe(true);
26
+ });
27
+
28
+ it('should fail validation for invalid email', () => {
29
+ const invalidEvent = {
30
+ action: 'USER_CREATED',
31
+ payload: {
32
+ id: 1,
33
+ email: 'invalid-email'
34
+ }
35
+ };
36
+ const result = UserEventSchema.safeParse(invalidEvent);
37
+ expect(result.success).toBe(false);
38
+ });
39
+
40
+ it('should fail validation for invalid action', () => {
41
+ const invalidEvent = {
42
+ action: 'INVALID_ACTION',
43
+ payload: {
44
+ id: 1,
45
+ email: 'test@example.com'
46
+ }
47
+ };
48
+ const result = UserEventSchema.safeParse(invalidEvent);
49
+ expect(result.success).toBe(false);
50
+ });
51
+ });
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod';
2
+
3
+ export const UserEventSchema = z.object({
4
+ action: z.enum(['USER_CREATED', 'UPDATE_USER', 'DELETE_USER']),
5
+ payload: z.object({
6
+ id: z.union([z.string(), z.number()]),
7
+ email: z.string().email(),
8
+ }),
9
+ });
10
+
11
+ export type UserEvent = z.infer<typeof UserEventSchema>;
@@ -0,0 +1,49 @@
1
+ import { WelcomeEmailConsumer } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/consumers/instances/welcomeEmailConsumer<% } else { %>@/messaging/consumers/instances/welcomeEmailConsumer<% } %>';
2
+ import logger from '<%= loggerPath %>';
3
+
4
+ jest.mock('<%= loggerPath %>');
5
+
6
+ describe('WelcomeEmailConsumer', () => {
7
+ let consumer: WelcomeEmailConsumer;
8
+
9
+ beforeEach(() => {
10
+ jest.clearAllMocks();
11
+ consumer = new WelcomeEmailConsumer();
12
+ });
13
+
14
+ it('should log welcome email simulation for USER_CREATED action', async () => {
15
+ const data = {
16
+ action: 'USER_CREATED',
17
+ payload: {
18
+ id: 1,
19
+ email: 'test@example.com'
20
+ }
21
+ };
22
+
23
+ await consumer.handle(data);
24
+
25
+ expect(logger.info).toHaveBeenCalledWith(
26
+ expect.stringContaining('[Kafka] Consumer: Received USER_CREATED.')
27
+ );
28
+ expect(logger.info).toHaveBeenCalledWith(
29
+ expect.stringContaining('📧 Sending welcome email to \'test@example.com\'... Done!')
30
+ );
31
+ });
32
+
33
+ it('should log error for invalid data', async () => {
34
+ const data = {
35
+ action: 'USER_CREATED',
36
+ payload: {
37
+ id: 1,
38
+ email: 'invalid-email'
39
+ }
40
+ };
41
+
42
+ await consumer.handle(data);
43
+
44
+ expect(logger.error).toHaveBeenCalledWith(
45
+ expect.stringContaining('[Kafka] Invalid user event data:'),
46
+ expect.anything()
47
+ );
48
+ });
49
+ });
@@ -0,0 +1,25 @@
1
+ import { BaseConsumer } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/baseConsumer<% } else { %>@/messaging/baseConsumer<% } %>';
2
+ import logger from '<%= loggerPath %>';
3
+ import { UserEventSchema } from '<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/schemas/userEventSchema<% } else { %>@/messaging/schemas/userEventSchema<% } %>';
4
+
5
+ export class WelcomeEmailConsumer extends BaseConsumer {
6
+ topic = 'user-topic';
7
+ groupId = 'welcome-email-group';
8
+
9
+ async handle(data: unknown) {
10
+ const result = UserEventSchema.safeParse(data);
11
+
12
+ if (!result.success) {
13
+ logger.error('[Kafka] Invalid user event data:', result.error.format());
14
+ return;
15
+ }
16
+
17
+ const { action, payload } = result.data;
18
+
19
+ if (action === 'USER_CREATED') {
20
+ logger.info(`[Kafka] Consumer: Received USER_CREATED.`);
21
+ logger.info(`[Kafka] Consumer: 📧 Sending welcome email to '${payload.email}'... Done!`);
22
+ // In a real app, you would call an EmailService here
23
+ }
24
+ }
25
+ }
@@ -34,13 +34,14 @@ describe('KafkaService', () => {
34
34
 
35
35
  expect(producer.connect).toHaveBeenCalled();
36
36
  expect(consumer.connect).toHaveBeenCalled();
37
- expect(consumer.subscribe).toHaveBeenCalledWith({ topic: 'test-topic', fromBeginning: true });
37
+ expect(consumer.subscribe).toHaveBeenCalledWith(expect.objectContaining({ topic: 'user-topic', fromBeginning: true }));
38
38
  expect(consumer.run).toHaveBeenCalled();
39
39
  });
40
40
 
41
41
  it('should send a message', async () => {
42
+ await kafkaService.connect();
42
43
  const topic = 'test-topic';
43
- const message = 'test-message';
44
+ const message = JSON.stringify({ action: 'TEST', payload: { email: 'test@example.com' } });
44
45
  await kafkaService.sendMessage(topic, message);
45
46
  const producer = (kafka.producer as jest.Mock).mock.results[0].value;
46
47
 
@@ -50,6 +51,25 @@ describe('KafkaService', () => {
50
51
  });
51
52
  });
52
53
 
54
+ it('should retry connection on failure', async () => {
55
+ const producer = (kafka.producer as jest.Mock).mock.results[0].value;
56
+ producer.connect
57
+ .mockRejectedValueOnce(new Error('Connection failed'))
58
+ .mockResolvedValueOnce(undefined);
59
+
60
+ jest.useFakeTimers();
61
+ const connectPromise = kafkaService.connect(2);
62
+
63
+ await jest.advanceTimersByTimeAsync(10000);
64
+ await connectPromise;
65
+
66
+ expect(producer.connect).toHaveBeenCalledTimes(2);
67
+ });
68
+
69
+ it('should throw error if producer not connected', async () => {
70
+ await expect(kafkaService.sendMessage('topic', 'msg')).rejects.toThrow('[Kafka] Producer not connected');
71
+ });
72
+
53
73
  it('should disconnect producer and consumer', async () => {
54
74
  await kafkaService.disconnect();
55
75
  const producer = (kafka.producer as jest.Mock).mock.results[0].value;
@@ -5,33 +5,85 @@ import logger from '<%= loggerPath %>';
5
5
  export class KafkaService {
6
6
  private producer: Producer;
7
7
  private consumer: Consumer;
8
+ private isConnected = false;
9
+ private connectionPromise: Promise<void> | null = null;
8
10
 
9
11
  constructor() {
10
12
  this.producer = kafka.producer();
11
13
  this.consumer = kafka.consumer({ groupId: 'test-group' });
12
14
  }
13
15
 
14
- async connect() {
15
- await this.producer.connect();
16
- await this.consumer.connect();
17
- await this.consumer.subscribe({ topic: 'test-topic', fromBeginning: true });
18
-
19
- await this.consumer.run({
20
- eachMessage: async ({ message }: EachMessagePayload) => {
21
- logger.info({
22
- value: message.value?.toString(),
23
- });
24
- },
25
- });
16
+ async connect(retries = 10) {
17
+ if (this.connectionPromise) return this.connectionPromise;
18
+
19
+ this.connectionPromise = (async () => {
20
+ let attempt = 0;
21
+ // Auto-register WelcomeEmailConsumer if it exists
22
+ // Note: Dynamic import used here for simplicity and to avoid startup crashes.
23
+ // In enterprise production, consider using Dependency Injection.
24
+ const { WelcomeEmailConsumer } = await import('<% if (architecture === "Clean Architecture") { %>@/interfaces/messaging/consumers/instances/welcomeEmailConsumer<% } else { %>@/messaging/consumers/instances/welcomeEmailConsumer<% } %>');
25
+ while (attempt < retries) {
26
+ try {
27
+ await this.producer.connect();
28
+ await this.consumer.connect();
29
+ logger.info('[Kafka] Producer connected successfully');
30
+ logger.info('[Kafka] Consumer connected successfully');
31
+ this.isConnected = true;
32
+
33
+ try {
34
+ const welcomeConsumer = new WelcomeEmailConsumer();
35
+ await this.consumer.subscribe({ topic: welcomeConsumer.topic, fromBeginning: true });
36
+ logger.info(`[Kafka] Registered consumer for topic: ${welcomeConsumer.topic}`);
37
+
38
+ await this.consumer.run({
39
+ eachMessage: async (payload) => welcomeConsumer.onMessage(payload),
40
+ });
41
+ } catch (e) {
42
+ // Fallback or no consumers found
43
+ logger.warn(`[Kafka] Could not load WelcomeEmailConsumer, using fallback: ${(e as Error).message}`);
44
+ await this.consumer.subscribe({ topic: 'user-topic', fromBeginning: true });
45
+ await this.consumer.run({
46
+ eachMessage: async ({ message }: EachMessagePayload) => {
47
+ logger.info(`[Kafka] Consumer: Received message on user-topic: ${message.value?.toString()}`);
48
+ },
49
+ });
50
+ }
51
+ return; // Success
52
+ } catch (error) {
53
+ attempt++;
54
+ logger.error(`[Kafka] Connection attempt ${attempt} failed:`, (error as Error).message);
55
+ if (attempt >= retries) {
56
+ throw error;
57
+ }
58
+ await new Promise(res => setTimeout(res, 3000));
59
+ }
60
+ }
61
+ })();
62
+
63
+ return this.connectionPromise;
26
64
  }
27
65
 
28
66
  async sendMessage(topic: string, message: string) {
67
+ if (this.connectionPromise) {
68
+ await this.connectionPromise;
69
+ }
70
+
71
+ if (!this.isConnected) {
72
+ throw new Error('[Kafka] Producer not connected. Check logs for connection errors.');
73
+ }
74
+
29
75
  await this.producer.send({
30
76
  topic,
31
77
  messages: [
32
78
  { value: message },
33
79
  ],
34
80
  });
81
+ try {
82
+ const parsed = JSON.parse(message);
83
+ logger.info(`[Kafka] Producer: Sent ${parsed.action} event for '${parsed.payload?.email || 'unknown'}'`);
84
+ } catch {
85
+ logger.info(`[Kafka] Producer: Sent message to ${topic}`);
86
+ }
35
87
  }
36
88
 
37
89
  async disconnect() {
@@ -40,3 +92,4 @@ export class KafkaService {
40
92
  }
41
93
  }
42
94
 
95
+ export const kafkaService = new KafkaService();
@@ -6,7 +6,7 @@
6
6
  "scripts": {
7
7
  "start": "<% if (language === 'TypeScript') { %>node dist/index.js<% } else { %>node src/index.js<% } %>",
8
8
  "dev": "<% if (language === 'TypeScript') { %>nodemon --exec ts-node -r tsconfig-paths/register src/index.ts<% } else { %>nodemon src/index.js<% } %>"<% if (language === 'TypeScript') { %>,
9
- "build": "rimraf dist && tsc && tsc-alias<% if (viewEngine && viewEngine !== 'None') { %> && cpx \"src/views/**/*\" dist/views<% } %><% if (communication === 'REST APIs') { %> && cpx \"src/**/*.yml\" dist/<% } %>"<% } %>,
9
+ "build": "rimraf dist && tsc && tsc-alias<% if (viewEngine && viewEngine !== 'None') { %> && cpx \"src/views/**/*\" dist/views<% } %><% if (communication === 'REST APIs' || communication === 'Kafka') { %> && cpx \"src/**/*.yml\" dist/<% } %>"<% } %>,
10
10
  "deploy": "npx pm2 start ecosystem.config.js --env production",
11
11
  "lint": "eslint .",
12
12
  "lint:fix": "eslint . --fix",
@@ -47,7 +47,7 @@
47
47
  "express-rate-limit": "^7.1.5",
48
48
  "winston": "^3.11.0",
49
49
  "winston-daily-rotate-file": "^5.0.0",
50
- "morgan": "^1.10.0"<% if (communication === 'REST APIs') { %>,
50
+ "morgan": "^1.10.0"<% if (communication === 'REST APIs' || communication === 'Kafka') { %>,
51
51
  "swagger-ui-express": "^5.0.0",
52
52
  "yamljs": "^0.3.0"<% } %><% if (communication === 'GraphQL') { %>,
53
53
  "@apollo/server": "^4.10.0",
@@ -72,16 +72,18 @@
72
72
  "@types/sequelize": "^4.28.19",
73
73
  <%_ } -%>
74
74
  "@types/morgan": "^1.9.9",
75
- "rimraf": "^6.0.1"<% if ((viewEngine && viewEngine !== 'None') || communication === 'REST APIs') { %>,
75
+ "rimraf": "^6.0.1"<% if ((viewEngine && viewEngine !== 'None') || communication === 'REST APIs' || communication === 'Kafka') { %>,
76
76
  "cpx2": "^8.0.0"<% } %><% } %>,
77
77
  "eslint": "^9.20.1",
78
78
  "@eslint/js": "^9.20.0",
79
79
  "globals": "^15.14.0",
80
80
  "prettier": "^3.5.1",
81
81
  "eslint-config-prettier": "^10.0.1",
82
+ "eslint-plugin-import-x": "^4.6.1",
83
+ "eslint-import-resolver-typescript": "^3.7.0",
82
84
  "husky": "^8.0.3",
83
85
  "lint-staged": "^15.4.3"<% if (language === 'TypeScript') { %>,
84
- "typescript-eslint": "^8.24.1",<%_ if (communication === 'REST APIs') { %>
86
+ "typescript-eslint": "^8.24.1",<%_ if (communication === 'REST APIs' || communication === 'Kafka') { %>
85
87
  "@types/swagger-ui-express": "^4.1.6",
86
88
  "@types/yamljs": "^0.2.34",<%_ } %>
87
89
  "jest": "^29.7.0",
@@ -4,6 +4,14 @@ import logger from '@/utils/logger';
4
4
  <%_ } else { -%>
5
5
  import logger from '@/infrastructure/log/logger';
6
6
  <%_ } -%>
7
+ <%_ if (database === 'MongoDB') { -%>
8
+ import mongoose from 'mongoose';
9
+ <%_ } else if (database !== 'None') { -%>
10
+ import sequelize from '<% if (architecture === "MVC") { %>@/config/database<% } else { %>@/infrastructure/database/database<% } %>';
11
+ <%_ } -%>
12
+ <%_ if (caching === 'Redis') { -%>
13
+ import redisService from '<% if (architecture === "MVC") { %>@/config/redisClient<% } else { %>@/infrastructure/caching/redisClient<% } %>';
14
+ <%_ } -%>
7
15
 
8
16
  export const setupGracefulShutdown = (server: Server<% if (communication === 'Kafka') { %>, kafkaService: { disconnect: () => Promise<void> }<% } %>) => {
9
17
  const gracefulShutdown = async (signal: string) => {
@@ -13,25 +21,14 @@ export const setupGracefulShutdown = (server: Server<% if (communication === 'Ka
13
21
  try {
14
22
  <%_ if (database !== 'None') { -%>
15
23
  <%_ if (database === 'MongoDB') { -%>
16
- const mongoose = (await import('mongoose')).default;
17
24
  await mongoose.connection.close(false);
18
25
  logger.info('MongoDB connection closed.');
19
26
  <%_ } else { -%>
20
- <%_ if (architecture === 'MVC') { -%>
21
- const sequelize = (await import('@/config/database')).default;
22
- <%_ } else { -%>
23
- const sequelize = (await import('@/infrastructure/database/database')).default;
24
- <%_ } -%>
25
27
  await sequelize.close();
26
28
  logger.info('Database connection closed.');
27
29
  <%_ } -%>
28
30
  <%_ } -%>
29
31
  <%_ if (caching === 'Redis') { -%>
30
- <%_ if (architecture === 'MVC') { -%>
31
- const redisService = (await import('@/config/redisClient')).default;
32
- <%_ } else { -%>
33
- const redisService = (await import('@/infrastructure/caching/redisClient')).default;
34
- <%_ } -%>
35
32
  await redisService.quit();
36
33
  logger.info('Redis connection closed.');
37
34
  <%_ } -%>
@@ -8,6 +8,9 @@ const cacheService = require('../config/redisClient');
8
8
  <%_ } else if (caching === 'Memory Cache') { -%>
9
9
  const cacheService = require('../config/memoryCache');
10
10
  <%_ } -%>
11
+ <%_ if (communication === 'Kafka') { -%>
12
+ const { sendMessage } = require('../services/kafkaService');
13
+ <%_ } -%>
11
14
 
12
15
  <% if (communication === 'GraphQL') { -%>
13
16
  const getUsers = async () => {
@@ -40,6 +43,12 @@ const createUser = async (data) => {
40
43
  const user = await User.create({ name, email });
41
44
  <%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
42
45
  await cacheService.del('users:all');
46
+ <%_ } -%>
47
+ <%_ if (communication === 'Kafka') { -%>
48
+ await sendMessage('user-topic', JSON.stringify({
49
+ action: 'USER_CREATED',
50
+ payload: { id: user.id || user._id, email: user.email }
51
+ }));
43
52
  <%_ } -%>
44
53
  return user;
45
54
  } catch (error) {
@@ -78,6 +87,12 @@ const createUser = async (req, res, next) => {
78
87
  const user = await User.create({ name, email });
79
88
  <%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
80
89
  await cacheService.del('users:all');
90
+ <%_ } -%>
91
+ <%_ if (communication === 'Kafka') { -%>
92
+ await sendMessage('user-topic', JSON.stringify({
93
+ action: 'USER_CREATED',
94
+ payload: { id: user.id || user._id, email: user.email }
95
+ }));
81
96
  <%_ } -%>
82
97
  res.status(HTTP_STATUS.CREATED).json(user);
83
98
  } catch (error) {
@@ -23,6 +23,14 @@ jest.mock('@/config/memoryCache', () => ({
23
23
  }));
24
24
  <%_ } -%>
25
25
  jest.mock('@/utils/logger');
26
+ <%_ if (communication === 'Kafka') { -%>
27
+ const { sendMessage } = require('@/services/kafkaService');
28
+ jest.mock('@/services/kafkaService', () => ({
29
+ sendMessage: jest.fn().mockResolvedValue(undefined),
30
+ connectKafka: jest.fn().mockResolvedValue(undefined)
31
+ }));
32
+ <%_ } -%>
33
+
26
34
 
27
35
  describe('UserController', () => {
28
36
  <% if (communication !== 'GraphQL') { -%>
@@ -143,6 +151,9 @@ describe('UserController', () => {
143
151
  <% } -%>
144
152
  <%_ if (caching === 'Redis' || caching === 'Memory Cache') { -%>
145
153
  expect(cacheService.del).toHaveBeenCalledWith('users:all');
154
+ <%_ } -%>
155
+ <%_ if (communication === 'Kafka') { -%>
156
+ expect(sendMessage).toHaveBeenCalled();
146
157
  <%_ } -%>
147
158
  });
148
159
 
@@ -166,5 +177,33 @@ describe('UserController', () => {
166
177
  expect(mockNext).toHaveBeenCalledWith(error);
167
178
  <% } -%>
168
179
  });
180
+
181
+ <%_ if (communication === 'Kafka') { -%>
182
+ it('should successfully create a new user with _id for Kafka (Happy Path)', async () => {
183
+ // Arrange
184
+ const payload = { name: 'Bob', email: 'bob@example.com' };
185
+ <% if (communication === 'GraphQL') { -%>
186
+ const dataArg = payload;
187
+ <% } else { -%>
188
+ mockRequest.body = payload;
189
+ <% } -%>
190
+
191
+ const expectedUser = { _id: '2', ...payload };
192
+ User.create.mockResolvedValue(expectedUser);
193
+
194
+ // Act
195
+ <% if (communication === 'GraphQL') { -%>
196
+ await createUser(dataArg);
197
+ <% } else { -%>
198
+ await createUser(mockRequest, mockResponse, mockNext);
199
+ <% } -%>
200
+
201
+ // Assert
202
+ expect(sendMessage).toHaveBeenCalledWith(
203
+ 'user-topic',
204
+ expect.stringContaining('"id":"2"')
205
+ );
206
+ });
207
+ <%_ } -%>
169
208
  });
170
209
  });
@@ -1,6 +1,6 @@
1
1
  const express = require('express');
2
2
  const cors = require('cors');
3
- <%_ if (communication === 'REST APIs') { -%>const apiRoutes = require('./routes/api');<%_ } %>
3
+ <%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>const apiRoutes = require('./routes/api');<%_ } %>
4
4
  const healthRoutes = require('./routes/healthRoute');
5
5
  <%_ if (communication === 'Kafka') { -%>const { connectKafka, sendMessage } = require('./services/kafkaService');<%_ } -%>
6
6
  <%_ if (communication === 'GraphQL') { -%>
@@ -11,12 +11,13 @@ const { unwrapResolverError } = require('@apollo/server/errors');
11
11
  const { ApiError } = require('./errors/ApiError');
12
12
  const { typeDefs, resolvers } = require('./graphql');
13
13
  const { gqlContext } = require('./graphql/context');
14
- <% } -%>
15
- <%_ if (communication === 'REST APIs') { -%>
14
+ <% } %>
15
+ <%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
16
16
  const swaggerUi = require('swagger-ui-express');
17
17
  const swaggerSpecs = require('./config/swagger');
18
18
  <%_ } -%>
19
19
  const { env } = require('./config/env');
20
+ const setupGracefulShutdown = require('./utils/gracefulShutdown');
20
21
 
21
22
  const app = express();
22
23
  const PORT = env.PORT;
@@ -28,7 +29,7 @@ app.use(cors());
28
29
  app.use(express.json());
29
30
  app.use(morgan('combined', { stream: { write: message => logger.info(message.trim()) } }));
30
31
 
31
- <%_ if (communication === 'REST APIs') { -%>
32
+ <%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
32
33
  app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
33
34
  <%_ } -%>
34
35
  <%_ if (viewEngine === 'EJS' || viewEngine === 'Pug') { -%>
@@ -37,7 +38,7 @@ const path = require('path');
37
38
  app.set('views', path.join(__dirname, 'views'));
38
39
  app.set('view engine', '<%= viewEngine.toLowerCase() %>');
39
40
  app.use(express.static(path.join(__dirname, '../public')));<%_ } %>
40
- <%_ if (communication === 'REST APIs') { -%>
41
+ <%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
41
42
  app.use('/api', apiRoutes);
42
43
  <%_ } -%><% if (viewEngine && viewEngine !== 'None') { -%>
43
44
  app.get('/', (req, res) => {
@@ -86,34 +87,38 @@ const startServer = async () => {
86
87
  const server = app.listen(PORT, () => {
87
88
  logger.info(`Server running on port ${PORT}`);
88
89
  <%_ if (communication === 'Kafka') { -%>
89
- connectKafka().then(() => {
90
- logger.info('Kafka connected');
91
- sendMessage('test-topic', 'Hello Kafka from MVC JS!');
92
- }).catch(err => {
93
- logger.error('Failed to connect to Kafka:', err);
94
- });
90
+ connectKafka()
91
+ .then(async () => {
92
+ logger.info('Kafka connected');
93
+ })
94
+ .catch(err => {
95
+ logger.error('Failed to connect to Kafka after retries:', err.message);
96
+ });
95
97
  <%_ } -%>
96
98
  });
97
99
 
98
- const setupGracefulShutdown = require('./utils/gracefulShutdown');
99
100
  setupGracefulShutdown(server);
100
101
  };
101
102
 
102
103
  <%_ if (database !== 'None') { -%>
103
104
  // Database Sync
105
+ <%_ if (database !== 'None') { -%>
106
+ <%_ if (database === 'MongoDB') { -%>
107
+ const connectDB = require('./config/database');
108
+ <%_ } else { -%>
109
+ const sequelize = require('./config/database');
110
+ <%_ } -%>
111
+ <%_ } -%>
104
112
  const syncDatabase = async () => {
105
113
  let retries = 30;
106
114
  while (retries) {
107
115
  try {
108
116
  <%_ if (database === 'MongoDB') { -%>
109
- const connectDB = require('./config/database');
110
117
  await connectDB();
111
118
  <%_ } else { -%>
112
- const sequelize = require('./config/database');
113
119
  await sequelize.sync();
114
120
  <%_ } -%>
115
121
  logger.info('Database synced');
116
-
117
122
  // Start Server after DB is ready
118
123
  await startServer();
119
124
  break;
@@ -1,4 +1,4 @@
1
- <% if (communication === 'REST APIs') { %>import path from 'path';
1
+ <% if (communication === 'REST APIs' || communication === 'Kafka') { %>import path from 'path';
2
2
  import YAML from 'yamljs';
3
3
 
4
4
  const swaggerDocument = YAML.load(path.join(__dirname, 'swagger.yml'));