nodejs-quickstart-structure 1.14.0 → 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 (55) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/bin/index.js +84 -80
  3. package/lib/generator.js +11 -1
  4. package/lib/modules/app-setup.js +3 -3
  5. package/lib/modules/config-files.js +27 -2
  6. package/lib/modules/kafka-setup.js +70 -24
  7. package/package.json +8 -3
  8. package/templates/clean-architecture/js/src/index.js.ejs +1 -3
  9. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +11 -10
  10. package/templates/clean-architecture/js/src/interfaces/controllers/userController.js.ejs +16 -1
  11. package/templates/clean-architecture/js/src/interfaces/controllers/userController.spec.js.ejs +36 -0
  12. package/templates/clean-architecture/ts/src/config/swagger.ts.ejs +1 -1
  13. package/templates/clean-architecture/ts/src/index.ts.ejs +12 -14
  14. package/templates/clean-architecture/ts/src/infrastructure/log/logger.spec.ts.ejs +0 -1
  15. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.spec.ts.ejs +19 -0
  16. package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts.ejs +16 -0
  17. package/templates/common/.cursorrules.ejs +60 -0
  18. package/templates/common/.dockerignore +2 -0
  19. package/templates/common/.gitlab-ci.yml.ejs +5 -5
  20. package/templates/common/Dockerfile +2 -0
  21. package/templates/common/Jenkinsfile.ejs +1 -1
  22. package/templates/common/README.md.ejs +34 -1
  23. package/templates/common/_github/workflows/ci.yml +7 -4
  24. package/templates/common/database/js/models/User.js.ejs +2 -1
  25. package/templates/common/database/ts/models/User.ts.ejs +4 -3
  26. package/templates/common/eslint.config.mjs.ejs +30 -3
  27. package/templates/common/jest.config.js.ejs +4 -1
  28. package/templates/common/kafka/js/messaging/baseConsumer.js.ejs +30 -0
  29. package/templates/common/kafka/js/messaging/baseConsumer.spec.js.ejs +58 -0
  30. package/templates/common/kafka/js/messaging/userEventSchema.js.ejs +11 -0
  31. package/templates/common/kafka/js/messaging/userEventSchema.spec.js.ejs +27 -0
  32. package/templates/common/kafka/js/messaging/welcomeEmailConsumer.js.ejs +31 -0
  33. package/templates/common/kafka/js/messaging/welcomeEmailConsumer.spec.js.ejs +49 -0
  34. package/templates/common/kafka/js/services/kafkaService.js.ejs +75 -23
  35. package/templates/common/kafka/js/services/kafkaService.spec.js.ejs +53 -7
  36. package/templates/common/kafka/ts/messaging/baseConsumer.spec.ts.ejs +50 -0
  37. package/templates/common/kafka/ts/messaging/baseConsumer.ts.ejs +27 -0
  38. package/templates/common/kafka/ts/messaging/userEventSchema.spec.ts.ejs +51 -0
  39. package/templates/common/kafka/ts/messaging/userEventSchema.ts.ejs +11 -0
  40. package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.spec.ts.ejs +49 -0
  41. package/templates/common/kafka/ts/messaging/welcomeEmailConsumer.ts.ejs +25 -0
  42. package/templates/common/kafka/ts/services/kafkaService.spec.ts.ejs +22 -2
  43. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +72 -12
  44. package/templates/common/package.json.ejs +6 -4
  45. package/templates/common/prompts/add-feature.md.ejs +26 -0
  46. package/templates/common/prompts/project-context.md.ejs +43 -0
  47. package/templates/common/prompts/troubleshoot.md.ejs +28 -0
  48. package/templates/mvc/js/src/controllers/userController.js.ejs +14 -0
  49. package/templates/mvc/js/src/controllers/userController.spec.js.ejs +39 -0
  50. package/templates/mvc/js/src/index.js.ejs +12 -11
  51. package/templates/mvc/ts/src/config/swagger.ts.ejs +1 -1
  52. package/templates/mvc/ts/src/controllers/userController.spec.ts.ejs +18 -0
  53. package/templates/mvc/ts/src/controllers/userController.ts.ejs +16 -0
  54. package/templates/mvc/ts/src/index.ts.ejs +13 -16
  55. package/templates/mvc/ts/src/utils/logger.spec.ts.ejs +0 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,38 @@ 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
