nodejs-quickstart-structure 1.1.11 → 1.3.5
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/README.md +2 -2
- package/bin/index.js +5 -7
- package/lib/generator.js +38 -8
- package/lib/prompts.js +6 -5
- package/package.json +39 -39
- package/templates/clean-architecture/js/src/index.js.ejs +3 -3
- package/templates/clean-architecture/js/src/infrastructure/log/logger.js +5 -5
- package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +10 -7
- package/templates/clean-architecture/js/src/interfaces/controllers/UserController.js +6 -1
- package/templates/clean-architecture/ts/src/index.ts.ejs +14 -12
- package/templates/clean-architecture/ts/src/infrastructure/log/logger.ts +5 -5
- package/templates/clean-architecture/ts/src/interfaces/controllers/userController.ts +3 -2
- package/templates/common/Jenkinsfile.ejs +35 -0
- package/templates/common/README.md.ejs +20 -0
- package/templates/common/kafka/js/services/{kafkaService.js → kafkaService.js.ejs} +2 -1
- package/templates/common/kafka/ts/services/{kafkaService.ts → kafkaService.ts.ejs} +2 -1
- package/templates/mvc/js/src/controllers/userController.js.ejs +3 -1
- package/templates/mvc/js/src/index.js.ejs +9 -9
- package/templates/mvc/js/src/utils/logger.js +5 -5
- package/templates/mvc/ts/src/controllers/userController.ts.ejs +3 -0
- package/templates/mvc/ts/src/index.ts.ejs +16 -13
- package/templates/mvc/ts/src/utils/logger.ts +5 -5
- package/templates/mvc/js/src/controllers/userController.js +0 -14
- /package/templates/clean-architecture/ts/src/infrastructure/repositories/{UserRepository.ts.ejs → userRepository.ts.ejs} +0 -0
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ We don't just generate boilerplate; we generate **production-ready** foundations
|
|
|
22
22
|
- **🔍 Code Quality**: Pre-configured `Eslint` and `Prettier` for consistent coding standards.
|
|
23
23
|
- **🛡️ Security**: Built-in `Helmet`, `HPP`, `CORS`, and Rate-Limiting middleware.
|
|
24
24
|
- **🧪 Testing Strategy**: Integrated `Jest` and `Supertest` setup for Unit and Integration testing.
|
|
25
|
-
- **🔄 CI/CD Support**:
|
|
25
|
+
- **🔄 CI/CD Support**: Choice between **GitHub Actions** and **Jenkins**.
|
|
26
26
|
- **⚓ Git Hooks**: `Husky` and `Lint-Staged` to ensure no bad code is ever committed.
|
|
27
27
|
- **🐳 DevOps**: Highly optimized **Multi-Stage Dockerfile** for small, secure production images.
|
|
28
28
|
|
|
@@ -70,7 +70,7 @@ The CLI will guide you through the following steps:
|
|
|
70
70
|
4. **Database**: `MySQL` or `PostgreSQL`.
|
|
71
71
|
5. **Database Name**: The name of the initial database.
|
|
72
72
|
6. **Communication**: `REST APIs` (default) or `Kafka`.
|
|
73
|
-
7. **CI/CD**: `
|
|
73
|
+
7. **CI/CD**: `GitHub Actions`, `Jenkins`, or `None`.
|
|
74
74
|
|
|
75
75
|
## Generated Project Structure
|
|
76
76
|
|
package/bin/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import chalk from 'chalk';
|
|
@@ -32,12 +32,11 @@ program
|
|
|
32
32
|
.option('-d, --database <database>', 'Database (MySQL, PostgreSQL)')
|
|
33
33
|
.option('--db-name <name>', 'Database name')
|
|
34
34
|
.option('-c, --communication <communication>', 'Communication (REST APIs, Kafka)')
|
|
35
|
-
.option('--
|
|
35
|
+
.option('--ci-provider <provider>', 'CI/CD Provider (None, GitHub Actions, Jenkins)')
|
|
36
36
|
.action(async (options) => {
|
|
37
37
|
// Fix for Commander camelCase conversion
|
|
38
|
-
if (options.
|
|
39
|
-
options.
|
|
40
|
-
delete options.includeCi;
|
|
38
|
+
if (options.ciProvider) {
|
|
39
|
+
options.ciProvider = options.ciProvider;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
console.log(chalk.blue('Welcome to the Node.js Quickstart Generator!'));
|
|
@@ -51,14 +50,13 @@ program
|
|
|
51
50
|
await generateProject(answers);
|
|
52
51
|
|
|
53
52
|
console.log(chalk.green('\n✔ Project generated successfully!'));
|
|
54
|
-
console.log(chalk.cyan(`\nNext steps:\n cd ${answers.projectName}\n npm install\n docker-compose up\n-----------------------\nStart the app manually:\n npm install\n npm run dev`));
|
|
53
|
+
console.log(chalk.cyan(`\nNext steps:\n cd ${answers.projectName}\n npm install\n docker-compose up\n-----------------------\nStart the app manually:\n cd ${answers.projectName}\n npm install\n npm run dev`));
|
|
55
54
|
|
|
56
55
|
} catch (error) {
|
|
57
56
|
console.error(chalk.red('Error generating project:'), error);
|
|
58
57
|
process.exit(1);
|
|
59
58
|
}
|
|
60
59
|
});
|
|
61
|
-
|
|
62
60
|
program.parse(process.argv);
|
|
63
61
|
|
|
64
62
|
if (!process.argv.slice(2).length) {
|
package/lib/generator.js
CHANGED
|
@@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
7
7
|
const __dirname = path.dirname(__filename);
|
|
8
8
|
|
|
9
9
|
export const generateProject = async (config) => {
|
|
10
|
-
const { projectName, architecture, database, dbName, communication, language, viewEngine,
|
|
10
|
+
const { projectName, architecture, database, dbName, communication, language, viewEngine, ciProvider } = config;
|
|
11
11
|
const targetDir = path.resolve(process.cwd(), projectName);
|
|
12
12
|
const templatesDir = path.join(__dirname, '../templates');
|
|
13
13
|
|
|
@@ -60,7 +60,8 @@ export const generateProject = async (config) => {
|
|
|
60
60
|
architecture,
|
|
61
61
|
database,
|
|
62
62
|
communication,
|
|
63
|
-
language
|
|
63
|
+
language,
|
|
64
|
+
ciProvider
|
|
64
65
|
});
|
|
65
66
|
await fs.writeFile(readmePath, readmeContent);
|
|
66
67
|
|
|
@@ -125,6 +126,24 @@ export const generateProject = async (config) => {
|
|
|
125
126
|
const kafkaSource = path.join(templatesDir, 'common', 'kafka', langExt);
|
|
126
127
|
await fs.copy(kafkaSource, path.join(targetDir, 'src'));
|
|
127
128
|
|
|
129
|
+
// Render Kafka Service with dynamic logger path
|
|
130
|
+
const kafkaServiceFileName = `kafkaService.${langExt}`;
|
|
131
|
+
const kafkaServiceTemplate = path.join(targetDir, 'src', 'services', `${kafkaServiceFileName}.ejs`);
|
|
132
|
+
|
|
133
|
+
if (await fs.pathExists(kafkaServiceTemplate)) {
|
|
134
|
+
const loggerPath = architecture === 'Clean Architecture' ? '../log/logger' : '../utils/logger';
|
|
135
|
+
// Note: For MVC, it's relative to src/services (../utils/logger). Wait, ../utils/logger means src/utils/logger.
|
|
136
|
+
// src/services/kafka.ts -> ../utils/logger -> src/utils/logger. Correct.
|
|
137
|
+
// But wait, generated code for MVC usually puts it in src/services.
|
|
138
|
+
// Let's re-verify MVC structure for logger.
|
|
139
|
+
// In MVC, logger is usually in src/utils/logger.ts?
|
|
140
|
+
// Let's check where logger is copied for MVC.
|
|
141
|
+
|
|
142
|
+
const content = ejs.render(await fs.readFile(kafkaServiceTemplate, 'utf-8'), { loggerPath });
|
|
143
|
+
await fs.writeFile(path.join(targetDir, 'src', 'services', kafkaServiceFileName), content);
|
|
144
|
+
await fs.remove(kafkaServiceTemplate);
|
|
145
|
+
}
|
|
146
|
+
|
|
128
147
|
if (architecture === 'Clean Architecture') {
|
|
129
148
|
// Clean Architecture Restructuring
|
|
130
149
|
await fs.ensureDir(path.join(targetDir, 'src/infrastructure/messaging'));
|
|
@@ -239,8 +258,10 @@ export const generateProject = async (config) => {
|
|
|
239
258
|
// MVC TS
|
|
240
259
|
const swaggerMvcTs = path.join(targetDir, 'src', 'config', 'swagger.ts.ejs');
|
|
241
260
|
if (await fs.pathExists(swaggerMvcTs)) {
|
|
242
|
-
|
|
243
|
-
|
|
261
|
+
if (communication === 'REST APIs') {
|
|
262
|
+
const content = ejs.render(await fs.readFile(swaggerMvcTs, 'utf-8'), { communication });
|
|
263
|
+
await fs.writeFile(path.join(targetDir, 'src', 'config', 'swagger.ts'), content);
|
|
264
|
+
}
|
|
244
265
|
await fs.remove(swaggerMvcTs);
|
|
245
266
|
}
|
|
246
267
|
// Clean Architecture TS
|
|
@@ -248,8 +269,10 @@ export const generateProject = async (config) => {
|
|
|
248
269
|
// Note: In Clean Arch, it might be in src/infrastructure/webserver or src/config depending on refactor.
|
|
249
270
|
// Based on previous moves, we saw it in src/config for TS.
|
|
250
271
|
if (await fs.pathExists(swaggerCleanTs)) {
|
|
251
|
-
|
|
252
|
-
|
|
272
|
+
if (communication === 'REST APIs') {
|
|
273
|
+
const content = ejs.render(await fs.readFile(swaggerCleanTs, 'utf-8'), { communication });
|
|
274
|
+
await fs.writeFile(path.join(targetDir, 'src', 'config', 'swagger.ts'), content);
|
|
275
|
+
}
|
|
253
276
|
await fs.remove(swaggerCleanTs);
|
|
254
277
|
}
|
|
255
278
|
|
|
@@ -275,9 +298,16 @@ export const generateProject = async (config) => {
|
|
|
275
298
|
await fs.writeFile(path.join(targetDir, 'tests', testFileName), healthTestContent);
|
|
276
299
|
|
|
277
300
|
// 11. Copy CI/CD Config (Optional)
|
|
278
|
-
|
|
301
|
+
// 11. Copy CI/CD Config
|
|
302
|
+
if (ciProvider === 'GitHub Actions') {
|
|
279
303
|
await fs.ensureDir(path.join(targetDir, '.github/workflows'));
|
|
280
304
|
await fs.copy(path.join(templatesDir, 'common', '_github/workflows/ci.yml'), path.join(targetDir, '.github/workflows/ci.yml'));
|
|
305
|
+
} else if (ciProvider === 'Jenkins') {
|
|
306
|
+
const jenkinsTemplate = await fs.readFile(path.join(templatesDir, 'common', 'Jenkinsfile.ejs'), 'utf-8');
|
|
307
|
+
const jenkinsContent = ejs.render(jenkinsTemplate, {
|
|
308
|
+
projectName
|
|
309
|
+
});
|
|
310
|
+
await fs.writeFile(path.join(targetDir, 'Jenkinsfile'), jenkinsContent);
|
|
281
311
|
}
|
|
282
312
|
|
|
283
313
|
console.log(`
|
|
@@ -299,7 +329,7 @@ export const generateProject = async (config) => {
|
|
|
299
329
|
✅ Security: Helmet, CORS, Rate-Limiting added
|
|
300
330
|
✅ Testing: Jest setup for Unit/Integration tests
|
|
301
331
|
✅ Docker: Production-ready multi-stage build
|
|
302
|
-
${
|
|
332
|
+
${ciProvider !== 'None' ? `✅ CI/CD: ${ciProvider} Workflow ready` : '❌ CI/CD: Skipped (User preferred)'}
|
|
303
333
|
|
|
304
334
|
----------------------------------------------------
|
|
305
335
|
👉 Next Steps:
|
package/lib/prompts.js
CHANGED
|
@@ -63,11 +63,12 @@ export const getProjectDetails = async (options = {}) => {
|
|
|
63
63
|
when: !options.communication
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
|
-
type: '
|
|
67
|
-
name: '
|
|
68
|
-
message: '
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
type: 'list',
|
|
67
|
+
name: 'ciProvider',
|
|
68
|
+
message: 'Select CI/CD Provider:',
|
|
69
|
+
choices: ['None', 'GitHub Actions', 'Jenkins'],
|
|
70
|
+
default: 'None',
|
|
71
|
+
when: !options.ciProvider
|
|
71
72
|
}
|
|
72
73
|
];
|
|
73
74
|
|
package/package.json
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "nodejs-quickstart-structure",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"description": "A CLI to scaffold Node.js microservices with MVC or Clean Architecture",
|
|
6
|
-
"main": "bin/index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"nodejs-quickstart": "./bin/index.js"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"test": "echo \"Error: no test specified\" && exit 1",
|
|
12
|
-
"test:e2e": "npm run test:e2e:windows",
|
|
13
|
-
"test:e2e:windows": "node scripts/validate-windows.js",
|
|
14
|
-
"test:e2e:linux": "node scripts/validate-linux.js"
|
|
15
|
-
},
|
|
16
|
-
"keywords": [
|
|
17
|
-
"nodejs",
|
|
18
|
-
"cli",
|
|
19
|
-
"scaffold",
|
|
20
|
-
"mvc",
|
|
21
|
-
"clean-architecture"
|
|
22
|
-
],
|
|
23
|
-
"author": "Pau Dang <
|
|
24
|
-
"repository": {
|
|
25
|
-
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/paudang/nodejs-quickstart-structure.git"
|
|
27
|
-
},
|
|
28
|
-
"bugs": {
|
|
29
|
-
"url": "https://github.com/paudang/nodejs-quickstart-structure/issues"
|
|
30
|
-
},
|
|
31
|
-
"homepage": "https://github.com/paudang/nodejs-quickstart-structure#readme",
|
|
32
|
-
"license": "ISC",
|
|
33
|
-
"dependencies": {
|
|
34
|
-
"chalk": "^5.4.1",
|
|
35
|
-
"commander": "^13.1.0",
|
|
36
|
-
"ejs": "^3.1.10",
|
|
37
|
-
"fs-extra": "^11.3.0",
|
|
38
|
-
"inquirer": "^12.4.1"
|
|
39
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "nodejs-quickstart-structure",
|
|
3
|
+
"version": "1.3.5",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A CLI to scaffold Node.js microservices with MVC or Clean Architecture",
|
|
6
|
+
"main": "bin/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nodejs-quickstart": "./bin/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
12
|
+
"test:e2e": "npm run test:e2e:windows",
|
|
13
|
+
"test:e2e:windows": "node scripts/validate-windows.js",
|
|
14
|
+
"test:e2e:linux": "node scripts/validate-linux.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"nodejs",
|
|
18
|
+
"cli",
|
|
19
|
+
"scaffold",
|
|
20
|
+
"mvc",
|
|
21
|
+
"clean-architecture"
|
|
22
|
+
],
|
|
23
|
+
"author": "Pau Dang <phucdangb1400718@gmail.com>",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/paudang/nodejs-quickstart-structure.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/paudang/nodejs-quickstart-structure/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/paudang/nodejs-quickstart-structure#readme",
|
|
32
|
+
"license": "ISC",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"chalk": "^5.4.1",
|
|
35
|
+
"commander": "^13.1.0",
|
|
36
|
+
"ejs": "^3.1.10",
|
|
37
|
+
"fs-extra": "^11.3.0",
|
|
38
|
+
"inquirer": "^12.4.1"
|
|
39
|
+
}
|
|
40
40
|
}
|
|
@@ -26,10 +26,10 @@ const syncDatabase = async () => {
|
|
|
26
26
|
});
|
|
27
27
|
<% } -%>
|
|
28
28
|
break;
|
|
29
|
-
} catch (
|
|
30
|
-
logger.error('
|
|
29
|
+
} catch (error) {
|
|
30
|
+
logger.error('Error syncing database:', error);
|
|
31
31
|
retries -= 1;
|
|
32
|
-
logger.info(`Retries left: ${retries}
|
|
32
|
+
logger.info(`Retries left: ${retries}`);
|
|
33
33
|
await new Promise(res => setTimeout(res, 5000));
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -13,10 +13,10 @@ const logger = winston.createLogger({
|
|
|
13
13
|
],
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
16
|
+
logger.add(new winston.transports.Console({
|
|
17
|
+
format: process.env.NODE_ENV !== 'production'
|
|
18
|
+
? winston.format.simple()
|
|
19
|
+
: winston.format.json(),
|
|
20
|
+
}));
|
|
21
21
|
|
|
22
22
|
module.exports = logger;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const cors = require('cors');
|
|
3
3
|
require('dotenv').config();
|
|
4
|
-
|
|
5
|
-
<% if (communication === 'REST APIs') { %>
|
|
4
|
+
const logger = require('../log/logger');
|
|
5
|
+
<% if (communication === 'REST APIs') { %>const apiRoutes = require('../../interfaces/routes/api');<% } -%>
|
|
6
|
+
<% if (communication === 'REST APIs') { -%>
|
|
6
7
|
const swaggerUi = require('swagger-ui-express');
|
|
7
8
|
const swaggerSpecs = require('./swagger');
|
|
8
|
-
<% }
|
|
9
|
+
<% } -%>
|
|
9
10
|
|
|
10
11
|
const startServer = (port) => {
|
|
11
12
|
const app = express();
|
|
@@ -13,18 +14,20 @@ const startServer = (port) => {
|
|
|
13
14
|
app.use(cors());
|
|
14
15
|
app.use(express.json());
|
|
15
16
|
|
|
16
|
-
<% if (communication === 'REST APIs') {
|
|
17
|
+
<% if (communication === 'REST APIs') { -%>
|
|
18
|
+
app.use('/api', apiRoutes);
|
|
19
|
+
<% } -%>
|
|
17
20
|
|
|
18
|
-
<% if (communication === 'REST APIs') {
|
|
21
|
+
<% if (communication === 'REST APIs') { -%>
|
|
19
22
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
|
|
20
|
-
<% }
|
|
23
|
+
<% } -%>
|
|
21
24
|
|
|
22
25
|
app.get('/health', (req, res) => {
|
|
23
26
|
res.json({ status: 'UP' });
|
|
24
27
|
});
|
|
25
28
|
|
|
26
29
|
app.listen(port, () => {
|
|
27
|
-
|
|
30
|
+
logger.info(`Server running on port ${port}`);
|
|
28
31
|
});
|
|
29
32
|
};
|
|
30
33
|
|
|
@@ -2,6 +2,7 @@ const CreateUser = require('../../usecases/CreateUser');
|
|
|
2
2
|
const GetAllUsers = require('../../usecases/GetAllUsers');
|
|
3
3
|
const UserRepository = require('../../infrastructure/repositories/UserRepository');
|
|
4
4
|
const HTTP_STATUS = require('../../utils/httpCodes');
|
|
5
|
+
const logger = require('../../infrastructure/log/logger');
|
|
5
6
|
|
|
6
7
|
class UserController {
|
|
7
8
|
constructor() {
|
|
@@ -13,7 +14,10 @@ class UserController {
|
|
|
13
14
|
getUsers(req, res) {
|
|
14
15
|
this.getAllUsersUseCase.execute()
|
|
15
16
|
.then(users => res.json(users))
|
|
16
|
-
.catch(err =>
|
|
17
|
+
.catch(err => {
|
|
18
|
+
logger.error('Error getting users:', err);
|
|
19
|
+
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: err.message });
|
|
20
|
+
});
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
async createUser(req, res) {
|
|
@@ -22,6 +26,7 @@ class UserController {
|
|
|
22
26
|
const user = await this.createUserUseCase.execute(name, email);
|
|
23
27
|
res.status(HTTP_STATUS.CREATED).json(user);
|
|
24
28
|
} catch (error) {
|
|
29
|
+
logger.error('Error creating user:', error);
|
|
25
30
|
res.status(HTTP_STATUS.BAD_REQUEST).json({ error: error.message });
|
|
26
31
|
}
|
|
27
32
|
}
|
|
@@ -5,11 +5,11 @@ import hpp from 'hpp';
|
|
|
5
5
|
import rateLimit from 'express-rate-limit';
|
|
6
6
|
import dotenv from 'dotenv';
|
|
7
7
|
import logger from './infrastructure/log/logger';
|
|
8
|
-
<% if (communication === 'REST APIs') { %>import userRoutes from './interfaces/routes/userRoutes';<% }
|
|
9
|
-
<% if (communication === 'REST APIs') {
|
|
8
|
+
<% if (communication === 'REST APIs') { %>import userRoutes from './interfaces/routes/userRoutes';<% } -%>
|
|
9
|
+
<% if (communication === 'REST APIs') { -%>
|
|
10
10
|
import swaggerUi from 'swagger-ui-express';
|
|
11
|
-
import swaggerSpecs from './config/swagger';<% }
|
|
12
|
-
<% if (communication === 'Kafka') { %>import { KafkaService } from './infrastructure/messaging/kafkaClient';<% }
|
|
11
|
+
import swaggerSpecs from './config/swagger';<% } -%>
|
|
12
|
+
<% if (communication === 'Kafka') { %>import { KafkaService } from './infrastructure/messaging/kafkaClient';<% } -%>
|
|
13
13
|
|
|
14
14
|
dotenv.config();
|
|
15
15
|
|
|
@@ -26,10 +26,12 @@ app.use(limiter);
|
|
|
26
26
|
app.use(express.json());
|
|
27
27
|
|
|
28
28
|
<% if (communication === 'REST APIs') { -%>
|
|
29
|
-
app.use('/api/users', userRoutes)
|
|
29
|
+
app.use('/api/users', userRoutes);
|
|
30
|
+
<% } -%>
|
|
30
31
|
|
|
31
|
-
<% if (communication === 'REST APIs') {
|
|
32
|
-
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs))
|
|
32
|
+
<% if (communication === 'REST APIs') { -%>
|
|
33
|
+
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
|
|
34
|
+
<% } -%>
|
|
33
35
|
|
|
34
36
|
app.get('/health', (req: Request, res: Response) => {
|
|
35
37
|
res.json({ status: 'UP' });
|
|
@@ -46,7 +48,7 @@ const syncDatabase = async () => {
|
|
|
46
48
|
|
|
47
49
|
app.listen(port, async () => {
|
|
48
50
|
logger.info(`Server running on port ${port}`);
|
|
49
|
-
|
|
51
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
50
52
|
try {
|
|
51
53
|
const kafkaService = new KafkaService();
|
|
52
54
|
await kafkaService.connect();
|
|
@@ -56,13 +58,13 @@ const syncDatabase = async () => {
|
|
|
56
58
|
} catch (err) {
|
|
57
59
|
logger.error('Failed to connect to Kafka:', err);
|
|
58
60
|
}
|
|
59
|
-
|
|
61
|
+
<%_ } -%>
|
|
60
62
|
});
|
|
61
63
|
break;
|
|
62
|
-
} catch (
|
|
63
|
-
logger.error('
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logger.error('Error syncing database:', error);
|
|
64
66
|
retries -= 1;
|
|
65
|
-
logger.info(`Retries left: ${retries}
|
|
67
|
+
logger.info(`Retries left: ${retries}`);
|
|
66
68
|
await new Promise(res => setTimeout(res, 5000));
|
|
67
69
|
}
|
|
68
70
|
}
|
|
@@ -13,10 +13,10 @@ const logger = winston.createLogger({
|
|
|
13
13
|
],
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
16
|
+
logger.add(new winston.transports.Console({
|
|
17
|
+
format: process.env.NODE_ENV !== 'production'
|
|
18
|
+
? winston.format.simple()
|
|
19
|
+
: winston.format.json(),
|
|
20
|
+
}));
|
|
21
21
|
|
|
22
22
|
export default logger;
|
|
@@ -3,6 +3,7 @@ import { UserRepository } from '../../infrastructure/repositories/UserRepository
|
|
|
3
3
|
import CreateUser from '../../usecases/createUser';
|
|
4
4
|
import GetAllUsers from '../../usecases/getAllUsers';
|
|
5
5
|
import { HTTP_STATUS } from '../../utils/httpCodes';
|
|
6
|
+
import logger from '../../infrastructure/log/logger';
|
|
6
7
|
|
|
7
8
|
export class UserController {
|
|
8
9
|
private createUserUseCase: CreateUser;
|
|
@@ -20,7 +21,7 @@ export class UserController {
|
|
|
20
21
|
const user = await this.createUserUseCase.execute(name, email);
|
|
21
22
|
res.status(HTTP_STATUS.CREATED).json(user);
|
|
22
23
|
} catch (error) {
|
|
23
|
-
|
|
24
|
+
logger.error('UserController Error:', error);
|
|
24
25
|
if (error instanceof Error) {
|
|
25
26
|
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: error.message });
|
|
26
27
|
} else {
|
|
@@ -34,7 +35,7 @@ export class UserController {
|
|
|
34
35
|
const users = await this.getAllUsersUseCase.execute();
|
|
35
36
|
res.json(users);
|
|
36
37
|
} catch (error) {
|
|
37
|
-
|
|
38
|
+
logger.error('UserController Error:', error);
|
|
38
39
|
if (error instanceof Error) {
|
|
39
40
|
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: error.message });
|
|
40
41
|
} else {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
pipeline {
|
|
2
|
+
agent any
|
|
3
|
+
|
|
4
|
+
environment {
|
|
5
|
+
CI = 'true'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
stages {
|
|
9
|
+
stage('Install Dependencies') {
|
|
10
|
+
steps {
|
|
11
|
+
// Use npm ci for clean install if package-lock.json exists, else npm install
|
|
12
|
+
sh 'if [ -f package-lock.json ]; then npm ci; else npm install; fi'
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
stage('Lint') {
|
|
17
|
+
steps {
|
|
18
|
+
sh 'npm run lint'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
stage('Test') {
|
|
23
|
+
steps {
|
|
24
|
+
sh 'npm test'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
post {
|
|
30
|
+
always {
|
|
31
|
+
// Clean up workspace
|
|
32
|
+
cleanWs()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -16,6 +16,23 @@ This project comes pre-configured with industry-standard tooling for **Code Qual
|
|
|
16
16
|
- **Testing**: Jest (Unit & Integration).
|
|
17
17
|
- **DevOps**: Multi-stage Docker build, CI/CD ready.
|
|
18
18
|
|
|
19
|
+
## 🔄 CI/CD Pipeline
|
|
20
|
+
<% if (ciProvider === 'GitHub Actions') { -%>
|
|
21
|
+
This project includes a **GitHub Actions** workflow located in `.github/workflows/ci.yml`.
|
|
22
|
+
It automatically runs:
|
|
23
|
+
- Linting
|
|
24
|
+
- Tests
|
|
25
|
+
- Builds
|
|
26
|
+
<% } else if (ciProvider === 'Jenkins') { -%>
|
|
27
|
+
This project includes a **Jenkinsfile** for comprehensive CI/CD.
|
|
28
|
+
Pipeline stages:
|
|
29
|
+
- Install Dependencies
|
|
30
|
+
- Lint
|
|
31
|
+
- Test
|
|
32
|
+
<% } else { -%>
|
|
33
|
+
CI/CD is not currently configured, but the project is ready for integration.
|
|
34
|
+
<% } -%>
|
|
35
|
+
|
|
19
36
|
## 🛠️ Getting Started
|
|
20
37
|
|
|
21
38
|
### 1. Prerequisites
|
|
@@ -24,6 +41,9 @@ This project comes pre-configured with industry-standard tooling for **Code Qual
|
|
|
24
41
|
|
|
25
42
|
### 2. Quick Start
|
|
26
43
|
```bash
|
|
44
|
+
# Initialize Git (Required for Husky)
|
|
45
|
+
git init
|
|
46
|
+
|
|
27
47
|
# Install dependencies
|
|
28
48
|
npm install
|
|
29
49
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { kafka } = require('../config/kafka');
|
|
2
|
+
const logger = require('<%= loggerPath %>');
|
|
2
3
|
|
|
3
4
|
const producer = kafka.producer();
|
|
4
5
|
const consumer = kafka.consumer({ groupId: 'test-group' });
|
|
@@ -10,7 +11,7 @@ const connectKafka = async () => {
|
|
|
10
11
|
|
|
11
12
|
await consumer.run({
|
|
12
13
|
eachMessage: async ({ topic, partition, message }) => {
|
|
13
|
-
|
|
14
|
+
logger.info({
|
|
14
15
|
value: message.value.toString(),
|
|
15
16
|
});
|
|
16
17
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { kafka } from '../config/kafka';
|
|
2
2
|
import { EachMessagePayload, Producer, Consumer } from 'kafkajs';
|
|
3
|
+
import logger from '<%= loggerPath %>';
|
|
3
4
|
|
|
4
5
|
export class KafkaService {
|
|
5
6
|
private producer: Producer;
|
|
@@ -17,7 +18,7 @@ export class KafkaService {
|
|
|
17
18
|
|
|
18
19
|
await this.consumer.run({
|
|
19
20
|
eachMessage: async ({ topic, partition, message }: EachMessagePayload) => {
|
|
20
|
-
|
|
21
|
+
logger.info({
|
|
21
22
|
value: message.value?.toString(),
|
|
22
23
|
});
|
|
23
24
|
},
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
const User = require('../models/User');
|
|
2
2
|
const HTTP_STATUS = require('../utils/httpCodes');
|
|
3
|
+
const logger = require('../utils/logger');
|
|
3
4
|
|
|
4
5
|
const getUsers = async (req, res) => {
|
|
5
6
|
try {
|
|
6
7
|
const users = await User.findAll();
|
|
7
8
|
res.json(users);
|
|
8
9
|
} catch (error) {
|
|
9
|
-
|
|
10
|
+
logger.error('Error fetching users:', error);
|
|
11
|
+
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: 'Internal Server Error' });
|
|
10
12
|
}
|
|
11
13
|
};
|
|
12
14
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
2
|
const cors = require('cors');
|
|
3
3
|
require('dotenv').config();
|
|
4
|
-
<% if (communication === 'REST APIs' || (viewEngine && viewEngine !== 'None')) { %>const apiRoutes = require('./routes/api');<% }
|
|
5
|
-
<% if (communication === 'Kafka') { %>const { connectKafka, sendMessage } = require('./services/kafkaService');<% }
|
|
6
|
-
<% if (communication === 'REST APIs') {
|
|
4
|
+
<% if (communication === 'REST APIs' || (viewEngine && viewEngine !== 'None')) { %>const apiRoutes = require('./routes/api');<% } -%>
|
|
5
|
+
<% if (communication === 'Kafka') { %>const { connectKafka, sendMessage } = require('./services/kafkaService');<% } -%>
|
|
6
|
+
<% if (communication === 'REST APIs') { -%>
|
|
7
7
|
const swaggerUi = require('swagger-ui-express');
|
|
8
8
|
const swaggerSpecs = require('./config/swagger');
|
|
9
|
-
<% }
|
|
9
|
+
<% } -%>
|
|
10
10
|
|
|
11
11
|
const app = express();
|
|
12
12
|
const PORT = process.env.PORT || 3000;
|
|
@@ -15,11 +15,11 @@ const logger = require('./utils/logger');
|
|
|
15
15
|
app.use(cors());
|
|
16
16
|
app.use(express.json());
|
|
17
17
|
|
|
18
|
-
<% if (communication === 'REST APIs') {
|
|
18
|
+
<% if (communication === 'REST APIs') { -%>
|
|
19
19
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
|
|
20
|
-
<% }
|
|
20
|
+
<% } -%>
|
|
21
21
|
|
|
22
|
-
<% if (viewEngine === 'EJS' || viewEngine === 'Pug') {
|
|
22
|
+
<% if (viewEngine === 'EJS' || viewEngine === 'Pug') { -%>
|
|
23
23
|
// View Engine Setup
|
|
24
24
|
const path = require('path');
|
|
25
25
|
app.set('views', path.join(__dirname, 'views'));
|
|
@@ -57,7 +57,7 @@ const syncDatabase = async () => {
|
|
|
57
57
|
// Start Server after DB is ready
|
|
58
58
|
app.listen(PORT, async () => {
|
|
59
59
|
logger.info(`Server running on port ${PORT}`);
|
|
60
|
-
|
|
60
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
61
61
|
try {
|
|
62
62
|
await connectKafka();
|
|
63
63
|
logger.info('Kafka connected');
|
|
@@ -66,7 +66,7 @@ const syncDatabase = async () => {
|
|
|
66
66
|
} catch (err) {
|
|
67
67
|
logger.error('Failed to connect to Kafka:', err);
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
<%_ } -%>
|
|
70
70
|
});
|
|
71
71
|
break;
|
|
72
72
|
} catch (err) {
|
|
@@ -21,10 +21,10 @@ const logger = winston.createLogger({
|
|
|
21
21
|
// If we're not in production then log to the `console` with the format:
|
|
22
22
|
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
|
|
23
23
|
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
24
|
+
logger.add(new winston.transports.Console({
|
|
25
|
+
format: process.env.NODE_ENV !== 'production'
|
|
26
|
+
? winston.format.simple()
|
|
27
|
+
: winston.format.json(),
|
|
28
|
+
}));
|
|
29
29
|
|
|
30
30
|
module.exports = logger;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Request, Response } from 'express';
|
|
2
2
|
import User from '../models/User';
|
|
3
3
|
import { HTTP_STATUS } from '../utils/httpCodes';
|
|
4
|
+
import logger from '../utils/logger';
|
|
4
5
|
|
|
5
6
|
export class UserController {
|
|
6
7
|
async getUsers(req: Request, res: Response) {
|
|
@@ -8,6 +9,7 @@ export class UserController {
|
|
|
8
9
|
const users = await User.findAll();
|
|
9
10
|
res.json(users);
|
|
10
11
|
} catch (error) {
|
|
12
|
+
logger.error('Error fetching users:', error);
|
|
11
13
|
if (error instanceof Error) {
|
|
12
14
|
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: error.message });
|
|
13
15
|
} else {
|
|
@@ -22,6 +24,7 @@ export class UserController {
|
|
|
22
24
|
const user = await User.create({ name, email });
|
|
23
25
|
res.status(HTTP_STATUS.CREATED).json(user);
|
|
24
26
|
} catch (error) {
|
|
27
|
+
logger.error('Error creating user:', error);
|
|
25
28
|
if (error instanceof Error) {
|
|
26
29
|
res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json({ error: error.message });
|
|
27
30
|
} else {
|
|
@@ -5,11 +5,11 @@ import hpp from 'hpp';
|
|
|
5
5
|
import rateLimit from 'express-rate-limit';
|
|
6
6
|
import dotenv from 'dotenv';
|
|
7
7
|
import logger from './utils/logger';
|
|
8
|
-
<% if (communication === 'REST APIs' || (viewEngine && viewEngine !== 'None')) { %>import apiRoutes from './routes/api';<% }
|
|
8
|
+
<% if (communication === 'REST APIs' || (viewEngine && viewEngine !== 'None')) { %>import apiRoutes from './routes/api';<% } -%>
|
|
9
9
|
<% if (communication === 'REST APIs') { %>
|
|
10
10
|
import swaggerUi from 'swagger-ui-express';
|
|
11
|
-
import swaggerSpecs from './config/swagger';<% }
|
|
12
|
-
<% if (communication === 'Kafka') { %>import { KafkaService } from './services/kafkaService';<% }
|
|
11
|
+
import swaggerSpecs from './config/swagger';<% } -%>
|
|
12
|
+
<% if (communication === 'Kafka') { %>import { KafkaService } from './services/kafkaService';<% } -%>
|
|
13
13
|
|
|
14
14
|
dotenv.config();
|
|
15
15
|
|
|
@@ -25,18 +25,21 @@ app.use(limiter);
|
|
|
25
25
|
|
|
26
26
|
app.use(express.json());
|
|
27
27
|
|
|
28
|
-
<% if (communication === 'REST APIs') {
|
|
29
|
-
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs))
|
|
28
|
+
<% if (communication === 'REST APIs') { -%>
|
|
29
|
+
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
|
|
30
|
+
<% } -%>
|
|
30
31
|
|
|
31
|
-
<% if (viewEngine === 'EJS' || viewEngine === 'Pug') {
|
|
32
|
+
<% if (viewEngine === 'EJS' || viewEngine === 'Pug') { -%>
|
|
32
33
|
// View Engine Setup
|
|
33
34
|
import path from 'path';
|
|
34
35
|
app.set('views', path.join(__dirname, 'views'));
|
|
35
|
-
app.set('view engine', '<%= viewEngine.toLowerCase() %>')
|
|
36
|
+
app.set('view engine', '<%= viewEngine.toLowerCase() %>');
|
|
37
|
+
<% } -%>
|
|
36
38
|
|
|
37
39
|
// Routes
|
|
38
40
|
<% if (communication === 'REST APIs' || (viewEngine && viewEngine !== 'None')) { -%>
|
|
39
|
-
app.use('/api', apiRoutes)
|
|
41
|
+
app.use('/api', apiRoutes);
|
|
42
|
+
<% } -%>
|
|
40
43
|
<% if (viewEngine && viewEngine !== 'None') { -%>
|
|
41
44
|
app.get('/', (req: Request, res: Response) => {
|
|
42
45
|
res.render('index', {
|
|
@@ -64,7 +67,7 @@ const syncDatabase = async () => {
|
|
|
64
67
|
// Start Server after DB is ready
|
|
65
68
|
app.listen(port, async () => {
|
|
66
69
|
logger.info(`Server running on port ${port}`);
|
|
67
|
-
|
|
70
|
+
<%_ if (communication === 'Kafka') { -%>
|
|
68
71
|
try {
|
|
69
72
|
const kafkaService = new KafkaService();
|
|
70
73
|
await kafkaService.connect();
|
|
@@ -74,13 +77,13 @@ const syncDatabase = async () => {
|
|
|
74
77
|
} catch (err) {
|
|
75
78
|
logger.error('Failed to connect to Kafka:', err);
|
|
76
79
|
}
|
|
77
|
-
|
|
80
|
+
<%_ } -%>
|
|
78
81
|
});
|
|
79
82
|
break;
|
|
80
|
-
} catch (
|
|
81
|
-
logger.error('
|
|
83
|
+
} catch (error) {
|
|
84
|
+
logger.error('Error syncing database:', error);
|
|
82
85
|
retries -= 1;
|
|
83
|
-
logger.info(`Retries left: ${retries}
|
|
86
|
+
logger.info(`Retries left: ${retries}`);
|
|
84
87
|
await new Promise(res => setTimeout(res, 5000));
|
|
85
88
|
}
|
|
86
89
|
}
|
|
@@ -13,10 +13,10 @@ const logger = winston.createLogger({
|
|
|
13
13
|
],
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
16
|
+
logger.add(new winston.transports.Console({
|
|
17
|
+
format: process.env.NODE_ENV !== 'production'
|
|
18
|
+
? winston.format.simple()
|
|
19
|
+
: winston.format.json(),
|
|
20
|
+
}));
|
|
21
21
|
|
|
22
22
|
export default logger;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
exports.getUsers = (req, res) => {
|
|
2
|
-
res.json([
|
|
3
|
-
{ id: 1, name: 'John Doe' },
|
|
4
|
-
{ id: 2, name: 'Jane Doe' }
|
|
5
|
-
]);
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
exports.createUser = (req, res) => {
|
|
9
|
-
const { name, email } = req.body;
|
|
10
|
-
res.status(201).json({
|
|
11
|
-
message: 'User created successfully',
|
|
12
|
-
user: { name, email }
|
|
13
|
-
});
|
|
14
|
-
};
|