nodejs-quickstart-structure 1.15.1 → 1.16.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/lib/modules/app-setup.js +3 -3
  3. package/lib/modules/config-files.js +2 -2
  4. package/lib/modules/kafka-setup.js +70 -24
  5. package/package.json +1 -1
  6. package/templates/clean-architecture/js/src/index.js.ejs +1 -3
  7. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +11 -10
  8. package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +16 -1
  9. package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +36 -0
  10. package/templates/clean-architecture/ts/src/config/swagger.ts.ejs +1 -1
  11. package/templates/clean-architecture/ts/src/index.ts.ejs +12 -14
  12. package/templates/clean-architecture/ts/src/infrastructure/log/logger.spec.ts.ejs +0 -1
  13. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +19 -0
  14. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +16 -0
  15. package/templates/common/Dockerfile +2 -0
  16. package/templates/common/README.md.ejs +24 -1
  17. package/templates/common/database/js/models/User.js.ejs +2 -1
  18. package/templates/common/database/ts/models/User.ts.ejs +4 -3
  19. package/templates/common/eslint.config.mjs.ejs +30 -3
  20. package/templates/common/jest.config.js.ejs +4 -1
  21. package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +30 -0
  22. package/templates/common/kafka/js/messaging/baseConsumer.spec.js.ejs +58 -0
  23. package/templates/common/kafka/js/messaging/userEventSchema.js.ejs +11 -0
  24. package/templates/common/kafka/js/messaging/userEventSchema.spec.js.ejs +27 -0
  25. package/templates/common/kafka/js/messaging/welcomeEmailConsumer.js.ejs +31 -0
  26. package/templates/common/kafka/js/messaging/welcomeEmailConsumer.spec.js.ejs +49 -0
  27. package/templates/common/kafka/js/services/kafkaService.js.ejs +75 -23
  28. package/templates/common/kafka/js/services/kafkaService.spec.js.ejs +53 -7
  29. package/templates/common/kafka/ts/messaging/baseConsumer.spec.ts.ejs +50 -0
  30. package/templates/common/kafka/ts/messaging/baseConsumer.ts.ejs +27 -0
  31. package/templates/common/kafka/ts/messaging/userEventSchema.spec.ts.ejs +51 -0
  32. package/templates/common/kafka/ts/messaging/userEventSchema.ts.ejs +11 -0
  33. package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.spec.ts.ejs +49 -0
  34. package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.ts.ejs +25 -0
  35. package/templates/common/kafka/ts/services/kafkaService.spec.ts.ejs +22 -2
  36. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +72 -12
  37. package/templates/common/package.json.ejs +6 -4
  38. package/templates/mvc/js/src/controllers/userController.js.ejs +14 -0
  39. package/templates/mvc/js/src/controllers/userController.spec.js.ejs +39 -0
  40. package/templates/mvc/js/src/index.js.ejs +12 -11
  41. package/templates/mvc/ts/src/config/swagger.ts.ejs +1 -1
  42. package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +18 -0
  43. package/templates/mvc/ts/src/controllers/userController.ts.ejs +16 -0
  44. package/templates/mvc/ts/src/index.ts.ejs +13 -16
  45. package/templates/mvc/ts/src/utils/logger.spec.ts.ejs +0 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.16.0] - 2026-03-14
9
+
10
+ ### Added
11
+ - **Robust Kafka Singleton Implementation**: Refactored `KafkaService` to a strict singleton pattern across all architectures with `connectionPromise` and automated retry logic for resilient messaging.
12
+ - **BaseConsumer Standards**: Implemented constructor guards in `BaseConsumer` to prevent direct instantiation of abstract messaging classes.
13
+ - **Professional Docker Log Hygiene**: Added `NPM_CONFIG_UPDATE_NOTIFIER=false` to both builder and production stages to suppress non-essential npm upgrade notifications.
14
+
15
+ ### Fixed
16
+ - **Docker Build & Type Safety**: Resolved a critical build failure in MVC TypeScript projects using EJS/Pug by addressing missing Express `Request` and `Response` type imports.
17
+ - **Network Resilience**: Removed redundant `npm install -g npm@latest` from the `Dockerfile` template to fix `ECONNRESET` failures during project verification on unstable networks.
18
+ - **Controller Testing Modernization**: Refactored `userController` spec templates (TS and JS) to correctly mock and verify the shared Kafka singleton, fixing persistent unit test failures.
19
+ - **Database Mocking Refinement**: Resolved a data flow bug in the "None" database mock where generated IDs were being overwritten, and enhanced TypeScript types to eliminate `any` in repository patterns.
20
+
8
21
  ## [1.15.1] - 2026-03-12