+
21
+ ## [1.15.1] - 2026-03-12
22
+
23
+ ### Added
24
+ - **Magic AI Scaffolding**: Automated AI context generation ( `.cursorrules`, `prompts/`) by intelligently inferring project goals from the project name, removing the need for manual business domain prompts.
25
+ - **Enhanced README**: Added specialized guidance for AI-native development with Cursor and LLMs.
26
+
27
+ ### Fixed
28
+ - **GitHub Actions Compliance**: Fully resolved Node.js 20 deprecation warnings by moving `FORCE_JAVASCRIPT_ACTIONS_TO_NODE24` to global workflow scope.
29
+ - **CI Modernization**: Upgraded daily audit and GitLab CI runners to Node.js 22.
30
+ - **CI Enforcement**: Updated all CI/CD templates (GitHub, GitLab, Jenkins) to strictly enforce the >70% test coverage gate.
31
+
32
+ ## [1.15.0] - 2026-03-12
33
+ ### Added
34
+ - **AI-Native Scaffolding & Agent Skill Templates:**
35
+ - Added a new CLI prompt for `businessDomain` to inject custom domain knowledge into generated templates.
36
+ - Generates a `.cursorrules` file at the root to enforce >70% Test coverage and Architecture patterns automatically for AI Code Editors.
37
+ - Scaffolds a `prompts/` directory with specialized Agent Skill templates (`project-context.md`, `add-feature.md`, `troubleshoot.md`) designed to provide deep structural understanding to LLMs like ChatGPT or Claude.
38
+ - Added an "AI-Native Development" section to the generated `README.md` and prominent print messages in the CLI upon successful completion.
39
+
8
40
  ## [1.14.0] - 2026-03-09
9
41
  ### Added
10
42
  - **Unit test:**
package/bin/index.js CHANGED
@@ -1,81 +1,85 @@
1
1
  #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import chalk from 'chalk';
