nodejs-quickstart-structure 1.11.1 → 1.13.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.
- package/CHANGELOG.md +22 -3
- package/README.md +2 -1
- package/bin/index.js +2 -2
- package/docs/generatorFlow.md +9 -9
- package/lib/generator.js +8 -2
- package/lib/modules/app-setup.js +85 -33
- package/lib/modules/config-files.js +18 -1
- package/lib/modules/kafka-setup.js +4 -39
- package/package.json +1 -1
- package/templates/clean-architecture/js/src/index.js.ejs +2 -4
- package/templates/clean-architecture/js/src/infrastructure/config/env.js.ejs +47 -0
- package/templates/clean-architecture/js/src/infrastructure/webserver/{middlewares/error.middleware.js → middleware/errorMiddleware.js} +2 -1
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +25 -11
- package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.js.ejs +4 -1
- package/templates/clean-architecture/ts/src/config/env.ts.ejs +46 -0
- package/templates/clean-architecture/ts/src/index.ts.ejs +23 -22
- package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +4 -1
- package/templates/clean-architecture/ts/src/utils/{error.middleware.ts.ejs → errorMiddleware.ts.ejs} +2 -1
- package/templates/common/.env.example.ejs +3 -1
- package/templates/common/README.md.ejs +30 -0
- package/templates/common/caching/js/redisClient.js.ejs +4 -0
- package/templates/common/caching/ts/redisClient.ts.ejs +4 -0
- package/templates/common/database/js/mongoose.js.ejs +3 -1
- package/templates/common/database/ts/mongoose.ts.ejs +3 -1
- package/templates/common/docker-compose.yml.ejs +11 -1
- package/templates/common/ecosystem.config.js.ejs +40 -0
- package/templates/common/health/js/healthRoute.js.ejs +44 -0
- package/templates/common/health/ts/healthRoute.ts.ejs +43 -0
- package/templates/common/kafka/js/services/kafkaService.js.ejs +6 -1
- package/templates/common/kafka/ts/services/kafkaService.ts.ejs +5 -0
- package/templates/common/package.json.ejs +3 -1
- package/templates/common/shutdown/js/gracefulShutdown.js.ejs +61 -0
- package/templates/common/shutdown/ts/gracefulShutdown.ts.ejs +58 -0
- package/templates/common/tests/health.test.ts.ejs +16 -6
- package/templates/mvc/js/src/config/env.js.ejs +46 -0
- package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +4 -1
- package/templates/mvc/js/src/index.js.ejs +12 -10
- package/templates/mvc/js/src/utils/{error.middleware.js → errorMiddleware.js} +2 -1
- package/templates/mvc/ts/src/config/env.ts.ejs +45 -0
- package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +4 -1
- package/templates/mvc/ts/src/index.ts.ejs +20 -20
- package/templates/mvc/ts/src/utils/{error.middleware.ts.ejs → errorMiddleware.ts.ejs} +2 -1
- package/templates/clean-architecture/js/src/domain/repositories/UserRepository.js +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -5,15 +5,34 @@ 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.13.0] - 2026-03-05
|
|
9
|
+
### Added
|
|
10
|
+
- **Enhanced Health Check System:**
|
|
11
|
+
- Standardized health logic for both MVC and Clean Architecture.
|
|
12
|
+
- Deep verification: Automatically pings the selected database (MySQL, PostgreSQL, MongoDB) during health checks and returns a `DOWN` status with 500 code if the connection is lost.
|
|
13
|
+
- Structured response including `uptime`, `memoryUsage`, `database` connectivity status, and `timestamp`.
|
|
14
|
+
- **Improved Graceful Shutdown:**
|
|
15
|
+
- Standardized signal handling (`SIGINT`, `SIGTERM`) across all architecture types to ensure clean disposal of database and caching resources.
|
|
16
|
+
- Integrated proper shutdown sequences for Redis, MySQL, PostgreSQL, and MongoDB.
|
|
17
|
+
|
|
18
|
+
## [1.12.0] - 2026-03-04
|
|
19
|
+
### Added
|
|
20
|
+
- **Zod Environment Validation:** Replaced manual `dotenv` process calls in server entry points with a centralized schema parser.
|
|
21
|
+
- Automatically generates `src/config/env.ts` (or `.js`) evaluating `NODE_ENV`, `PORT`, and strictly mapping database, cache, and Kafka connection definitions gracefully crashing the app at startup if missing.
|
|
22
|
+
- **PM2 Deployment Configuration:** Natively supports PM2 ecosystem clustering for VPS and EC2 configurations out-of-the-box.
|
|
23
|
+
- Generates `ecosystem.config.js` intelligently mapping dynamic environments for Redis, Databases, and Kafka without user prompts.
|
|
24
|
+
- Modifies `package.json` with an out-of-the-box `npm run deploy` script bound to `pm2 start ecosystem.config.js --env production`.
|
|
25
|
+
- Upgraded generated README deployment guides for transparent CLI instruction workflows outlining the contrast between running Docker vs PM2.
|
|
26
|
+
|
|
8
27
|
## [1.11.1] - 2026-03-03
|
|
9
28
|
### Fixed
|
|
10
|
-
- Fixed relative import paths in Clean Architecture JS `
|
|
29
|
+
- Fixed relative import paths in Clean Architecture JS `errorMiddleware.js` — changed to correct 3-level relative paths (`../../../`).
|
|
11
30
|
|
|
12
31
|
## [1.11.0] - 2026-03-02
|
|
13
32
|
### Added
|
|
14
33
|
- **Centralized Error Handling Mechanism:** All generated projects now include a standardized, predictable error response structure for both REST APIs and GraphQL communication types.
|
|
15
34
|
- New `src/errors/` directory with custom error classes: `ApiError`, `NotFoundError`, `BadRequestError`.
|
|
16
|
-
- New `error.middleware.{ts|js}` global error handler placed at the end of the Express middleware chain in `src/utils/` (MVC) and `src/utils/` + `src/infrastructure/webserver/
|
|
35
|
+
- New `error.middleware.{ts|js}` global error handler placed at the end of the Express middleware chain in `src/utils/` (MVC) and `src/utils/` + `src/infrastructure/webserver/middleware/` (Clean Architecture).
|
|
17
36
|
- Integrates `winston` logger to automatically log 500-level errors to persistent log files.
|
|
18
37
|
- All controllers updated to pass errors via `next(error)` instead of manually sending responses.
|
|
19
38
|
- **GraphQL:** Apollo Server `formatError` hook configured with `unwrapResolverError` to intercept resolver errors and map `ApiError` instances to structured GraphQL extension codes.
|
|
@@ -23,7 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
23
42
|
### Fixed
|
|
24
43
|
- Fixed `swagger.js` / `swagger.yml` being incorrectly generated for non-REST API configurations (GraphQL, Kafka). Converted static `swagger.js` to `swagger.js.ejs` in MVC JS and Clean Architecture JS templates so `renderSwaggerConfig` can conditionally control its generation.
|
|
25
44
|
- Fixed `userRoutes.ts` in Clean Architecture TypeScript template not passing `NextFunction` to controller methods, causing `TypeScript TS2554: Expected 3 arguments` compile errors during Docker builds.
|
|
26
|
-
- Fixed incorrect relative import paths (`../../../../`) in Clean Architecture JS `
|
|
45
|
+
- Fixed incorrect relative import paths (`../../../../`) in Clean Architecture JS `errorMiddleware.js` — changed to correct 3-level relative paths (`../../../`).
|
|
27
46
|
- Fixed `HTTP_STATUS` being imported without destructuring from `httpCodes.js` (which uses a plain `module.exports = HTTP_STATUS` default export), causing `Cannot read properties of undefined` runtime errors.
|
|
28
47
|
- Fixed `renderErrorMiddleware` in `app-setup.js` deleting from the **template source directory** instead of the generated project's target directory. This caused error middleware templates to disappear from disk after the first test run, breaking all subsequent generations.
|
|
29
48
|
|
package/README.md
CHANGED
|
@@ -34,7 +34,9 @@ We don't just generate boilerplate; we generate **production-ready** foundations
|
|
|
34
34
|
- **🧪 Testing Strategy**: Integrated `Jest` and `Supertest` setup for Unit and Integration testing.
|
|
35
35
|
- **🔄 CI/CD Integration**: Pre-configured workflows for **GitHub Actions**, **Jenkins**, and **GitLab CI**.
|
|
36
36
|
- **⚓ Git Hooks**: `Husky` and `Lint-Staged` to ensure no bad code is ever committed.
|
|
37
|
+
- **🤝 Reliability**: Advanced Health Checks (`/health`) with deep database pings and Graceful Shutdown workflows for zero-downtime rollouts.
|
|
37
38
|
- **🐳 DevOps**: Highly optimized **Multi-Stage Dockerfile** for small, secure production images.
|
|
39
|
+
- **🚀 Deployment**: Ship confidently with an integrated **PM2 Ecosystem Configuration** for zero-downtime reloads and robust process management.
|
|
38
40
|
|
|
39
41
|
## 🧩 240+ Project Combinations
|
|
40
42
|
|
|
@@ -92,7 +94,6 @@ The generated project will include:
|
|
|
92
94
|
|
|
93
95
|
- `src/`: Source code (controllers, routes, services/use-cases).
|
|
94
96
|
- `src/errors/`: Custom error classes — `ApiError`, `NotFoundError`, `BadRequestError`.
|
|
95
|
-
- `src/utils/error.middleware.{ts|js}`: Global Express error handler (logs 500s, returns `{ statusCode, message }`).
|
|
96
97
|
- `flyway/sql/`: SQL migration scripts (if SQL database selected).
|
|
97
98
|
- `docker-compose.yml`: Services configuration for DB, Flyway, and Kafka.
|
|
98
99
|
- `package.json`: Dependencies and scripts (`start`, `dev`, `build`).
|
package/bin/index.js
CHANGED
|
@@ -52,7 +52,7 @@ program
|
|
|
52
52
|
|
|
53
53
|
console.log(chalk.green('\n✔ Project generated successfully!'));
|
|
54
54
|
|
|
55
|
-
let manualStartInstructions = `\
|
|
55
|
+
let manualStartInstructions = `\n${chalk.yellow('Development:')}\n cd ${answers.projectName}\n npm install`;
|
|
56
56
|
|
|
57
57
|
const needsInfrastructure = answers.database !== 'None' || answers.caching === 'Redis' || answers.communication === 'Kafka';
|
|
58
58
|
|
|
@@ -67,7 +67,7 @@ program
|
|
|
67
67
|
manualStartInstructions += `\n npm run dev`;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
console.log(chalk.cyan(`\nNext steps:\n cd ${answers.projectName}\n npm install\n docker-compose up\n-----------------------${manualStartInstructions}`));
|
|
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
71
|
|
|
72
72
|
} catch (error) {
|
|
73
73
|
console.error(chalk.red('Error generating project:'), error);
|
package/docs/generatorFlow.md
CHANGED
|
@@ -50,10 +50,10 @@ The `generateProject` function in `lib/generator.js` executes the following step
|
|
|
50
50
|
* Processes the entry point file to wire up the selected DB, Architecture, and Communication type.
|
|
51
51
|
* **GraphQL**: Wires up Apollo Server middleware with `formatError` hook for centralized error handling.
|
|
52
52
|
* **REST APIs / Kafka**: Registers `app.use(errorMiddleware)` at the end of the Express chain.
|
|
53
|
-
7. **
|
|
54
|
-
* Processes `
|
|
55
|
-
* Renders to `src/utils/
|
|
56
|
-
* **Clean Architecture**: Also handles `src/infrastructure/webserver/
|
|
53
|
+
7. **Error Middleware setup** (`renderErrorMiddleware`):
|
|
54
|
+
* Processes `errorMiddleware.{ts|js}.ejs` template from the target directory's `src/utils/` path.
|
|
55
|
+
* Renders to `src/utils/errorMiddleware.{ts|js}` in the generated project.
|
|
56
|
+
* **Clean Architecture**: Also handles `src/infrastructure/webserver/middleware/errorMiddleware.{js}` path.
|
|
57
57
|
8. **Dynamic Component Generation**:
|
|
58
58
|
* **MVC**: Generates `userController` (imports specific DB service, uses `next(error)`).
|
|
59
59
|
* **Clean Architecture**: Generates `UserRepository` (infrastructure layer implementation).
|
|
@@ -141,7 +141,7 @@ project-name/
|
|
|
141
141
|
│ ├── models/ # Database models
|
|
142
142
|
│ ├── routes/ # Express routes
|
|
143
143
|
│ ├── utils/
|
|
144
|
-
│ │ ├──
|
|
144
|
+
│ │ ├── errorMiddleware.{ts|js} # Global error handler
|
|
145
145
|
│ │ ├── logger.{ts|js}
|
|
146
146
|
│ │ └── httpCodes.{ts|js}
|
|
147
147
|
│ └── index.js|ts # Entry point (registers errorMiddleware last)
|
|
@@ -165,7 +165,7 @@ project-name/
|
|
|
165
165
|
│ │ └── resolvers/ # user.resolvers (calls controllers, throws errors)
|
|
166
166
|
│ ├── models/
|
|
167
167
|
│ ├── utils/
|
|
168
|
-
│ │ ├──
|
|
168
|
+
│ │ ├── errorMiddleware.{ts|js} # Express-level fallback error handler
|
|
169
169
|
│ │ └── logger.{ts|js}
|
|
170
170
|
│ └── index.js|ts # Apollo Server + formatError hook
|
|
171
171
|
└── ...
|
|
@@ -205,11 +205,11 @@ project-name/
|
|
|
205
205
|
│ │ ├── database/ # DB connection & models
|
|
206
206
|
│ │ ├── repositories/ # Data access implementation
|
|
207
207
|
│ │ └── webserver/
|
|
208
|
-
│ │ ├──
|
|
209
|
-
│ │ │ └──
|
|
208
|
+
│ │ ├── middleware/
|
|
209
|
+
│ │ │ └── errorMiddleware.js # JS only — Express error handler
|
|
210
210
|
│ │ └── server.js # Express app setup
|
|
211
211
|
│ ├── utils/
|
|
212
|
-
│ │ └──
|
|
212
|
+
│ │ └── errorMiddleware.ts # TS — global error handler
|
|
213
213
|
│ └── index.js|ts # Registers errorMiddleware after Apollo/Express
|
|
214
214
|
└── ...
|
|
215
215
|
```
|
package/lib/generator.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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 } from './modules/config-files.js';
|
|
5
|
-
import { renderIndexFile, renderErrorMiddleware, renderDynamicComponents, renderSwaggerConfig, setupViews as setupSrcViews } from './modules/app-setup.js';
|
|
4
|
+
import { renderPackageJson, renderDockerCompose, renderReadme, renderDockerfile, renderProfessionalConfig, setupCiCd, renderTestSample, renderEnvExample, renderPm2Config } from './modules/config-files.js';
|
|
5
|
+
import { renderIndexFile, renderEnvConfig, renderErrorMiddleware, renderDynamicComponents, renderSwaggerConfig, setupViews as setupSrcViews } from './modules/app-setup.js';
|
|
6
6
|
import { setupDatabase } from './modules/database-setup.js';
|
|
7
7
|
import { setupKafka, setupViews } from './modules/kafka-setup.js';
|
|
8
8
|
import { setupCaching } from './modules/caching-setup.js';
|
|
@@ -33,6 +33,9 @@ export const generateProject = async (config) => {
|
|
|
33
33
|
// 6. Render index file (ts/js)
|
|
34
34
|
await renderIndexFile(templatePath, targetDir, config);
|
|
35
35
|
|
|
36
|
+
// 6a. Render Environment Configuration
|
|
37
|
+
await renderEnvConfig(templatePath, targetDir, config);
|
|
38
|
+
|
|
36
39
|
// 6a. Render error middleware
|
|
37
40
|
await renderErrorMiddleware(templatePath, targetDir, config);
|
|
38
41
|
|
|
@@ -73,6 +76,9 @@ export const generateProject = async (config) => {
|
|
|
73
76
|
// 15. Env Example
|
|
74
77
|
await renderEnvExample(templatesDir, targetDir, config);
|
|
75
78
|
|
|
79
|
+
// 16. PM2 Configuration
|
|
80
|
+
await renderPm2Config(templatesDir, targetDir, config);
|
|
81
|
+
|
|
76
82
|
console.log(`
|
|
77
83
|
====================================================
|
|
78
84
|
Node.js Project Created Successfully!
|
package/lib/modules/app-setup.js
CHANGED
|
@@ -3,28 +3,53 @@ import path from 'path';
|
|
|
3
3
|
import ejs from 'ejs';
|
|
4
4
|
|
|
5
5
|
export const renderIndexFile = async (templatePath, targetDir, config) => {
|
|
6
|
-
const { communication, viewEngine, database, architecture, projectName, language } = config;
|
|
6
|
+
const { communication, viewEngine, database, architecture, projectName, language, caching } = config;
|
|
7
7
|
const indexFileName = language === 'TypeScript' ? 'index.ts' : 'index.js';
|
|
8
8
|
const indexPath = path.join(targetDir, 'src', indexFileName);
|
|
9
9
|
const indexTemplateSource = path.join(templatePath, 'src', `${indexFileName}.ejs`);
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
if (await fs.pathExists(indexTemplateSource)) {
|
|
12
12
|
const indexTemplate = await fs.readFile(indexTemplateSource, 'utf-8');
|
|
13
|
-
const indexContent = ejs.render(indexTemplate, {
|
|
14
|
-
communication,
|
|
13
|
+
const indexContent = ejs.render(indexTemplate, {
|
|
14
|
+
communication,
|
|
15
15
|
viewEngine,
|
|
16
16
|
database,
|
|
17
17
|
architecture,
|
|
18
|
-
projectName
|
|
18
|
+
projectName,
|
|
19
|
+
caching
|
|
19
20
|
});
|
|
20
21
|
await fs.writeFile(indexPath, indexContent);
|
|
21
22
|
await fs.remove(path.join(targetDir, 'src', `${indexFileName}.ejs`));
|
|
22
23
|
}
|
|
23
24
|
};
|
|
24
25
|
|
|
26
|
+
export const renderEnvConfig = async (templatePath, targetDir, config) => {
|
|
27
|
+
const { language, architecture, database, caching, communication } = config;
|
|
28
|
+
const envExt = language === 'TypeScript' ? 'ts' : 'js';
|
|
29
|
+
|
|
30
|
+
let configDir = path.join(targetDir, 'src', 'config');
|
|
31
|
+
if (architecture === 'Clean Architecture' && language === 'JavaScript') {
|
|
32
|
+
configDir = path.join(targetDir, 'src', 'infrastructure', 'config');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const envTemplatePath = path.join(configDir, `env.${envExt}.ejs`);
|
|
36
|
+
const envDestPath = path.join(configDir, `env.${envExt}`);
|
|
37
|
+
|
|
38
|
+
if (await fs.pathExists(envTemplatePath)) {
|
|
39
|
+
const envTemplate = await fs.readFile(envTemplatePath, 'utf-8');
|
|
40
|
+
const envContent = ejs.render(envTemplate, {
|
|
41
|
+
database,
|
|
42
|
+
caching,
|
|
43
|
+
communication
|
|
44
|
+
});
|
|
45
|
+
await fs.writeFile(envDestPath, envContent);
|
|
46
|
+
await fs.remove(envTemplatePath);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
25
50
|
export const renderErrorMiddleware = async (templatePath, targetDir, config) => {
|
|
26
51
|
const { language, architecture } = config;
|
|
27
|
-
const errName = language === 'TypeScript' ? '
|
|
52
|
+
const errName = language === 'TypeScript' ? 'errorMiddleware.ts' : 'errorMiddleware.js';
|
|
28
53
|
const errTemplateName = `${errName}.ejs`;
|
|
29
54
|
|
|
30
55
|
if (architecture === 'MVC') {
|
|
@@ -44,8 +69,8 @@ export const renderErrorMiddleware = async (templatePath, targetDir, config) =>
|
|
|
44
69
|
await fs.writeFile(utilsDest, await fs.readFile(utilsEjsCopy, 'utf-8'));
|
|
45
70
|
await fs.remove(utilsEjsCopy);
|
|
46
71
|
}
|
|
47
|
-
// Also render the
|
|
48
|
-
const mwDir = path.join(targetDir, 'src/infrastructure/webserver/
|
|
72
|
+
// Also render the middleware version if present (NOT removing from template)
|
|
73
|
+
const mwDir = path.join(targetDir, 'src/infrastructure/webserver/middleware');
|
|
49
74
|
const mwEjsCopy = path.join(mwDir, errTemplateName);
|
|
50
75
|
const mwDest = path.join(mwDir, errName);
|
|
51
76
|
if (await fs.pathExists(mwEjsCopy)) {
|
|
@@ -70,11 +95,11 @@ export const renderDynamicComponents = async (templatePath, targetDir, config) =
|
|
|
70
95
|
await fs.writeFile(userControllerPath, content);
|
|
71
96
|
await fs.remove(path.join(targetDir, 'src/controllers', `${userControllerName}.ejs`));
|
|
72
97
|
}
|
|
73
|
-
}
|
|
98
|
+
}
|
|
74
99
|
// Clean Architecture Repo
|
|
75
100
|
else if (architecture === 'Clean Architecture') {
|
|
76
101
|
const repoName = language === 'TypeScript' ? 'UserRepository.ts' : 'UserRepository.js';
|
|
77
|
-
const repoPath = path.join(targetDir, 'src/infrastructure/repositories', repoName);
|
|
102
|
+
const repoPath = path.join(targetDir, 'src/infrastructure/repositories', repoName);
|
|
78
103
|
const repoTemplate = path.join(templatePath, 'src/infrastructure/repositories', `${repoName}.ejs`);
|
|
79
104
|
|
|
80
105
|
if (await fs.pathExists(repoTemplate)) {
|
|
@@ -95,22 +120,58 @@ export const renderDynamicComponents = async (templatePath, targetDir, config) =
|
|
|
95
120
|
}
|
|
96
121
|
// Render Server (Clean Arch JS only)
|
|
97
122
|
if (architecture === 'Clean Architecture' && language === 'JavaScript') {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
const serverName = 'server.js';
|
|
124
|
+
const serverPath = path.join(targetDir, 'src/infrastructure/webserver', serverName);
|
|
125
|
+
const serverTemplate = path.join(templatePath, 'src/infrastructure/webserver', `${serverName}.ejs`);
|
|
126
|
+
|
|
127
|
+
if (await fs.pathExists(serverTemplate)) {
|
|
128
|
+
const content = ejs.render(await fs.readFile(serverTemplate, 'utf-8'), { communication: config.communication, database, caching });
|
|
129
|
+
await fs.writeFile(serverPath, content);
|
|
130
|
+
await fs.remove(path.join(targetDir, 'src/infrastructure/webserver', `${serverName}.ejs`));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Cleanup REST routes if GraphQL or Kafka is selected
|
|
135
|
+
if (config.communication !== 'REST APIs') {
|
|
136
|
+
if (architecture === 'MVC') {
|
|
137
|
+
await fs.remove(path.join(targetDir, 'src/routes'));
|
|
138
|
+
} else if (architecture === 'Clean Architecture') {
|
|
139
|
+
await fs.remove(path.join(targetDir, 'src/interfaces/routes'));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Advanced Health Check Route Modularization
|
|
144
|
+
const healthExt = language === 'TypeScript' ? 'ts' : 'js';
|
|
145
|
+
const healthTemplatePath = path.join(templatePath, '../../common/health', healthExt, `healthRoute.${healthExt}.ejs`);
|
|
146
|
+
|
|
147
|
+
if (await fs.pathExists(healthTemplatePath)) {
|
|
148
|
+
let routeDestDir = path.join(targetDir, 'src', 'routes');
|
|
149
|
+
if (architecture === 'Clean Architecture') {
|
|
150
|
+
routeDestDir = path.join(targetDir, 'src', 'interfaces', 'routes');
|
|
151
|
+
}
|
|
152
|
+
await fs.ensureDir(routeDestDir);
|
|
153
|
+
|
|
154
|
+
const healthRouteContent = ejs.render(await fs.readFile(healthTemplatePath, 'utf-8'), { database, architecture });
|
|
155
|
+
await fs.writeFile(path.join(routeDestDir, `healthRoute.${healthExt}`), healthRouteContent);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Advanced Graceful Shutdown Modularization
|
|
159
|
+
const shutdownExt = language === 'TypeScript' ? 'ts' : 'js';
|
|
160
|
+
const shutdownTemplatePath = path.join(templatePath, '../../common/shutdown', shutdownExt, `gracefulShutdown.${shutdownExt}.ejs`);
|
|
161
|
+
|
|
162
|
+
if (await fs.pathExists(shutdownTemplatePath)) {
|
|
163
|
+
let utilsDestDir = path.join(targetDir, 'src', 'utils');
|
|
164
|
+
await fs.ensureDir(utilsDestDir);
|
|
165
|
+
|
|
166
|
+
const shutdownContent = ejs.render(await fs.readFile(shutdownTemplatePath, 'utf-8'), { database, architecture, caching, communication: config.communication });
|
|
167
|
+
await fs.writeFile(path.join(utilsDestDir, `gracefulShutdown.${shutdownExt}`), shutdownContent);
|
|
107
168
|
}
|
|
108
169
|
|
|
109
170
|
// GraphQL Setup
|
|
110
171
|
if (config.communication === 'GraphQL') {
|
|
111
172
|
const ext = language === 'TypeScript' ? 'ts' : 'js';
|
|
112
173
|
let graphqlInternalPath = architecture === 'MVC' ? 'src/graphql' : 'src/interfaces/graphql';
|
|
113
|
-
|
|
174
|
+
|
|
114
175
|
const sourceGraphqDir = path.join(templatePath, graphqlInternalPath);
|
|
115
176
|
const targetGraphqlDir = path.join(targetDir, graphqlInternalPath);
|
|
116
177
|
|
|
@@ -145,27 +206,18 @@ export const renderDynamicComponents = async (templatePath, targetDir, config) =
|
|
|
145
206
|
let graphqlInternalPath = architecture === 'MVC' ? 'src/graphql' : 'src/interfaces/graphql';
|
|
146
207
|
await fs.remove(path.join(targetDir, graphqlInternalPath));
|
|
147
208
|
}
|
|
148
|
-
|
|
149
|
-
// Cleanup REST routes if GraphQL or Kafka is selected
|
|
150
|
-
if (config.communication !== 'REST APIs') {
|
|
151
|
-
if (architecture === 'MVC') {
|
|
152
|
-
await fs.remove(path.join(targetDir, 'src/routes'));
|
|
153
|
-
} else if (architecture === 'Clean Architecture') {
|
|
154
|
-
await fs.remove(path.join(targetDir, 'src/interfaces/routes'));
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
209
|
};
|
|
158
210
|
|
|
159
211
|
export const renderSwaggerConfig = async (templatesDir, targetDir, config) => {
|
|
160
212
|
const { communication, projectName, architecture, language } = config;
|
|
161
|
-
|
|
213
|
+
|
|
162
214
|
// Check for Swagger config template (typically in src/config/swagger.ts.ejs)
|
|
163
215
|
const swaggerTsTemplate = path.join(targetDir, 'src', 'config', 'swagger.ts.ejs');
|
|
164
216
|
// MVC JS uses swagger.js.ejs in src/config/
|
|
165
217
|
const swaggerJsTemplate = path.join(targetDir, 'src', 'config', 'swagger.js.ejs');
|
|
166
218
|
// Clean Arch JS uses swagger.js.ejs in src/infrastructure/webserver/
|
|
167
219
|
const swaggerJsCleanTemplate = path.join(targetDir, 'src', 'infrastructure', 'webserver', 'swagger.js.ejs');
|
|
168
|
-
|
|
220
|
+
|
|
169
221
|
// Ensure config directory exists
|
|
170
222
|
let configDir = path.join(targetDir, 'src', 'config');
|
|
171
223
|
if (architecture === 'Clean Architecture' && language === 'JavaScript') {
|
|
@@ -197,7 +249,7 @@ export const renderSwaggerConfig = async (templatesDir, targetDir, config) => {
|
|
|
197
249
|
await fs.writeFile(path.join(targetDir, 'src', 'infrastructure', 'webserver', 'swagger.js'), content);
|
|
198
250
|
}
|
|
199
251
|
}
|
|
200
|
-
|
|
252
|
+
|
|
201
253
|
// Always remove the .ejs template after processing (or if non-REST, just delete it)
|
|
202
254
|
if (await fs.pathExists(swaggerTsTemplate)) await fs.remove(swaggerTsTemplate);
|
|
203
255
|
if (await fs.pathExists(swaggerJsTemplate)) await fs.remove(swaggerJsTemplate);
|
|
@@ -215,7 +267,7 @@ export const setupViews = async (templatesDir, targetDir, config) => {
|
|
|
215
267
|
if (architecture === 'MVC' && viewEngine && viewEngine !== 'None') {
|
|
216
268
|
const viewsSource = path.join(templatesDir, 'common', 'views', viewEngine.toLowerCase());
|
|
217
269
|
if (await fs.pathExists(viewsSource)) {
|
|
218
|
-
|
|
270
|
+
await fs.copy(viewsSource, path.join(targetDir, 'src/views'));
|
|
219
271
|
}
|
|
220
272
|
}
|
|
221
273
|
};
|
|
@@ -59,6 +59,21 @@ export const renderDockerfile = async (templatesDir, targetDir, config) => {
|
|
|
59
59
|
await fs.writeFile(path.join(targetDir, 'Dockerfile'), dockerfileContent);
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
export const renderPm2Config = async (templatesDir, targetDir, config) => {
|
|
63
|
+
const { projectName, database, dbName, communication, language, caching } = config;
|
|
64
|
+
const pm2ConfigPath = path.join(targetDir, 'ecosystem.config.js');
|
|
65
|
+
const pm2Template = await fs.readFile(path.join(templatesDir, 'common', 'ecosystem.config.js.ejs'), 'utf-8');
|
|
66
|
+
const pm2Content = ejs.render(pm2Template, {
|
|
67
|
+
projectName,
|
|
68
|
+
database,
|
|
69
|
+
dbName,
|
|
70
|
+
communication,
|
|
71
|
+
language,
|
|
72
|
+
caching
|
|
73
|
+
});
|
|
74
|
+
await fs.writeFile(pm2ConfigPath, pm2Content);
|
|
75
|
+
};
|
|
76
|
+
|
|
62
77
|
export const renderProfessionalConfig = async (templatesDir, targetDir, language) => {
|
|
63
78
|
const eslintTemplate = await fs.readFile(path.join(templatesDir, 'common', 'eslint.config.mjs.ejs'), 'utf-8');
|
|
64
79
|
const eslintContent = ejs.render(eslintTemplate, { language });
|
|
@@ -98,7 +113,8 @@ export const renderTestSample = async (templatesDir, targetDir, language) => {
|
|
|
98
113
|
|
|
99
114
|
export const renderEnvExample = async (templatesDir, targetDir, config) => {
|
|
100
115
|
const { database, dbName, communication, projectName, caching } = config;
|
|
101
|
-
const
|
|
116
|
+
const envExamplePath = path.join(targetDir, '.env.example');
|
|
117
|
+
const envPath = path.join(targetDir, '.env');
|
|
102
118
|
const envTemplate = await fs.readFile(path.join(templatesDir, 'common', '.env.example.ejs'), 'utf-8');
|
|
103
119
|
|
|
104
120
|
const envContent = ejs.render(envTemplate, {
|
|
@@ -109,5 +125,6 @@ export const renderEnvExample = async (templatesDir, targetDir, config) => {
|
|
|
109
125
|
caching
|
|
110
126
|
});
|
|
111
127
|
|
|
128
|
+
await fs.writeFile(envExamplePath, envContent);
|
|
112
129
|
await fs.writeFile(envPath, envContent);
|
|
113
130
|
};
|
|
@@ -55,25 +55,14 @@ export const setupKafka = async (templatesDir, targetDir, config) => {
|
|
|
55
55
|
// Cleanup old services folder
|
|
56
56
|
await fs.remove(path.join(targetDir, 'src/services'));
|
|
57
57
|
|
|
58
|
-
// Remove
|
|
59
|
-
// But Database setup might assume src/config existence in some templates (though moved to infrastructure/database for clean arch)
|
|
60
|
-
// Safest to leave src/config if non-empty, or remove if empty.
|
|
61
|
-
// For now, mirroring original logic: remove specific REST folders
|
|
62
|
-
|
|
63
|
-
// Remove REST-specific folders (Interfaces)
|
|
64
|
-
await fs.remove(path.join(targetDir, 'src/interfaces/routes'));
|
|
58
|
+
// Remove REST-specific folders (Interfaces) - Note: routes is kept for health endpoint
|
|
65
59
|
await fs.remove(path.join(targetDir, 'src/interfaces/controllers'));
|
|
66
60
|
|
|
67
|
-
// Original logic removed src/config entirely for
|
|
68
|
-
//
|
|
69
|
-
// await fs.remove(path.join(targetDir, 'src/config'));
|
|
70
|
-
// Yes, it did.
|
|
71
|
-
await fs.remove(path.join(targetDir, 'src/config'));
|
|
72
|
-
|
|
61
|
+
// Original logic removed src/config entirely, but now we use it for Zod env validation in TS.
|
|
62
|
+
// We will no longer delete it.
|
|
73
63
|
} else if (architecture === 'MVC' && (!config.viewEngine || config.viewEngine === 'None')) {
|
|
74
|
-
// MVC Cleanup for Kafka Worker (No views)
|
|
64
|
+
// MVC Cleanup for Kafka Worker (No views) - Note: routes is kept for health endpoint
|
|
75
65
|
await fs.remove(path.join(targetDir, 'src/controllers'));
|
|
76
|
-
await fs.remove(path.join(targetDir, 'src/routes'));
|
|
77
66
|
}
|
|
78
67
|
};
|
|
79
68
|
|
|
@@ -84,29 +73,5 @@ export const setupViews = async (templatesDir, targetDir, config) => {
|
|
|
84
73
|
if (await fs.pathExists(publicDir)) {
|
|
85
74
|
await fs.copy(publicDir, path.join(targetDir, 'public'));
|
|
86
75
|
}
|
|
87
|
-
|
|
88
|
-
// Copy views mapping
|
|
89
|
-
// Logic handled in database-setup (part of db config block in original) but functionally belongs here or separate.
|
|
90
|
-
// Original: if (viewEngine && viewEngine !== 'None') await fs.copy(...) inside the DB block for MVC.
|
|
91
|
-
// We moved it to database-setup.js to match flow, but let's double check if we missed it there.
|
|
92
|
-
// Checked database-setup.js: It copies views ONLY if database !== 'None' OR if database === 'None'
|
|
93
|
-
// So it is covered. Ideally it should be here, but for now strict refactor keeps it effectively in DB/structure setup phase.
|
|
94
|
-
// To be cleaner, we should move the VIEW copying here.
|
|
95
|
-
|
|
96
|
-
// Moving View Copying Check here for better separation:
|
|
97
|
-
// We need to verify if database-setup.js ALREADY does this.
|
|
98
|
-
// In my prev step for database-setup.js, I included logic:
|
|
99
|
-
// if (architecture === 'MVC') { if (viewEngine...) copy views }
|
|
100
|
-
// So duplication might occur if I add it here too.
|
|
101
|
-
// Let's relies on this module ONLY for public assets for now, or ensure idempotency.
|
|
102
|
-
|
|
103
|
-
// Actually, let's keep it clean. database-setup.js shouldn't handle views.
|
|
104
|
-
// I will assume I can update database-setup.js to remove view copying if I put it here?
|
|
105
|
-
// OR just leave it there for this iteration to avoid breaking changes in flow order.
|
|
106
|
-
// Let's stick to the original flow where possible, but this module is 'kafka-and-views'.
|
|
107
|
-
|
|
108
|
-
// The original logic had view copying inside the "Database Config" block.
|
|
109
|
-
// My database-setup.js preserved that.
|
|
110
|
-
// So this logic here only handles 'public' folder copying which was Step 8 in original.
|
|
111
76
|
}
|
|
112
77
|
};
|
package/package.json
CHANGED
|
@@ -4,8 +4,6 @@ const logger = require('./infrastructure/log/logger');
|
|
|
4
4
|
const { connectKafka, sendMessage } = require('./infrastructure/messaging/kafkaClient');
|
|
5
5
|
<% } -%>
|
|
6
6
|
|
|
7
|
-
const PORT = process.env.PORT || 3000;
|
|
8
|
-
|
|
9
7
|
<%_ if (database !== 'None') { -%>
|
|
10
8
|
// Database Sync
|
|
11
9
|
const syncDatabase = async () => {
|
|
@@ -21,7 +19,7 @@ const syncDatabase = async () => {
|
|
|
21
19
|
<%_ } -%>
|
|
22
20
|
logger.info('Database synced');
|
|
23
21
|
// Start the web server after DB sync
|
|
24
|
-
startServer(
|
|
22
|
+
startServer();
|
|
25
23
|
<%_ if (communication === 'Kafka') { -%>
|
|
26
24
|
// Connect Kafka
|
|
27
25
|
connectKafka().then(async () => {
|
|
@@ -42,7 +40,7 @@ const syncDatabase = async () => {
|
|
|
42
40
|
};
|
|
43
41
|
syncDatabase();
|
|
44
42
|
<%_ } else { -%>
|
|
45
|
-
startServer(
|
|
43
|
+
startServer();
|
|
46
44
|
<%_ if (communication === 'Kafka') { -%>
|
|
47
45
|
// Connect Kafka
|
|
48
46
|
connectKafka().then(async () => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const { z } = require('zod');
|
|
2
|
+
const logger = require('../log/logger');
|
|
3
|
+
|
|
4
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
5
|
+
require('dotenv').config();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const envSchema = z.object({
|
|
9
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
10
|
+
PORT: z.string().transform(Number).default('3000'),
|
|
11
|
+
<%_ if (database !== 'None') { -%>
|
|
12
|
+
DB_HOST: z.string(),
|
|
13
|
+
<%_ if (database === 'MySQL') { -%>
|
|
14
|
+
DB_USER: z.string(),
|
|
15
|
+
DB_PASSWORD: z.string(),
|
|
16
|
+
DB_NAME: z.string(),
|
|
17
|
+
DB_PORT: z.string().transform(Number),
|
|
18
|
+
<%_ } else if (database === 'PostgreSQL') { -%>
|
|
19
|
+
DB_USER: z.string(),
|
|
20
|
+
DB_PASSWORD: z.string(),
|
|
21
|
+
DB_NAME: z.string(),
|
|
22
|
+
DB_PORT: z.string().transform(Number),
|
|
23
|
+
<%_ } else if (database === 'MongoDB') { -%>
|
|
24
|
+
DB_NAME: z.string(),
|
|
25
|
+
DB_PORT: z.string().transform(Number),
|
|
26
|
+
<%_ } -%>
|
|
27
|
+
<%_ } -%>
|
|
28
|
+
<%_ if (caching === 'Redis') { -%>
|
|
29
|
+
REDIS_HOST: z.string(),
|
|
30
|
+
REDIS_PORT: z.string().transform(Number),
|
|
31
|
+
REDIS_PASSWORD: z.string().optional(),
|
|
32
|
+
<%_ } -%>
|
|
33
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
34
|
+
KAFKA_BROKER: z.string(),
|
|
35
|
+
<%_ } -%>
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const _env = envSchema.safeParse(process.env);
|
|
39
|
+
|
|
40
|
+
if (!_env.success) {
|
|
41
|
+
logger.error('❌ Invalid environment variables:', _env.error.format());
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const env = _env.data;
|
|
46
|
+
|
|
47
|
+
module.exports = { env };
|
|
@@ -2,7 +2,8 @@ const logger = require('../../log/logger');
|
|
|
2
2
|
const { ApiError } = require('../../../errors/ApiError');
|
|
3
3
|
const HTTP_STATUS = require('../../../utils/httpCodes');
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// eslint-disable-next-line no-unused-vars
|
|
6
|
+
const errorMiddleware = (err, req, res, next) => {
|
|
6
7
|
let error = err;
|
|
7
8
|
|
|
8
9
|
if (!(error instanceof ApiError)) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const cors = require('cors');
|
|
3
|
-
require('dotenv').config();
|
|
4
3
|
const logger = require('../log/logger');
|
|
5
4
|
const morgan = require('morgan');
|
|
6
|
-
const { errorMiddleware } = require('./
|
|
7
|
-
|
|
5
|
+
const { errorMiddleware } = require('./middleware/errorMiddleware');
|
|
6
|
+
const healthRoutes = require('../../interfaces/routes/healthRoute');
|
|
8
7
|
<%_ if (communication === 'REST APIs') { -%>
|
|
8
|
+
const apiRoutes = require('../../interfaces/routes/api');
|
|
9
9
|
const swaggerUi = require('swagger-ui-express');
|
|
10
10
|
const swaggerSpecs = require('./swagger');
|
|
11
11
|
<%_ } -%>
|
|
@@ -19,7 +19,11 @@ const { typeDefs, resolvers } = require('../../interfaces/graphql');
|
|
|
19
19
|
const { gqlContext } = require('../../interfaces/graphql/context');
|
|
20
20
|
<%_ } -%>
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const { env } = require('../config/env');
|
|
23
|
+
|
|
24
|
+
const startServer = async () => {
|
|
25
|
+
// Determine port using the validated env
|
|
26
|
+
const port = env.PORT;
|
|
23
27
|
const app = express();
|
|
24
28
|
|
|
25
29
|
app.use(cors());
|
|
@@ -33,7 +37,7 @@ const startServer = async (port) => {
|
|
|
33
37
|
<%_ } -%>
|
|
34
38
|
<%_ if (communication === 'GraphQL') { -%>
|
|
35
39
|
// GraphQL Setup
|
|
36
|
-
const
|
|
40
|
+
const apolloServer = new ApolloServer({
|
|
37
41
|
typeDefs,
|
|
38
42
|
resolvers,
|
|
39
43
|
plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })],
|
|
@@ -57,18 +61,28 @@ const startServer = async (port) => {
|
|
|
57
61
|
return formattedError;
|
|
58
62
|
},
|
|
59
63
|
});
|
|
60
|
-
await
|
|
61
|
-
app.use('/graphql', expressMiddleware(
|
|
64
|
+
await apolloServer.start();
|
|
65
|
+
app.use('/graphql', expressMiddleware(apolloServer, { context: gqlContext }));
|
|
62
66
|
<%_ } -%>
|
|
63
|
-
app.
|
|
64
|
-
res.json({ status: 'UP' });
|
|
65
|
-
});
|
|
67
|
+
app.use('/health', healthRoutes);
|
|
66
68
|
|
|
67
69
|
app.use(errorMiddleware);
|
|
68
70
|
|
|
69
|
-
app.listen(port, () => {
|
|
71
|
+
const server = app.listen(port, () => {
|
|
70
72
|
logger.info(`Server running on port ${port}`);
|
|
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
|
+
});
|
|
81
|
+
<%_ } -%>
|
|
71
82
|
});
|
|
83
|
+
|
|
84
|
+
const setupGracefulShutdown = require('../../utils/gracefulShutdown');
|
|
85
|
+
setupGracefulShutdown(server);
|
|
72
86
|
};
|
|
73
87
|
|
|
74
88
|
module.exports = startServer;
|
package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.js.ejs
CHANGED
|
@@ -12,7 +12,10 @@ const userResolvers = {
|
|
|
12
12
|
createUser: async (_, { name, email }) => {
|
|
13
13
|
return await userController.createUser({ name, email });
|
|
14
14
|
}
|
|
15
|
-
}
|
|
15
|
+
}<%_ if (database === 'MongoDB') { -%>,
|
|
16
|
+
User: {
|
|
17
|
+
id: (parent) => parent.id || parent._id
|
|
18
|
+
}<%_ } %>
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
module.exports = { userResolvers };
|