9
22
 
10
23
  ### Added
@@ -165,8 +165,8 @@ export const renderDynamicComponents = async (templatePath, targetDir, config) =
165
165
  }
166
166
  }
167
167
 
168
- // Cleanup REST routes if GraphQL or Kafka is selected
169
- if (config.communication !== 'REST APIs') {
168
+ // Cleanup REST routes if neither REST APIs nor Kafka is selected
169
+ if (config.communication !== 'REST APIs' && config.communication !== 'Kafka') {
170
170
  if (architecture === 'MVC') {
171
171
  await fs.remove(path.join(targetDir, 'src/routes'));
172
172
  } else if (architecture === 'Clean Architecture') {
@@ -280,7 +280,7 @@ export const renderSwaggerConfig = async (templatesDir, targetDir, config) => {
280
280
  }
281
281
  await fs.ensureDir(configDir);
282
282
 
283
- if (communication === 'REST APIs') {
283
+ if (communication === 'REST APIs' || communication === 'Kafka') {
284
284
  const swaggerYmlTemplateSource = path.join(templatesDir, 'common', 'swagger.yml.ejs');
285
285
  if (await fs.pathExists(swaggerYmlTemplateSource)) {
286
286
  const ymlContent = ejs.render(await fs.readFile(swaggerYmlTemplateSource, 'utf-8'), { projectName });
@@ -97,10 +97,10 @@ export const renderTestSample = async (templatesDir, targetDir, config) => {
97
97
  "extends": "../tsconfig.json",
98
98
  "compilerOptions": {
99
99
  "types": ["jest", "node"],
100
- "rootDir": "../tests",
100
+ "rootDir": "..",
101
101
  "noEmit": true
102
102
  },
103
- "include": ["**/*.ts"]
103
+ "include": ["**/*.ts", "../src/**/*.ts"]
104
104
  };
105
105
  await fs.writeFile(path.join(targetDir, 'tests', 'tsconfig.json'), JSON.stringify(testsTsConfig, null, 4));
106
106
  }
@@ -8,7 +8,14 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
8
8
 
9
9
  const langExt = language === 'TypeScript' ? 'ts' : 'js';
10
10
  const kafkaSource = path.join(templatesDir, 'common', 'kafka', langExt);
11
- await fs.copy(kafkaSource, path.join(targetDir, 'src'));
11
+
12
+ // 1. Copy necessary directories individually (to avoid orphaned templates in src)
13
+ if (await fs.pathExists(path.join(kafkaSource, 'services'))) {
14
+ await fs.copy(path.join(kafkaSource, 'services'), path.join(targetDir, 'src/services'));
15
+ }
16
+ if (await fs.pathExists(path.join(kafkaSource, 'config'))) {
17
+ await fs.copy(path.join(kafkaSource, 'config'), path.join(targetDir, 'src/config'));
18
+ }
12
19
 
13
20
  // Render Kafka Service with dynamic logger path
14
21
  const kafkaServiceFileName = `kafkaService.${langExt}`;
@@ -16,7 +23,7 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
16
23
  // Render Kafka Service Spec
17
24
  const kafkaSpecFileName = `kafkaService.spec.${langExt}`;
18
25
  const kafkaSpecTemplate = path.join(targetDir, 'src', 'services', `${kafkaSpecFileName}.ejs`);
19
-
26
+
20
27
  if (await fs.pathExists(kafkaServiceTemplate)) {
21
28
  let serviceLoggerPath, serviceConfigPath;
22
29
  if (language === 'TypeScript') {
@@ -39,10 +46,9 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
39
46
  specConfigPath = architecture === 'Clean Architecture' ? '@/infrastructure/config/kafka' : '@/config/kafka';
40
47
  specServicePath = architecture === 'Clean Architecture' ? '@/infrastructure/messaging/kafkaClient' : '@/services/kafkaService';
41
48
  } else {
42
- // For JS tests, we use @/ to ensure resolution after move to tests/
43
- specLoggerPath = architecture === 'Clean Architecture' ? '@/infrastructure/log/logger' : '@/utils/logger';
44
- specConfigPath = architecture === 'Clean Architecture' ? '@/infrastructure/config/kafka' : '@/config/kafka';
45
- specServicePath = architecture === 'Clean Architecture' ? '@/infrastructure/messaging/kafkaClient' : '@/services/kafkaService';
49
+ specLoggerPath = architecture === 'Clean Architecture' ? '../../infrastructure/log/logger' : '../utils/logger';
50
+ specConfigPath = architecture === 'Clean Architecture' ? '../../infrastructure/config/kafka' : '../config/kafka';
51
+ specServicePath = architecture === 'Clean Architecture' ? '../../infrastructure/messaging/kafkaClient' : '../services/kafkaService';
46
52
  }
47
53
 
48
54
  const specContent = ejs.render(await fs.readFile(kafkaSpecTemplate, 'utf-8'), { ...config, loggerPath: specLoggerPath, configPath: specConfigPath, servicePath: specServicePath });
@@ -64,7 +70,6 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
64
70
  await fs.ensureDir(path.dirname(specTarget));
65
71
  await fs.writeFile(specTarget, specContent);
66
72
 
67
- // Remove the template from src in targetDir to avoid double processing by processAllTests
68
73
  const targetSpecTemplate = path.join(targetDir, 'src', 'config', `${kafkaConfigSpecFileName}.ejs`);
69
74
  if (await fs.pathExists(targetSpecTemplate)) {
70
75
  await fs.remove(targetSpecTemplate);
@@ -79,14 +84,12 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
79
84
 
80
85
  const serviceExt = language === 'TypeScript' ? 'ts' : 'js';
81
86
 
82
- // Move Service to Infrastructure/Messaging
83
87
  await fs.move(
84
88
  path.join(targetDir, `src/services/kafkaService.${serviceExt}`),
85
89
  path.join(targetDir, `src/infrastructure/messaging/kafkaClient.${serviceExt}`),
86
90
  { overwrite: true }
87
91
  );
88
92
 
89
- // Move Spec to Tests/Infrastructure/Messaging
90
93
  if (await fs.pathExists(path.join(targetDir, `src/services/kafkaService.spec.${serviceExt}`))) {
91
94
  await fs.move(
92
95
  path.join(targetDir, `src/services/kafkaService.spec.${serviceExt}`),
@@ -95,8 +98,6 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
95
98
  );
96
99
  }
97
100
 
98
- // Move Config to Infrastructure/Config
99
- // Note: Check if config path exists before moving, though copy above should have put it there
100
101
  if (await fs.pathExists(path.join(targetDir, `src/config/kafka.${serviceExt}`))) {
101
102
  await fs.move(
102
103
  path.join(targetDir, `src/config/kafka.${serviceExt}`),
@@ -105,18 +106,68 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
105
106
  );
106
107
  }
107
108
 
108
- // Cleanup old services folder
109
109
  await fs.remove(path.join(targetDir, 'src/services'));
110
+
111
+ // Messaging Infrastructure Enhancement
112
+ const messagingDir = path.join(targetDir, 'src/interfaces/messaging');
113
+ await fs.ensureDir(path.join(messagingDir, 'consumers/instances'));
114
+ await fs.ensureDir(path.join(messagingDir, 'schemas'));
115
+
116
+ const loggerPath = language === 'TypeScript' ? '@/infrastructure/log/logger' : '../../infrastructure/log/logger';
117
+ const messagingTemplates = [
118
+ { src: 'baseConsumer', dest: 'interfaces/messaging/baseConsumer' },
119
+ { src: 'userEventSchema', dest: 'interfaces/messaging/schemas/userEventSchema' },
120
+ { src: 'welcomeEmailConsumer', dest: 'interfaces/messaging/consumers/instances/welcomeEmailConsumer' }
121
+ ];
122
+
123
+ for (const t of messagingTemplates) {
124
+ const templateSource = path.join(templatesDir, 'common', 'kafka', langExt, 'messaging', `${t.src}.${langExt}.ejs`);
125
+ if (await fs.pathExists(templateSource)) {
126
+ const content = ejs.render(await fs.readFile(templateSource, 'utf-8'), { ...config, loggerPath });
127
+ await fs.writeFile(path.join(targetDir, 'src', `${t.dest}.${langExt}`), content);
128
+ }
129
+
130
+ // Render Specs for messaging components
131
+ const specTemplateSource = path.join(templatesDir, 'common', 'kafka', langExt, 'messaging', `${t.src}.spec.${langExt}.ejs`);
132
+ if (await fs.pathExists(specTemplateSource)) {
133
+ const specContent = ejs.render(await fs.readFile(specTemplateSource, 'utf-8'), { ...config, loggerPath });
134
+ const specDest = path.join(targetDir, 'tests', `${t.dest}.spec.${langExt}`);
135
+ await fs.ensureDir(path.dirname(specDest));
136
+ await fs.writeFile(specDest, specContent);
137
+ }
138
+ }
110
139
 
111
- // Remove REST-specific folders (Interfaces) - Note: routes is kept for health endpoint
112
- await fs.remove(path.join(targetDir, 'src/interfaces/controllers'));
113
- await fs.remove(path.join(targetDir, 'tests/interfaces/controllers'));
114
-
115
- // Original logic removed src/config entirely, but now we use it for Zod env validation in TS.
116
- // We will no longer delete it.
117
140
  } else if (architecture === 'MVC') {
118
141
  const serviceExt = language === 'TypeScript' ? 'ts' : 'js';
119
- // Move Spec to Tests/Services
142
+
143
+ const messagingDir = path.join(targetDir, 'src/messaging');
144
+ await fs.ensureDir(path.join(messagingDir, 'consumers/instances'));
145
+ await fs.ensureDir(path.join(messagingDir, 'schemas'));
146
+
147
+ const loggerPath = language === 'TypeScript' ? '@/utils/logger' : '../utils/logger';
148
+ const messagingTemplates = [
149
+ { src: 'baseConsumer', dest: 'messaging/baseConsumer' },
150
+ { src: 'userEventSchema', dest: 'messaging/schemas/userEventSchema' },
151
+ { src: 'welcomeEmailConsumer', dest: 'messaging/consumers/instances/welcomeEmailConsumer' }
152
+ ];
153
+
154
+ for (const t of messagingTemplates) {
155
+ const templateSource = path.join(templatesDir, 'common', 'kafka', langExt, 'messaging', `${t.src}.${langExt}.ejs`);
156
+ if (await fs.pathExists(templateSource)) {
157
+ const content = ejs.render(await fs.readFile(templateSource, 'utf-8'), { ...config, loggerPath });
158
+ await fs.writeFile(path.join(targetDir, 'src', `${t.dest}.${langExt}`), content);
159
+ }
160
+
161
+ // Render Specs for messaging components
162
+ const specTemplateSource = path.join(templatesDir, 'common', 'kafka', langExt, 'messaging', `${t.src}.spec.${langExt}.ejs`);
163
+ if (await fs.pathExists(specTemplateSource)) {
164
+ const specContent = ejs.render(await fs.readFile(specTemplateSource, 'utf-8'), { ...config, loggerPath });
165
+ const specDest = path.join(targetDir, 'tests', `${t.dest}.spec.${langExt}`);
166
+ await fs.ensureDir(path.dirname(specDest));
167
+ await fs.writeFile(specDest, specContent);
168
+ }
169
+ }
170
+
120
171
  if (await fs.pathExists(path.join(targetDir, `src/services/kafkaService.spec.${serviceExt}`))) {
121
172
  await fs.ensureDir(path.join(targetDir, 'tests/services'));
122
173
  await fs.move(
@@ -126,11 +177,6 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
126
177
  );
127
178
  }
128
179
 
129
- if (!config.viewEngine || config.viewEngine === 'None') {
130
- // MVC Cleanup for Kafka Worker (No views) - Note: routes is kept for health endpoint
131
- await fs.remove(path.join(targetDir, 'src/controllers'));
132
- await fs.remove(path.join(targetDir, 'tests/controllers'));
133
- }
134
180
  }
135
181
  };
136
182
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodejs-quickstart-structure",
3
- "version": "1.15.1",
3
+ "version": "1.16.0",
4
4
  "type": "module",
5
5
  "description": "The ultimate nodejs quickstart structure CLI to scaffold Node.js microservices with MVC or Clean Architecture",
6
6
  "main": "bin/index.js",
@@ -1,7 +1,7 @@
1
1
  const startServer = require('./infrastructure/webserver/server');
2
2
  const logger = require('./infrastructure/log/logger');
3
3
  <% if (communication === 'Kafka') { -%>
4
- const { connectKafka, sendMessage } = require('./infrastructure/messaging/kafkaClient');
4
+ const { connectKafka } = require('./infrastructure/messaging/kafkaClient');
5
5
  <% } -%>
6
6
 
7
7
  <%_ if (database !== 'None') { -%>
@@ -24,7 +24,6 @@ const syncDatabase = async () => {
24
24
  // Connect Kafka
25
25
  connectKafka().then(async () => {
26
26
  logger.info('Kafka connected');
27
- await sendMessage('test-topic', 'Hello Kafka from Clean Arch JS!');
28
27
  }).catch(err => {
29
28
  logger.error('Failed to connect to Kafka:', err);
30
29
  });
@@ -45,7 +44,6 @@ startServer();
45
44
  // Connect Kafka
46
45
  connectKafka().then(async () => {
47
46
  logger.info('Kafka connected');
48
- await sendMessage('test-topic', 'Hello Kafka from Clean Arch JS!');
49
47
  }).catch(err => {
50
48
  logger.error('Failed to connect to Kafka:', err);
51
49
  });
@@ -4,7 +4,7 @@ const logger = require('../log/logger');
4
4
  const morgan = require('morgan');
5
5
  const { errorMiddleware } = require('./middleware/errorMiddleware');
6
6
  const healthRoutes = require('../../interfaces/routes/healthRoute');
7
- <%_ if (communication === 'REST APIs') { -%>
7
+ <%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
8
8
  const apiRoutes = require('../../interfaces/routes/api');
9
9
  const swaggerUi = require('swagger-ui-express');
10
10
  const swaggerSpecs = require('./swagger');
@@ -29,10 +29,10 @@ const startServer = async () => {
29
29
  app.use(cors());
30
30
  app.use(express.json());
31
31
  app.use(morgan('combined', { stream: { write: message => logger.info(message.trim()) } }));
32
- <%_ if (communication === 'REST APIs') { -%>
32
+ <%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
33
33
  app.use('/api', apiRoutes);
34
34
  <%_ } -%>
35
- <%_ if (communication === 'REST APIs') { -%>
35
+ <%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
36
36
  app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
37
37
  <%_ } -%>
38
38
  <%_ if (communication === 'GraphQL') { -%>
@@ -71,13 +71,14 @@ const startServer = async () => {
71
71
  const server = app.listen(port, () => {
72
72
  logger.info(`Server running on port ${port}`);
73
73
  <%_ if (communication === 'Kafka') { -%>
74
- const { connectKafka, sendMessage } = require('../../infrastructure/messaging/kafkaClient');
75
- connectKafka().then(() => {
76
- logger.info('Kafka connected');
77
- sendMessage('test-topic', 'Hello Kafka from Clean Arch JS!');
78
- }).catch(err => {
79
- logger.error('Failed to connect to Kafka:', err);
80
- });
74
+ const { connectKafka } = require('../../infrastructure/messaging/kafkaClient');
75
+ connectKafka()
76
+ .then(async () => {
77
+ logger.info('Kafka connected');
78
+ })
79
+ .catch(err => {
80
+ logger.error('Failed to connect to Kafka after retries:', err.message);
81
+ });
81
82
  <%_ } -%>
82
83
  });
83
84
 
@@ -26,7 +26,15 @@ class UserController {
26
26
  async createUser(data) {
27
27
  const { name, email } = data;
28
28
  try {
29
- return await this.createUserUseCase.execute(name, email);
29
+ const user = await this.createUserUseCase.execute(name, email);
30
+ <%_ if (communication === 'Kafka') { -%>
31
+ const { sendMessage } = require('../../infrastructure/messaging/kafkaClient');
32
+ await sendMessage('user-topic', JSON.stringify({
33
+ action: 'USER_CREATED',
34
+ payload: { id: user.id || user._id, email: user.email }
35
+ }));
36
+ <%_ } -%>
37
+ return user;
30
38
  } catch (error) {
31
39
  logger.error('Error creating user:', error);
32
40
  throw error;
@@ -47,6 +55,13 @@ class UserController {
47
55
  const { name, email } = req.body;
48
56
  try {
49
57
  const user = await this.createUserUseCase.execute(name, email);
58
+ <%_ if (communication === 'Kafka') { -%>
59
+ const { sendMessage } = require('../../infrastructure/messaging/kafkaClient');
60
+ await sendMessage('user-topic', JSON.stringify({
61
+ action: 'USER_CREATED',
62
+ payload: { id: user.id || user._id, email: user.email }
63
+ }));
64
+ <%_ } -%>
50
65
  res.status(HTTP_STATUS.CREATED).json(user);
51
66
  } catch (error) {
52
67
  logger.error('Error creating user:', error);
@@ -4,6 +4,12 @@ const GetAllUsers = require('@/usecases/GetAllUsers');
4
4
 
5
5
  jest.mock('@/usecases/CreateUser');
6
6
  jest.mock('@/usecases/GetAllUsers');
7
+ <%_ if (communication === 'Kafka') { -%>
8
+ jest.mock('@/infrastructure/messaging/kafkaClient', () => ({
9
+ sendMessage: jest.fn().mockResolvedValue(undefined)
10
+ }));
11
+ <%_ } -%>
12
+
7
13
 
8
14
  describe('UserController (Clean Architecture)', () => {
9
15
  let userController;
@@ -81,7 +87,12 @@ describe('UserController (Clean Architecture)', () => {
81
87
  expect(result).toEqual(expectedUser);
82
88
  <%_ } else { -%>
83
89
  await userController.createUser(mockRequest, mockResponse, mockNext);
90
+ <%_ if (communication === 'Kafka') { -%>
91
+ const { sendMessage } = require('@/infrastructure/messaging/kafkaClient');
92
+ expect(sendMessage).toHaveBeenCalled();
93
+ <%_ } -%>
84
94
  expect(mockResponse.status).toHaveBeenCalledWith(201);
95
+
85
96
  expect(mockResponse.json).toHaveBeenCalledWith(expectedUser);
86
97
  <%_ } -%>
87
98
  expect(mockCreateUserUseCase.execute).toHaveBeenCalledWith(payload.name, payload.email);
@@ -98,5 +109,30 @@ describe('UserController (Clean Architecture)', () => {
98
109
  expect(mockNext).toHaveBeenCalledWith(error);
99
110
  <%_ } -%>
100
111
  });
112
+
113
+ <%_ if (communication === 'Kafka') { -%>
114
+ it('should successfully create a new user with _id for Kafka (Happy Path)', async () => {
115
+ const payload = { name: 'Bob', email: 'bob@example.com' };
116
+ <% if (communication === 'GraphQL') { -%>
117
+ const dataArg = payload;
118
+ <% } else { -%>
119
+ mockRequest.body = payload;
120
+ <% } -%>
121
+ const expectedUser = { _id: '2', ...payload };
122
+
123
+ mockCreateUserUseCase.execute.mockResolvedValue(expectedUser);
124
+
125
+ <% if (communication === 'GraphQL') { -%>
126
+ await userController.createUser(dataArg);
127
+ <% } else { -%>
128
+ await userController.createUser(mockRequest, mockResponse, mockNext);
129
+ <% } -%>
130
+ const { sendMessage } = require('@/infrastructure/messaging/kafkaClient');
131
+ expect(sendMessage).toHaveBeenCalledWith(
132
+ 'user-topic',
133
+ expect.stringContaining('"id":"2"')
134
+ );
135
+ });
136
+ <%_ } -%>
101
137
  });
102
138
  });
@@ -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'));
@@ -8,11 +8,11 @@ import morgan from 'morgan';
8
8
  import { errorMiddleware } from '@/utils/errorMiddleware';
9
9
  import { setupGracefulShutdown } from '@/utils/gracefulShutdown';
10
10
  import healthRoutes from '@/interfaces/routes/healthRoute';
11
- <% if (communication === 'REST APIs') { -%>
11
+ <% if (communication === 'REST APIs' || communication === 'Kafka') { -%>
12
12
  import userRoutes from '@/interfaces/routes/userRoutes';
13
13
  import swaggerUi from 'swagger-ui-express';
14
- import swaggerSpecs from '@/config/swagger';<% } -%>
15
- <%_ if (communication === 'Kafka') { -%>import { KafkaService } from '@/infrastructure/messaging/kafkaClient';<%_ } -%>
14
+ import swaggerSpecs from '@/config/swagger';<% } %>
15
+ <%_ if (communication === 'Kafka') { -%>import { kafkaService } from '@/infrastructure/messaging/kafkaClient';<%_ } -%>
16
16
  <%_ if (communication === 'GraphQL') { -%>
17
17
  import { ApolloServer } from '@apollo/server';
18
18
  import { expressMiddleware } from '@apollo/server/express4';
@@ -52,10 +52,10 @@ app.use(limiter);
52
52
  app.use(express.json());
53
53
  app.use(morgan('combined', { stream: { write: (message) => logger.info(message.trim()) } }));
54
54
 
55
- <%_ if (communication === 'REST APIs') { -%>
55
+ <%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
56
56
  app.use('/api/users', userRoutes);
57
57
  <%_ } -%>
58
- <%_ if (communication === 'REST APIs') { -%>
58
+ <%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
59
59
  app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
60
60
  <%_ } -%>
61
61
  app.use('/health', healthRoutes);
@@ -92,18 +92,16 @@ const startServer = async () => {
92
92
  app.use('/graphql', expressMiddleware(apolloServer, { context: gqlContext }));
93
93
  <%_ } -%>
94
94
  app.use(errorMiddleware);
95
- <%_ if (communication === 'Kafka') { -%>
96
- const kafkaService = new KafkaService();
97
- <%_ } -%>
98
95
  const server = app.listen(port, () => {
99
96
  logger.info(`Server running on port ${port}`);
100
97
  <%_ if (communication === 'Kafka') { -%>
101
- kafkaService.connect().then(() => {
102
- logger.info('Kafka connected');
103
- kafkaService.sendMessage('test-topic', 'Hello Kafka from Clean Arch TS!');
104
- }).catch(err => {
105
- logger.error('Failed to connect to Kafka:', err);
106
- });
98
+ kafkaService.connect()
99
+ .then(async () => {
100
+ logger.info('Kafka connected');
101
+ })
102
+ .catch(err => {
103
+ logger.error('Failed to connect to Kafka after retries:', (err as Error).message);
104
+ });
107
105
  <%_ } -%>
108
106
  });
109
107
 
@@ -56,7 +56,6 @@ describe('Logger', () => {
56
56
  const winston = require('winston');
57
57
  jest.resetModules();
58
58
  process.env.NODE_ENV = 'production';
59
- // eslint-disable-next-line @typescript-eslint/no-require-imports
60
59
  require('<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>');
61
60
  expect(winston.format.json).toHaveBeenCalled();
62
61
  process.env.NODE_ENV = 'test';
@@ -11,6 +11,20 @@ jest.mock('@/infrastructure/repositories/UserRepository');
11
11
  jest.mock('@/usecases/createUser');
12
12
  jest.mock('@/usecases/getAllUsers');
13
13
  jest.mock('@/infrastructure/log/logger');
14
+ <%_ if (communication === 'Kafka') { -%>
15
+ jest.mock('@/infrastructure/messaging/kafkaClient', () => {
16
+ const mockSendMessage = jest.fn().mockResolvedValue(undefined);
17
+ return {
18
+ kafkaService: {
19
+ sendMessage: mockSendMessage
20
+ },
21
+ KafkaService: jest.fn().mockImplementation(() => ({
22
+ sendMessage: mockSendMessage
23
+ }))
24
+ };
25
+ });
26
+ <%_ } -%>
27
+
14
28
 
15
29
  describe('UserController (Clean Architecture)', () => {
16
30
  let userController: UserController;
@@ -115,7 +129,12 @@ describe('UserController (Clean Architecture)', () => {
115
129
  await userController.createUser(mockRequest as Request, mockResponse as Response, mockNext);
116
130
 
117
131
  // Assert
132
+ <%_ if (communication === 'Kafka') { -%>
133
+ const { kafkaService } = require('@/infrastructure/messaging/kafkaClient');
134
+ expect(kafkaService.sendMessage).toHaveBeenCalled();
135
+ <%_ } -%>
118
136
  expect(mockResponse.status).toHaveBeenCalledWith(HTTP_STATUS.CREATED);
137
+
119
138
  expect(mockResponse.json).toHaveBeenCalledWith(expectedUser);
120
139
  <% } -%>
121
140
  expect(mockCreateUserUseCase.execute).toHaveBeenCalledWith(payload.name, payload.email);
@@ -22,6 +22,14 @@ export class UserController {
22
22
  try {
23
23
  const { name, email } = data;
24
24
  const user = await this.createUserUseCase.execute(name, email);
25
+ <%_ if (communication === 'Kafka') { -%>
26
+ const { kafkaService } = await import('@/infrastructure/messaging/kafkaClient');
27
+ await kafkaService.sendMessage('user-topic', JSON.stringify({
28
+ action: 'USER_CREATED',
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ payload: { id: (user as any).id || (user as any)._id, email: user.email }
31
+ }));
32
+ <%_ } -%>
25
33
  return user;
26
34
  } catch (error: unknown) {
27
35
  const message = error instanceof Error ? error.message : 'Unknown error';
@@ -45,6 +53,14 @@ export class UserController {
45
53
  try {
46
54
  const { name, email } = req.body;
47
55
  const user = await this.createUserUseCase.execute(name, email);
56
+ <%_ if (communication === 'Kafka') { -%>
57
+ const { kafkaService } = await import('@/infrastructure/messaging/kafkaClient');
58
+ await kafkaService.sendMessage('user-topic', JSON.stringify({
59
+ action: 'USER_CREATED',
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ payload: { id: (user as any).id || (user as any)._id, email: user.email }
62
+ }));
63
+ <%_ } -%>
48
64
  res.status(HTTP_STATUS.CREATED).json(user);
49
65
  } catch (error: unknown) {
50
66
  const message = error instanceof Error ? error.message : 'Unknown error';
@@ -4,6 +4,7 @@
4
4
  FROM node:22-alpine AS builder
5
5
 
6
6
  WORKDIR /app
7
+ ENV NPM_CONFIG_UPDATE_NOTIFIER=false
7
8
 
8
9
  COPY package*.json ./
9
10
  COPY tsconfig*.json ./
@@ -24,6 +25,7 @@ FROM node:22-alpine AS production
24
25
  WORKDIR /app
25
26
 
26
27
  ENV NODE_ENV=production
28
+ ENV NPM_CONFIG_UPDATE_NOTIFIER=false
27
29
 
28
30
  COPY package*.json ./
29
31
 
@@ -115,6 +115,29 @@ mutation CreateUser {
115
115
  API is exposed via **REST**.
116
116
  A Swagger UI for API documentation is available at:
117
117
  - **URL**: `http://localhost:3000/api-docs` (Dynamic based on PORT)
118
+ <%_ } -%>
119
+
120
+ <% if (communication === 'Kafka') { -%>
121
+ ## 📡 Testing Kafka Asynchronous Flow
122
+ This project demonstrates a production-ready Kafka flow:
123
+ 1. **Producer**: When a user is created via the API, a `USER_CREATED` event is sent to `user-topic`.
124
+ 2. **Consumer**: `WelcomeEmailConsumer` listens to `user-topic` and simulates sending an email.
125
+
126
+ ### How to verify:
127
+ 1. Ensure infrastructure is running: `docker-compose up -d<% if (database !== 'None') { %> db<% } %><% if (caching === 'Redis') { %> redis<% } %><% if (communication === 'Kafka') { %> zookeeper kafka<% } %>`
128
+ 2. Start the app: `npm run dev`
129
+ 3. Trigger an event by creating a user (via Postman or curl):
130
+ ```bash
131
+ curl -X POST http://localhost:3000/api/users \
132
+ -H "Content-Type: application/json" \
133
+ -d '{"name": "Kafka Tester", "email": "kafka@example.com"}'
134
+ ```
135
+ 4. Observe the logs:
136
+ ```text
137
+ [Kafka] Producer: Sent USER_CREATED event for 'kafka@example.com'
138
+ [Kafka] Consumer: Received USER_CREATED.
139
+ [Kafka] Consumer: 📧 Sending welcome email to 'kafka@example.com'... Done!
140
+ ```
118
141
  <% } -%>
119
142
 
120
143
  <% if (caching === 'Redis') { -%>
@@ -227,4 +250,4 @@ This project is "AI-Ready" out of the box. We have pre-configured industry-leadi
227
250
  - **Magic Defaults**: We've automatically tailored your AI context to focus on **<%= projectName %>** and its specific architectural stack (<%= architecture %>, <%= database %>, etc.).
228
251
  - **Use Cursor?** We've configured **`.cursorrules`** at the root. It enforces project standards (70% coverage, MVC/Clean) directly within the editor.
229
252
  - *Pro-tip*: You can customize the `Project Goal` placeholder in `.cursorrules` to help the AI understand your specific business logic!
230
- - **Use ChatGPT/Gemini/Claude?** Check the **`prompts/`** directory. It contains highly-specialized Agent Skill templates. You can copy-paste these into any LLM to give it a "Senior Developer" understanding of your codebase immediately.
253
+ - **Use ChatGPT/Gemini/Claude?** Check the **`prompts/`** directory. It contains highly-specialized Agent Skill templates. You can copy-paste these into any LLM to give it a "Senior Developer" understanding of your codebase immediately.
@@ -11,7 +11,8 @@ class UserModel {
11
11
  }
12
12
 
13
13
  static async create(data) {
14
- const newUser = { id: String(this.mockData.length + 1), ...data };
14
+ const { id, ...rest } = data;
15
+ const newUser = { id: String(this.mockData.length + 1), ...rest };
15
16
  this.mockData.push(newUser);
16
17
  return newUser;
17
18
  }
@@ -16,10 +16,11 @@ export default class UserModel {
16
16
  return this.mockData;
17
17
  }
18
18
 
19
- static async create(data: Omit<User, 'id'>) {
20
- const newUser = { id: String(this.mockData.length + 1), ...data };
19
+ static async create(data: Omit<User, 'id'> & { id?: string | number | null }) {
20
+ const { id, ...rest } = data;
21
+ const newUser: User = { id: String(this.mockData.length + 1), ...rest };
21
22
  this.mockData.push(newUser);
22
- return newUser as User;
23
+ return newUser;
23
24
  }
24
25
  }
25
26
  <% } else { -%>