5
- import { getProjectDetails } from '../lib/prompts.js';
6
- import { generateProject } from '../lib/generator.js';
7
- import { readFileSync } from 'fs';
8
- import { join, dirname } from 'path';
9
- import { fileURLToPath } from 'url';
10
-
11
- const __dirname = dirname(fileURLToPath(import.meta.url));
12
- const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
13
-
14
- const program = new Command();
15
-
16
- program
17
- .name('nodejs-quickstart')
18
- .description('🚀 CLI to scaffold production-ready Node.js microservices.\n\nGenerates projects with:\n- MVC or Clean Architecture\n- REST or Kafka\n- MySQL, PostgreSQL, or MongoDB\n- Docker, Flyway & Mongoose support')
19
- .version(pkg.version, '-v, --version', 'Output the current version')
20
- .addHelpText('after', `
21
- \n${chalk.yellow('Example:')}
22
- $ nodejs-quickstart init ${chalk.gray('# Start the interactive setup')}
23
- `);
24
-
25
- program
26
- .command('init')
27
- .description('Initialize a new Node.js project')
28
- .option('-n, --project-name <name>', 'Project name')
29
- .option('-l, --language <language>', 'Language (JavaScript, TypeScript)')
30
- .option('-a, --architecture <architecture>', 'Architecture (MVC, Clean Architecture)')
31
- .option('--view-engine <view>', 'View Engine (None, EJS, Pug) - MVC only')
32
- .option('-d, --database <database>', 'Database (MySQL, PostgreSQL)')
33
- .option('--db-name <name>', 'Database name')
34
- .option('-c, --communication <communication>', 'Communication (REST APIs, GraphQL, Kafka)')
35
- .option('--ci-provider <provider>', 'CI/CD Provider (None, GitHub Actions, Jenkins)')
36
- .option('--caching <type>', 'Caching Layer (None/Redis)')
37
- .action(async (options) => {
38
- // Fix for Commander camelCase conversion
39
- if (options.ciProvider) {
40
- options.ciProvider = options.ciProvider;
41
- }
42
-
43
- console.log(chalk.blue('Welcome to the Node.js Quickstart Generator!'));
44
-
45
- try {
46
- const answers = await getProjectDetails(options);
47
- console.log(chalk.green('\nConfiguration received:'));
48
- console.log(JSON.stringify(answers, null, 2));
49
-
50
- console.log(chalk.yellow('\nGenerating project...'));
51
- await generateProject(answers);
52
-
53
- console.log(chalk.green('\n✔ Project generated successfully!'));
54
-
55
- let manualStartInstructions = `\n${chalk.yellow('Development:')}\n cd ${answers.projectName}\n npm install`;
56
-
57
- const needsInfrastructure = answers.database !== 'None' || answers.caching === 'Redis' || answers.communication === 'Kafka';
58
-
59
- if (needsInfrastructure) {
60
- let servicesToStart = '';
61
- if (answers.database !== 'None') servicesToStart += ' db';
62
- if (answers.caching === 'Redis') servicesToStart += ' redis';
63
- if (answers.communication === 'Kafka') servicesToStart += ' zookeeper kafka';
64
-
65
- manualStartInstructions += `\n docker-compose up -d${servicesToStart} # Start infrastructure first\n npm run dev`;
66
- } else {
67
- manualStartInstructions += `\n npm run dev`;
68
- }
69
-
70
- console.log(chalk.cyan(`\nNext steps:\n cd ${answers.projectName}\n npm install\n docker-compose up\n-----------------------${manualStartInstructions}\n\n${chalk.yellow('Production (PM2):')}\n npm run build\n npm run deploy\n npx pm2 logs`));
71
-
72
- } catch (error) {
73
- console.error(chalk.red('Error generating project:'), error);
74
- process.exit(1);
75
- }
76
- });
77
- program.parse(process.argv);
78
-
79
- if (!process.argv.slice(2).length) {
80
- program.outputHelp();
81
- }
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import { getProjectDetails } from '../lib/prompts.js';
6
+ import { generateProject } from '../lib/generator.js';
7
+ import { readFileSync } from 'fs';
8
+ import { join, dirname } from 'path';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
13
+
14
+ const program = new Command();
15
+
16
+ program
17
+ .name('nodejs-quickstart')
18
+ .description('🚀 CLI to scaffold production-ready Node.js microservices.\n\nGenerates projects with:\n- MVC or Clean Architecture\n- REST or Kafka\n- MySQL, PostgreSQL, or MongoDB\n- Docker, Flyway & Mongoose support')
19
+ .version(pkg.version, '-v, --version', 'Output the current version')
20
+ .addHelpText('after', `\n${chalk.yellow('Example:')}\n $ nodejs-quickstart init ${chalk.gray('# Start the interactive setup')}\n`);
21
+
22
+ program
23
+ .command('init')
24
+ .description('Initialize a new Node.js project')
25
+ .option('-n, --project-name <name>', 'Project name')
26
+ .option('-l, --language <language>', 'Language (JavaScript, TypeScript)')
27
+ .option('-a, --architecture <architecture>', 'Architecture (MVC, Clean Architecture)')
28
+ .option('--view-engine <view>', 'View Engine (None, EJS, Pug) - MVC only')
29
+ .option('-d, --database <database>', 'Database (MySQL, PostgreSQL)')
30
+ .option('--db-name <name>', 'Database name')
31
+ .option('-c, --communication <communication>', 'Communication (REST APIs, GraphQL, Kafka)')
32
+ .option('--ci-provider <provider>', 'CI/CD Provider (None, GitHub Actions, Jenkins)')
33
+ .option('--caching <type>', 'Caching Layer (None/Redis)')
34
+ .action(async (options) => {
35
+ // Fix for Commander camelCase conversion
36
+ if (options.ciProvider) {
37
+ options.ciProvider = options.ciProvider;
38
+ }
39
+
40
+ console.log(chalk.blue('Welcome to the Node.js Quickstart Generator!'));
41
+
42
+ try {
43
+ const answers = await getProjectDetails(options);
44
+ console.log(chalk.green('\nConfiguration received:'));
45
+ console.log(JSON.stringify(answers, null, 2));
46
+
47
+ console.log(chalk.yellow('\nGenerating project...'));
48
+ await generateProject(answers);
49
+
50
+ console.log(chalk.green('\n✔ Project generated successfully!'));
51
+
52
+ console.log(chalk.magenta('\n🚀 Project is AI-Ready!'));
53
+ console.log(chalk.magenta('-----------------------------------------'));
54
+ console.log(chalk.magenta('🤖 We detected you are using AI tools.'));
55
+ console.log(chalk.magenta(`📍 Use Cursor? We've configured '.cursorrules' for you.`));
56
+ console.log(chalk.magenta(`📍 Use ChatGPT/Gemini? Check the 'prompts/' folder for Agent Skills.`));
57
+ console.log(chalk.magenta('-----------------------------------------'));
58
+
59
+ let manualStartInstructions = `\n${chalk.yellow('Development:')}\n cd ${answers.projectName}\n npm install`;
60
+
61
+ const needsInfrastructure = answers.database !== 'None' || answers.caching === 'Redis' || answers.communication === 'Kafka';
62
+
63
+ if (needsInfrastructure) {
64
+ let servicesToStart = '';
65
+ if (answers.database !== 'None') servicesToStart += ' db';
66
+ if (answers.caching === 'Redis') servicesToStart += ' redis';
67
+ if (answers.communication === 'Kafka') servicesToStart += ' zookeeper kafka';
68
+
69
+ manualStartInstructions += `\n docker-compose up -d${servicesToStart} # Start infrastructure first\n npm run dev`;
70
+ } else {
71
+ manualStartInstructions += `\n npm run dev`;
72
+ }
73
+
74
+ console.log(chalk.cyan(`\nNext steps:\n cd ${answers.projectName}\n npm install\n docker-compose up\n-----------------------${manualStartInstructions}\n\n${chalk.yellow('Production (PM2):')}\n npm run build\n npm run deploy\n npx pm2 logs`));
75
+
76
+ } catch (error) {
77
+ console.error(chalk.red('Error generating project:'), error);
78
+ process.exit(1);
79
+ }
80
+ });
81
+ program.parse(process.argv);
82
+
83
+ if (!process.argv.slice(2).length) {
84
+ program.outputHelp();
85
+ }
package/lib/generator.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from 'path';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { setupProjectDirectory, copyBaseStructure, copyCommonFiles } from './modules/project-setup.js';
4
- import { renderPackageJson, renderDockerCompose, renderReadme, renderDockerfile, renderProfessionalConfig, setupCiCd, renderTestSample, renderEnvExample, renderPm2Config } from './modules/config-files.js';
4
+ import { renderPackageJson, renderDockerCompose, renderReadme, renderDockerfile, renderProfessionalConfig, setupCiCd, renderTestSample, renderEnvExample, renderPm2Config, renderAiNativeFiles } from './modules/config-files.js';
5
5
  import { renderIndexFile, renderEnvConfig, renderErrorMiddleware, renderDynamicComponents, renderSwaggerConfig, setupViews as setupSrcViews, processAllTests } from './modules/app-setup.js';
