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.
- package/CHANGELOG.md +23 -0
- package/README.md +14 -5
- package/lib/modules/app-setup.js +3 -3
- package/lib/modules/config-files.js +2 -2
- package/lib/modules/kafka-setup.js +70 -24
- package/package.json +1 -1
- package/templates/clean-architecture/js/src/index.js.ejs +9 -6
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +12 -11
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +17 -1
- package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +36 -0
- package/templates/clean-architecture/ts/src/config/swagger.ts.ejs +1 -1
- package/templates/clean-architecture/ts/src/index.ts.ejs +16 -16
- package/templates/clean-architecture/ts/src/infrastructure/log/logger.spec.ts.ejs +0 -1
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +19 -0
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +17 -0
- package/templates/common/Dockerfile +2 -0
- package/templates/common/README.md.ejs +24 -1
- package/templates/common/database/js/models/User.js.ejs +2 -1
- package/templates/common/database/ts/models/User.ts.ejs +4 -3
- package/templates/common/eslint.config.mjs.ejs +30 -3
- package/templates/common/health/js/healthRoute.js.ejs +5 -2
- package/templates/common/health/ts/healthRoute.ts.ejs +5 -2
- package/templates/common/jest.config.js.ejs +4 -1
- package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +30 -0
- package/templates/common/kafka/js/messaging/baseConsumer.spec.js.ejs +58 -0
- package/templates/common/kafka/js/messaging/userEventSchema.js.ejs +11 -0
- package/templates/common/kafka/js/messaging/userEventSchema.spec.js.ejs +27 -0
- package/templates/common/kafka/js/messaging/welcomeEmailConsumer.js.ejs +31 -0
- package/templates/common/kafka/js/messaging/welcomeEmailConsumer.spec.js.ejs +49 -0
- package/templates/common/kafka/js/services/kafkaService.js.ejs +77 -23
- package/templates/common/kafka/js/services/kafkaService.spec.js.ejs +53 -7
- package/templates/common/kafka/ts/messaging/baseConsumer.spec.ts.ejs +50 -0
- package/templates/common/kafka/ts/messaging/baseConsumer.ts.ejs +27 -0
- package/templates/common/kafka/ts/messaging/userEventSchema.spec.ts.ejs +51 -0
- package/templates/common/kafka/ts/messaging/userEventSchema.ts.ejs +11 -0
- package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.spec.ts.ejs +49 -0
- package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.ts.ejs +25 -0
- package/templates/common/kafka/ts/services/kafkaService.spec.ts.ejs +22 -2
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +65 -12
- package/templates/common/package.json.ejs +6 -4
- package/templates/common/shutdown/ts/gracefulShutdown.ts.ejs +8 -11
- package/templates/mvc/js/src/controllers/userController.js.ejs +15 -0
- package/templates/mvc/js/src/controllers/userController.spec.js.ejs +39 -0
- package/templates/mvc/js/src/index.js.ejs +20 -15
- package/templates/mvc/ts/src/config/swagger.ts.ejs +1 -1
- package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +18 -0
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +16 -0
- package/templates/mvc/ts/src/index.ts.ejs +16 -18
- package/templates/mvc/ts/src/utils/logger.spec.ts.ejs +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ 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.1] - 2026-03-17
|
|
9
|
+
|
|
10
|
+
### Refactored
|
|
11
|
+
- **Top-Level Dynamic Imports**: Moved dynamic `await import()` and `require()` calls to the top-level across all TypeScript and JavaScript templates (MVC & Clean Architecture).
|
|
12
|
+
- **Module Loading Standardization**: Standardized database connections (Mongoose/Sequelize), Kafka services, and graceful shutdown logic for better static analysis and ESM/CJS consistency.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Kafka Service Scope**: Resolved a potential `ReferenceError` in `kafkaService.ts.ejs` by correcting the instantiation order of dynamic consumers.
|
|
16
|
+
- **Health Check Imports**: Optimized `healthRoute.ts.ejs` to use top-level database driver imports.
|
|
17
|
+
|
|
18
|
+
## [1.16.0] - 2026-03-14
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Robust Kafka Singleton Implementation**: Refactored `KafkaService` to a strict singleton pattern across all architectures with `connectionPromise` and automated retry logic for resilient messaging.
|
|
22
|
+
- **BaseConsumer Standards**: Implemented constructor guards in `BaseConsumer` to prevent direct instantiation of abstract messaging classes.
|
|
23
|
+
- **Professional Docker Log Hygiene**: Added `NPM_CONFIG_UPDATE_NOTIFIER=false` to both builder and production stages to suppress non-essential npm upgrade notifications.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- **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.
|
|
27
|
+
- **Network Resilience**: Removed redundant `npm install -g npm@latest` from the `Dockerfile` template to fix `ECONNRESET` failures during project verification on unstable networks.
|
|
28
|
+
- **Controller Testing Modernization**: Refactored `userController` spec templates (TS and JS) to correctly mock and verify the shared Kafka singleton, fixing persistent unit test failures.
|
|
29
|
+
- **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.
|
|
30
|
+
|
|
8
31
|
## [1.15.1] - 2026-03-12
|
|
9
32
|
|
|
10
33
|
### Added
|
package/README.md
CHANGED
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
A powerful CLI tool to scaffold production-ready Node.js microservices with built-in best practices, allowing you to choose between **MVC** or **Clean Architecture**, **JavaScript** or **TypeScript**, and your preferred database.
|
|
9
9
|
|
|
10
|
-
[](https://medium.com/@paudang/nodejs-quickstart-generator-93c276d60e0b)
|
|
11
|
-
|
|
12
10
|

|
|
13
11
|
|
|
14
12
|
## Features
|
|
@@ -24,6 +22,12 @@ A powerful CLI tool to scaffold production-ready Node.js microservices with buil
|
|
|
24
22
|
- **Database Migrations/Schemas**: Integrated **Flyway** for SQL migrations or **Mongoose** schemas for MongoDB.
|
|
25
23
|
- **Professional Standards**: Generated projects come with highly professional, industry-standard tooling.
|
|
26
24
|
|
|
25
|
+
## 🌟 Why developers love this?
|
|
26
|
+
|
|
27
|
+
- **Community Trusted**: Over **3,000+** projects bootstrapped within the first month.
|
|
28
|
+
- **Actively Maintained**: Constant updates and optimizations (30+ versions released) based on real-world developer feedback.
|
|
29
|
+
- **Resilience First**: Unlike basic boilerplates, our generated code focuses on infrastructure stability (Retry logic, Health checks, and Graceful shutdowns).
|
|
30
|
+
|
|
27
31
|
## 🏆 Professional Standards (New)
|
|
28
32
|
|
|
29
33
|
We don't just generate boilerplate; we generate **production-ready** foundations. Every project includes:
|
|
@@ -34,7 +38,7 @@ We don't just generate boilerplate; we generate **production-ready** foundations
|
|
|
34
38
|
- **🧪 Testing Excellence**: Integrated `Jest` and `Supertest`. Every generated project maintains **>70% Unit Test coverage** for controllers, services, and resolvers out of the box.
|
|
35
39
|
- **🔄 CI/CD Integration**: Pre-configured workflows for **GitHub Actions**, **Jenkins**, and **GitLab CI**.
|
|
36
40
|
- **⚓ Git Hooks**: `Husky` and `Lint-Staged` to ensure no bad code is ever committed.
|
|
37
|
-
- **🤝 Reliability**:
|
|
41
|
+
- **🤝 Reliability**: Health Checks (`/health`) with deep database pings, Infrastructure Retry Logic (handling Docker startup delays), and Graceful Shutdown workflows.
|
|
38
42
|
- **🐳 DevOps**: Highly optimized **Multi-Stage Dockerfile** for small, secure production images.
|
|
39
43
|
- **🚀 Deployment**: Ship confidently with an integrated **PM2 Ecosystem Configuration** for zero-downtime reloads and robust process management.
|
|
40
44
|
|
|
@@ -66,14 +70,13 @@ Once installed, simply run the following command in any directory where you want
|
|
|
66
70
|
```bash
|
|
67
71
|
nodejs-quickstart init
|
|
68
72
|
```
|
|
69
|
-
or
|
|
70
73
|
|
|
71
74
|
## Quick Start (Recommended)
|
|
72
75
|
|
|
73
76
|
You can run the generator directly without installing it globally:
|
|
74
77
|
|
|
75
78
|
```bash
|
|
76
|
-
npx nodejs-quickstart-structure init
|
|
79
|
+
npx nodejs-quickstart-structure@latest init
|
|
77
80
|
```
|
|
78
81
|
|
|
79
82
|
### Configuration Options
|
|
@@ -111,6 +114,12 @@ npm install
|
|
|
111
114
|
docker-compose up
|
|
112
115
|
```
|
|
113
116
|
|
|
117
|
+
## ❤️ Support the Project
|
|
118
|
+
|
|
119
|
+
We just hit **3,000 downloads**! If this tool helped you save hours of setup time, please consider:
|
|
120
|
+
- Giving us a ⭐ on [GitHub](https://github.com/paudang/nodejs-quickstart-structure) to help others find it.
|
|
121
|
+
- Following the [Medium Article](https://medium.com/@paudang/nodejs-quickstart-generator-93c276d60e0b) for deep-dive tutorials.
|
|
122
|
+
|
|
114
123
|
## License
|
|
115
124
|
|
|
116
125
|
ISC
|
package/lib/modules/app-setup.js
CHANGED
|
@@ -165,8 +165,8 @@ export const renderDynamicComponents = async (templatePath, targetDir, config) =
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
// Cleanup REST routes if
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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,20 +1,25 @@
|
|
|
1
1
|
const startServer = require('./infrastructure/webserver/server');
|
|
2
2
|
const logger = require('./infrastructure/log/logger');
|
|
3
3
|
<% if (communication === 'Kafka') { -%>
|
|
4
|
-
const { connectKafka
|
|
4
|
+
const { connectKafka } = require('./infrastructure/messaging/kafkaClient');
|
|
5
5
|
<% } -%>
|
|
6
|
-
|
|
7
6
|
<%_ if (database !== 'None') { -%>
|
|
8
7
|
// Database Sync
|
|
8
|
+
<%_ if (database !== 'None') { -%>
|
|
9
|
+
<%_ if (database === 'MongoDB') { -%>
|
|
10
|
+
const connectDB = require('./infrastructure/database/database');
|
|
11
|
+
<%_ } else { -%>
|
|
12
|
+
const sequelize = require('./infrastructure/database/database');
|
|
13
|
+
<%_ } -%>
|
|
14
|
+
<%_ } -%>
|
|
15
|
+
|
|
9
16
|
const syncDatabase = async () => {
|
|
10
17
|
let retries = 30;
|
|
11
18
|
while (retries) {
|
|
12
19
|
try {
|
|
13
20
|
<%_ if (database === 'MongoDB') { -%>
|
|
14
|
-
const connectDB = require('./infrastructure/database/database');
|
|
15
21
|
await connectDB();
|
|
16
22
|
<%_ } else { -%>
|
|
17
|
-
const sequelize = require('./infrastructure/database/database');
|
|
18
23
|
await sequelize.sync();
|
|
19
24
|
<%_ } -%>
|
|
20
25
|
logger.info('Database synced');
|
|
@@ -24,7 +29,6 @@ const syncDatabase = async () => {
|
|
|
24
29
|
// Connect Kafka
|
|
25
30
|
connectKafka().then(async () => {
|
|
26
31
|
logger.info('Kafka connected');
|
|
27
|
-
await sendMessage('test-topic', 'Hello Kafka from Clean Arch JS!');
|
|
28
32
|
}).catch(err => {
|
|
29
33
|
logger.error('Failed to connect to Kafka:', err);
|
|
30
34
|
});
|
|
@@ -45,7 +49,6 @@ startServer();
|
|
|
45
49
|
// Connect Kafka
|
|
46
50
|
connectKafka().then(async () => {
|
|
47
51
|
logger.info('Kafka connected');
|
|
48
|
-
await sendMessage('test-topic', 'Hello Kafka from Clean Arch JS!');
|
|
49
52
|
}).catch(err => {
|
|
50
53
|
logger.error('Failed to connect to Kafka:', err);
|
|
51
54
|
});
|
|
@@ -4,7 +4,8 @@ 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
|
-
|
|
7
|
+
const setupGracefulShutdown = require('../../utils/gracefulShutdown');
|
|
8
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
8
9
|
const apiRoutes = require('../../interfaces/routes/api');
|
|
9
10
|
const swaggerUi = require('swagger-ui-express');
|
|
10
11
|
const swaggerSpecs = require('./swagger');
|
|
@@ -29,10 +30,10 @@ const startServer = async () => {
|
|
|
29
30
|
app.use(cors());
|
|
30
31
|
app.use(express.json());
|
|
31
32
|
app.use(morgan('combined', { stream: { write: message => logger.info(message.trim()) } }));
|
|
32
|
-
<%_ if (communication === 'REST APIs') { -%>
|
|
33
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
33
34
|
app.use('/api', apiRoutes);
|
|
34
35
|
<%_ } -%>
|
|
35
|
-
<%_ if (communication === 'REST APIs') { -%>
|
|
36
|
+
<%_ if (communication === 'REST APIs' || communication === 'Kafka') { -%>
|
|
36
37
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
|
|
37
38
|
<%_ } -%>
|
|
38
39
|
<%_ if (communication === 'GraphQL') { -%>
|
|
@@ -71,17 +72,17 @@ const startServer = async () => {
|
|
|
71
72
|
const server = app.listen(port, () => {
|
|
72
73
|
logger.info(`Server running on port ${port}`);
|
|
73
74
|
<%_ if (communication === 'Kafka') { -%>
|
|
74
|
-
const { connectKafka
|
|
75
|
-
connectKafka()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
const { connectKafka } = require('../../infrastructure/messaging/kafkaClient');
|
|
76
|
+
connectKafka()
|
|
77
|
+
.then(async () => {
|
|
78
|
+
logger.info('Kafka connected');
|
|
79
|
+
})
|
|
80
|
+
.catch(err => {
|
|
81
|
+
logger.error('Failed to connect to Kafka after retries:', err.message);
|
|
82
|
+
});
|
|
81
83
|
<%_ } -%>
|
|
82
84
|
});
|
|
83
85
|
|
|
84
|
-
const setupGracefulShutdown = require('../../utils/gracefulShutdown');
|
|
85
86
|
setupGracefulShutdown(server);
|
|
86
87
|
};
|
|
87
88
|
|
|
@@ -5,6 +5,9 @@ const UserRepository = require('../../infrastructure/repositories/UserRepository
|
|
|
5
5
|
const HTTP_STATUS = require('../../utils/httpCodes');
|
|
6
6
|
<% } -%>
|
|
7
7
|
const logger = require('../../infrastructure/log/logger');
|
|
8
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
9
|
+
const { sendMessage } = require('../../infrastructure/messaging/kafkaClient');
|
|
10
|
+
<%_ } -%>
|
|
8
11
|
|
|
9
12
|
class UserController {
|
|
10
13
|
constructor() {
|
|
@@ -26,7 +29,14 @@ class UserController {
|
|
|
26
29
|
async createUser(data) {
|
|
27
30
|
const { name, email } = data;
|
|
28
31
|
try {
|
|
29
|
-
|
|
32
|
+
const user = await this.createUserUseCase.execute(name, email);
|
|
33
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
34
|
+
await sendMessage('user-topic', JSON.stringify({
|
|
35
|
+
action: 'USER_CREATED',
|
|
36
|
+
payload: { id: user.id || user._id, email: user.email }
|
|
37
|
+
}));
|
|
38
|
+
<%_ } -%>
|
|
39
|
+
return user;
|
|
30
40
|
} catch (error) {
|
|
31
41
|
logger.error('Error creating user:', error);
|
|
32
42
|
throw error;
|
|
@@ -47,6 +57,12 @@ class UserController {
|
|
|
47
57
|
const { name, email } = req.body;
|
|
48
58
|
try {
|
|
49
59
|
const user = await this.createUserUseCase.execute(name, email);
|
|
60
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
61
|
+
await sendMessage('user-topic', JSON.stringify({
|
|
62
|
+
action: 'USER_CREATED',
|
|
63
|
+
payload: { id: user.id || user._id, email: user.email }
|
|
64
|
+
}));
|
|
65
|
+
<%_ } -%>
|
|
50
66
|
res.status(HTTP_STATUS.CREATED).json(user);
|
|
51
67
|
} catch (error) {
|
|
52
68
|
logger.error('Error creating user:', error);
|
package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs
CHANGED
|
@@ -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
|
});
|
|
@@ -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 {
|
|
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()
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
|
@@ -112,15 +110,17 @@ const startServer = async () => {
|
|
|
112
110
|
|
|
113
111
|
<%_ if (database !== 'None') { -%>
|
|
114
112
|
// Database Sync
|
|
113
|
+
<%_ if (database !== 'None') { -%>
|
|
114
|
+
import <% if (database === 'MongoDB') { %>connectDB<% } else { %>sequelize<% } %> from '@/infrastructure/database/database';
|
|
115
|
+
<%_ } -%>
|
|
116
|
+
|
|
115
117
|
const syncDatabase = async () => {
|
|
116
118
|
let retries = 30;
|
|
117
119
|
while (retries) {
|
|
118
120
|
try {
|
|
119
121
|
<%_ if (database === 'MongoDB') { -%>
|
|
120
|
-
const connectDB = (await import('@/infrastructure/database/database')).default;
|
|
121
122
|
await connectDB();
|
|
122
123
|
<%_ } else { -%>
|
|
123
|
-
const sequelize = (await import('@/infrastructure/database/database')).default;
|
|
124
124
|
await sequelize.sync();
|
|
125
125
|
<%_ } -%>
|
|
126
126
|
logger.info('Database synced');
|
|
@@ -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';
|
package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs
CHANGED
|
@@ -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);
|
|
@@ -6,6 +6,9 @@ import { UserRepository } from '@/infrastructure/repositories/UserRepository';
|
|
|
6
6
|
import CreateUser from '@/usecases/createUser';
|
|
7
7
|
import GetAllUsers from '@/usecases/getAllUsers';
|
|
8
8
|
import logger from '@/infrastructure/log/logger';
|
|
9
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
10
|
+
import { kafkaService } from '@/infrastructure/messaging/kafkaClient';
|
|
11
|
+
<%_ } -%>
|
|
9
12
|
|
|
10
13
|
export class UserController {
|
|
11
14
|
private createUserUseCase: CreateUser;
|
|
@@ -22,6 +25,13 @@ export class UserController {
|
|
|
22
25
|
try {
|
|
23
26
|
const { name, email } = data;
|
|
24
27
|
const user = await this.createUserUseCase.execute(name, email);
|
|
28
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
29
|
+
await kafkaService.sendMessage('user-topic', JSON.stringify({
|
|
30
|
+
action: 'USER_CREATED',
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
payload: { id: (user as any).id || (user as any)._id, email: user.email }
|
|
33
|
+
}));
|
|
34
|
+
<%_ } -%>
|
|
25
35
|
return user;
|
|
26
36
|
} catch (error: unknown) {
|
|
27
37
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -45,6 +55,13 @@ export class UserController {
|
|
|
45
55
|
try {
|
|
46
56
|
const { name, email } = req.body;
|
|
47
57
|
const user = await this.createUserUseCase.execute(name, email);
|
|
58
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
59
|
+
await kafkaService.sendMessage('user-topic', JSON.stringify({
|
|
60
|
+
action: 'USER_CREATED',
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
payload: { id: (user as any).id || (user as any)._id, email: user.email }
|
|
63
|
+
}));
|
|
64
|
+
<%_ } -%>
|
|
48
65
|
res.status(HTTP_STATUS.CREATED).json(user);
|
|
49
66
|
} catch (error: unknown) {
|
|
50
67
|
const message = error instanceof Error ? error.message : 'Unknown error';
|