6
6
  import { setupDatabase } from './modules/database-setup.js';
7
7
  import { setupKafka, setupViews } from './modules/kafka-setup.js';
@@ -81,6 +81,9 @@ export const generateProject = async (config) => {
81
81
  await renderProfessionalConfig(templatesDir, targetDir, config);
82
82
  await renderTestSample(templatesDir, targetDir, config);
83
83
 
84
+ // 13.5 AI-Native Scaffolding
85
+ await renderAiNativeFiles(templatesDir, targetDir, config);
86
+
84
87
  // 14. CI/CD
85
88
  await setupCiCd(templatesDir, targetDir, config);
86
89
 
@@ -114,6 +117,13 @@ export const generateProject = async (config) => {
114
117
  ✅ Docker: Production-ready multi-stage build
115
118
  ${config.ciProvider !== 'None' ? `✅ CI/CD: ${config.ciProvider} Workflow ready` : '❌ CI/CD: Skipped (User preferred)'}
116
119
 
120
+ ----------------------------------------------------
121
+ 🚀 Project is AI-Ready!
122
+ ----------------------------------------------------
123
+ 🤖 We detected you are using AI tools.
124
+ 📍 Use Cursor? We've configured '.cursorrules' for you.
125
+ 📍 Use ChatGPT/Gemini? Check the 'prompts/' folder for Agent Skills.
126
+
117
127
  ----------------------------------------------------
118
128
  👉 Next Steps:
119
129
  ----------------------------------------------------
@@ -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 });
@@ -49,6 +49,31 @@ export const renderProfessionalConfig = async (templatesDir, targetDir, config)
49
49
  await fs.writeFile(path.join(targetDir, 'jest.config.js'), jestContent);
50
50
  };
51
51
 
52
+ export const renderAiNativeFiles = async (templatesDir, targetDir, config) => {
53
+ // 1. .cursorrules
54
+ const cursorRulesPath = path.join(targetDir, '.cursorrules');
55
+ const cursorRulesTemplate = await fs.readFile(path.join(templatesDir, 'common', '.cursorrules.ejs'), 'utf-8');
56
+ const cursorRulesContent = ejs.render(cursorRulesTemplate, { ...config });
57
+ await fs.writeFile(cursorRulesPath, cursorRulesContent);
58
+
59
+ // 2. prompts/
60
+ const promptsDirTarget = path.join(targetDir, 'prompts');
61
+ await fs.ensureDir(promptsDirTarget);
62
+
63
+ const promptsSourceDir = path.join(templatesDir, 'common', 'prompts');
64
+ const promptFiles = await fs.readdir(promptsSourceDir);
65
+
66
+ for (const file of promptFiles) {
67
+ if (file.endsWith('.ejs')) {
68
+ const templatePath = path.join(promptsSourceDir, file);
69
+ const targetFilePath = path.join(promptsDirTarget, file.replace('.ejs', ''));
70
+ const templateContent = await fs.readFile(templatePath, 'utf-8');
71
+ const renderedContent = ejs.render(templateContent, { ...config });
72
+ await fs.writeFile(targetFilePath, renderedContent);
73
+ }
74
+ }
75
+ };
76
+
52
77
  export const setupCiCd = async (templatesDir, targetDir, config) => {
53
78
  const { ciProvider } = config;
54
79
  if (ciProvider === 'GitHub Actions') {
@@ -72,10 +97,10 @@ export const renderTestSample = async (templatesDir, targetDir, config) => {
72
97
  "extends": "../tsconfig.json",
73
98
  "compilerOptions": {
74
99
  "types": ["jest", "node"],
75
- "rootDir": "../tests",
100
+ "rootDir": "..",
76
101
  "noEmit": true
77
102
  },
78
- "include": ["**/*.ts"]
103
+ "include": ["**/*.ts", "../src/**/*.ts"]
79
104
  };
80
105
  await fs.writeFile(path.join(targetDir, 'tests', 'tsconfig.json'), JSON.stringify(testsTsConfig, null, 4));
81
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,8 +1,8 @@
1
1
  {
2
2
  "name": "nodejs-quickstart-structure",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "type": "module",
5
- "description": "A CLI to scaffold Node.js microservices with MVC or Clean Architecture",
5
+ "description": "The ultimate nodejs quickstart structure CLI to scaffold Node.js microservices with MVC or Clean Architecture",
6
6
  "main": "bin/index.js",
7
7
  "bin": {
8
8
  "nodejs-quickstart": "./bin/index.js"
@@ -16,12 +16,17 @@
16
16
  },
17
17
  "keywords": [
18
18
  "nodejs",
19
+ "node",
20
+ "quickstart",
21
+ "structure",
19
22
  "cli",
20
23
  "scaffold",
21
24
  "mvc",
22
25
  "clean-architecture",
23
26
  "microservices",
24
- "backend"
27
+ "backend",
28
+ "generator",
29
+ "boilerplate"
25
30
  ],
26
31
  "author": "Pau Dang <[EMAIL_ADDRESS]>",
27
32
  "repository": {
@@ -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);