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.
Files changed (43) hide show
  1. package/CHANGELOG.md +22 -3
  2. package/README.md +2 -1
  3. package/bin/index.js +2 -2
  4. package/docs/generatorFlow.md +9 -9
  5. package/lib/generator.js +8 -2
  6. package/lib/modules/app-setup.js +85 -33
  7. package/lib/modules/config-files.js +18 -1
  8. package/lib/modules/kafka-setup.js +4 -39
  9. package/package.json +1 -1
  10. package/templates/clean-architecture/js/src/index.js.ejs +2 -4
  11. package/templates/clean-architecture/js/src/infrastructure/config/env.js.ejs +47 -0
  12. package/templates/clean-architecture/js/src/infrastructure/webserver/{middlewares/error.middleware.js → middleware/errorMiddleware.js} +2 -1
  13. package/templates/clean-architecture/js/src/infrastructure/webserver/server.js.ejs +25 -11
  14. package/templates/clean-architecture/js/src/interfaces/graphql/resolvers/user.resolvers.js.ejs +4 -1
  15. package/templates/clean-architecture/ts/src/config/env.ts.ejs +46 -0
  16. package/templates/clean-architecture/ts/src/index.ts.ejs +23 -22
  17. package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs +4 -1
  18. package/templates/clean-architecture/ts/src/utils/{error.middleware.ts.ejs → errorMiddleware.ts.ejs} +2 -1
  19. package/templates/common/.env.example.ejs +3 -1
  20. package/templates/common/README.md.ejs +30 -0
  21. package/templates/common/caching/js/redisClient.js.ejs +4 -0
  22. package/templates/common/caching/ts/redisClient.ts.ejs +4 -0
  23. package/templates/common/database/js/mongoose.js.ejs +3 -1
  24. package/templates/common/database/ts/mongoose.ts.ejs +3 -1
  25. package/templates/common/docker-compose.yml.ejs +11 -1
  26. package/templates/common/ecosystem.config.js.ejs +40 -0
  27. package/templates/common/health/js/healthRoute.js.ejs +44 -0
  28. package/templates/common/health/ts/healthRoute.ts.ejs +43 -0
  29. package/templates/common/kafka/js/services/kafkaService.js.ejs +6 -1
  30. package/templates/common/kafka/ts/services/kafkaService.ts.ejs +5 -0
  31. package/templates/common/package.json.ejs +3 -1
  32. package/templates/common/shutdown/js/gracefulShutdown.js.ejs +61 -0
  33. package/templates/common/shutdown/ts/gracefulShutdown.ts.ejs +58 -0
  34. package/templates/common/tests/health.test.ts.ejs +16 -6
  35. package/templates/mvc/js/src/config/env.js.ejs +46 -0
  36. package/templates/mvc/js/src/graphql/resolvers/user.resolvers.js.ejs +4 -1
  37. package/templates/mvc/js/src/index.js.ejs +12 -10
  38. package/templates/mvc/js/src/utils/{error.middleware.js → errorMiddleware.js} +2 -1
  39. package/templates/mvc/ts/src/config/env.ts.ejs +45 -0
  40. package/templates/mvc/ts/src/graphql/resolvers/user.resolvers.ts.ejs +4 -1
  41. package/templates/mvc/ts/src/index.ts.ejs +20 -20
  42. package/templates/mvc/ts/src/utils/{error.middleware.ts.ejs → errorMiddleware.ts.ejs} +2 -1
  43. package/templates/clean-architecture/js/src/domain/repositories/UserRepository.js +0 -9
@@ -0,0 +1,46 @@
1
+ import dotenv from 'dotenv';
2
+ import { z } from 'zod';
3
+ import logger from '@/infrastructure/log/logger';
4
+
5
+ if (process.env.NODE_ENV !== 'production') {
6
+ dotenv.config();
7
+ }
8
+
9
+ const envSchema = z.object({
10
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
11
+ PORT: z.string().transform(Number).default('3000'),
12
+ <%_ if (database !== 'None') { -%>
13
+ DB_HOST: z.string(),
14
+ <%_ if (database === 'MySQL') { -%>
15
+ DB_USER: z.string(),
16
+ DB_PASSWORD: z.string(),
17
+ DB_NAME: z.string(),
18
+ DB_PORT: z.string().transform(Number),
19
+ <%_ } else if (database === 'PostgreSQL') { -%>
20
+ DB_USER: z.string(),
21
+ DB_PASSWORD: z.string(),
22
+ DB_NAME: z.string(),
23
+ DB_PORT: z.string().transform(Number),
24
+ <%_ } else if (database === 'MongoDB') { -%>
25
+ DB_NAME: z.string(),
26
+ DB_PORT: z.string().transform(Number),
27
+ <%_ } -%>
28
+ <%_ } -%>
29
+ <%_ if (caching === 'Redis') { -%>
30
+ REDIS_HOST: z.string(),
31
+ REDIS_PORT: z.string().transform(Number),
32
+ REDIS_PASSWORD: z.string().optional(),
33
+ <%_ } -%>
34
+ <%_ if (communication === 'Kafka') { -%>
35
+ KAFKA_BROKER: z.string(),
36
+ <%_ } -%>
37
+ });
38
+
39
+ const _env = envSchema.safeParse(process.env);
40
+
41
+ if (!_env.success) {
42
+ logger.error('❌ Invalid environment variables:', _env.error.format());
43
+ process.exit(1);
44
+ }
45
+
46
+ export const env = _env.data;
@@ -1,14 +1,15 @@
1
- import express, { Request, Response } from 'express';
1
+ import express from 'express';
2
2
  import cors from 'cors';
3
3
  import helmet from 'helmet';
4
4
  import hpp from 'hpp';
5
5
  import rateLimit from 'express-rate-limit';
6
- import dotenv from 'dotenv';
7
6
  import logger from '@/infrastructure/log/logger';
8
7
  import morgan from 'morgan';
9
- import { errorMiddleware } from '@/utils/error.middleware';
10
- <%_ if (communication === 'REST APIs') { -%>import userRoutes from '@/interfaces/routes/userRoutes';<%_ } -%>
8
+ import { errorMiddleware } from '@/utils/errorMiddleware';
9
+ import { setupGracefulShutdown } from '@/utils/gracefulShutdown';
10
+ import healthRoutes from '@/interfaces/routes/healthRoute';
11
11
  <% if (communication === 'REST APIs') { -%>
12
+ import userRoutes from '@/interfaces/routes/userRoutes';
12
13
  import swaggerUi from 'swagger-ui-express';
13
14
  import swaggerSpecs from '@/config/swagger';<% } -%>
14
15
  <%_ if (communication === 'Kafka') { -%>import { KafkaService } from '@/infrastructure/messaging/kafkaClient';<%_ } -%>
@@ -20,12 +21,12 @@ import { unwrapResolverError } from '@apollo/server/errors';
20
21
  import { ApiError } from '@/errors/ApiError';
21
22
  import { typeDefs, resolvers } from '@/interfaces/graphql';
22
23
  import { gqlContext, MyContext } from '@/interfaces/graphql/context';
23
- <% } -%>
24
+ <%_ } -%>
24
25
 
25
- dotenv.config();
26
+ import { env } from '@/config/env';
26
27
 
27
28
  const app = express();
28
- const port = process.env.PORT || 3000;
29
+ const port = env.PORT;
29
30
 
30
31
  // Security Middleware
31
32
  <%_ if (communication === 'GraphQL') { -%>
@@ -57,15 +58,13 @@ app.use('/api/users', userRoutes);
57
58
  <%_ if (communication === 'REST APIs') { -%>
58
59
  app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs));
59
60
  <%_ } -%>
60
- app.get('/health', (req: Request, res: Response) => {
61
- res.json({ status: 'UP' });
62
- });
61
+ app.use('/health', healthRoutes);
63
62
 
64
63
  // Start Server Logic
65
64
  const startServer = async () => {
66
65
  <%_ if (communication === 'GraphQL') { -%>
67
66
  // GraphQL Setup
68
- const server = new ApolloServer<MyContext>({
67
+ const apolloServer = new ApolloServer<MyContext>({
69
68
  typeDefs,
70
69
  resolvers,
71
70
  plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })],
@@ -89,24 +88,26 @@ const startServer = async () => {
89
88
  return formattedError;
90
89
  },
91
90
  });
92
- await server.start();
93
- app.use('/graphql', expressMiddleware(server, { context: gqlContext }));
91
+ await apolloServer.start();
92
+ app.use('/graphql', expressMiddleware(apolloServer, { context: gqlContext }));
94
93
  <%_ } -%>
95
94
  app.use(errorMiddleware);
96
- app.listen(port, () => {
95
+ <%_ if (communication === 'Kafka') { -%>
96
+ const kafkaService = new KafkaService();
97
+ <%_ } -%>
98
+ const server = app.listen(port, () => {
97
99
  logger.info(`Server running on port ${port}`);
98
100
  <%_ if (communication === 'Kafka') { -%>
99
- try {
100
- const kafkaService = new KafkaService();
101
- kafkaService.connect().then(() => {
102
- logger.info('Kafka connected');
103
- kafkaService.sendMessage('test-topic', 'Hello Kafka from Clean Arch TS!');
104
- });
105
- } catch (err) {
101
+ kafkaService.connect().then(() => {
102
+ logger.info('Kafka connected');
103
+ kafkaService.sendMessage('test-topic', 'Hello Kafka from Clean Arch TS!');
104
+ }).catch(err => {
106
105
  logger.error('Failed to connect to Kafka:', err);
107
- }
106
+ });
108
107
  <%_ } -%>
109
108
  });
109
+
110
+ setupGracefulShutdown(server<% if(communication === 'Kafka') { %>, kafkaService<% } %>);
110
111
  };
111
112
 
112
113
  <%_ if (database !== 'None') { -%>
@@ -14,5 +14,8 @@ export const userResolvers = {
14
14
  const user = await userController.createUser({ name, email });
15
15
  return user;
16
16
  }
17
- }
17
+ }<%_ if (database === 'MongoDB') { -%>,
18
+ User: {
19
+ id: (parent: { id?: string; _id?: unknown }) => parent.id || parent._id
20
+ }<%_ } %>
18
21
  };
@@ -3,7 +3,8 @@ import logger from '@/infrastructure/log/logger';
3
3
  import { ApiError } from '@/errors/ApiError';
4
4
  import { HTTP_STATUS } from '@/utils/httpCodes';
5
5
 
6
- export const errorMiddleware = (err: Error, req: Request, res: Response, _: unknown) => {
6
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
7
+ export const errorMiddleware = (err: Error, req: Request, res: Response, next: unknown) => {
7
8
  let error = err;
8
9
 
9
10
  if (!(error instanceof ApiError)) {
@@ -19,7 +19,9 @@ DB_PASSWORD=root
19
19
  DB_NAME=<%= dbName %>
20
20
  <%_ } -%>
21
21
  <%_ if (database === 'MongoDB') { -%>
22
- DB_URI=mongodb://localhost:27017/<%= dbName %>
22
+ DB_HOST=localhost
23
+ DB_PORT=27017
24
+ DB_NAME=<%= dbName %>
23
25
  <%_ } -%>
24
26
  <%_ } -%>
25
27
 
@@ -183,6 +183,36 @@ docker run -p 3000:3000 <%= projectName %>
183
183
  ```
184
184
  <% } -%>
185
185
 
186
+ ## 🚀 PM2 Deployment (VPS/EC2)
187
+ This project is pre-configured for direct deployment to a VPS/EC2 instance using **PM2** (via `ecosystem.config.js`).
188
+ 1. Install dependencies
189
+ ```bash
190
+ npm install
191
+ ```
192
+ 2. **Start Infrastructure (DB, Redis, Kafka, etc.) in the background**
193
+ *(This specifically starts the background services without running the application inside Docker, allowing PM2 to handle it).*
194
+ ```bash
195
+ docker-compose up -d<% if (database !== 'None') { %> db<% } %><% if (caching === 'Redis') { %> redis<% } %><% if (communication === 'Kafka') { %> zookeeper kafka<% } %>
196
+ ```
197
+ 3. **Wait 5-10s** for the database to fully initialize.
198
+ 4. **Deploy the App using PM2 in Cluster Mode**
199
+ ```bash
200
+ <% if (language === 'TypeScript') { %>npm run build
201
+ <% } %>npm run deploy
202
+ ```
203
+ 5. **Check logs**
204
+ ```bash
205
+ npx pm2 logs
206
+ ```
207
+ 6. Stop and remove the PM2 application
208
+ ```bash
209
+ npx pm2 delete <%= projectName %>
210
+ ```
211
+ 7. Stop and remove the Docker infrastructure
212
+ ```bash
213
+ docker-compose down
214
+ ```
215
+
186
216
  ## 🔒 Security Features
187
217
  - **Helmet**: Sets secure HTTP headers.
188
218
  - **CORS**: Configured for cross-origin requests.
@@ -66,6 +66,10 @@ class RedisService {
66
66
  if (data) await this.set(key, data, ttl);
67
67
  return data;
68
68
  }
69
+
70
+ async quit() {
71
+ return await this.client.quit();
72
+ }
69
73
  }
70
74
 
71
75
  module.exports = RedisService.getInstance();
@@ -71,6 +71,10 @@ class RedisService {
71
71
  if (data) await this.set(key, data, ttl);
72
72
  return data;
73
73
  }
74
+
75
+ public async quit(): Promise<'OK'> {
76
+ return await this.client.quit();
77
+ }
74
78
  }
75
79
 
76
80
  export default RedisService.getInstance();
@@ -8,7 +8,9 @@ logger = require('../log/logger');
8
8
  <% } %>
9
9
  const connectDB = async () => {
10
10
  const dbHost = process.env.DB_HOST || 'localhost';
11
- const mongoURI = process.env.MONGO_URI || `mongodb://${dbHost}:27017/<%= dbName %>`;
11
+ const dbPort = process.env.DB_PORT || '27017';
12
+ const dbName = process.env.DB_NAME || '<%= dbName %>';
13
+ const mongoURI = process.env.MONGO_URI || `mongodb://${dbHost}:${dbPort}/${dbName}`;
12
14
 
13
15
  let retries = 5;
14
16
  while (retries) {
@@ -6,7 +6,9 @@ import logger from '@/infrastructure/log/logger';
6
6
  <% } %>
7
7
  const connectDB = async (): Promise<void> => {
8
8
  const dbHost = process.env.DB_HOST || 'localhost';
9
- const mongoURI = process.env.MONGO_URI || `mongodb://${dbHost}:27017/<%= dbName %>`;
9
+ const dbPort = process.env.DB_PORT || '27017';
10
+ const dbName = process.env.DB_NAME || '<%= dbName %>';
11
+ const mongoURI = process.env.MONGO_URI || `mongodb://${dbHost}:${dbPort}/${dbName}`;
10
12
 
11
13
  let retries = 5;
12
14
  while (retries) {
@@ -28,10 +28,15 @@ services:
28
28
  - DB_USER=root
29
29
  - DB_PASSWORD=root
30
30
  - DB_NAME=<%= dbName %>
31
+ - DB_PORT=3306
31
32
  <%_ } -%><%_ if (database === 'PostgreSQL') { -%>
32
33
  - DB_USER=postgres
33
34
  - DB_PASSWORD=root
34
35
  - DB_NAME=<%= dbName %>
36
+ - DB_PORT=5432
37
+ <%_ } -%><%_ if (database === 'MongoDB') { -%>
38
+ - DB_NAME=<%= dbName %>
39
+ - DB_PORT=27017
35
40
  <%_ } -%>
36
41
  <%_ } -%>
37
42
  <%_ } else { -%>
@@ -48,10 +53,15 @@ services:
48
53
  - DB_USER=root
49
54
  - DB_PASSWORD=root
50
55
  - DB_NAME=<%= dbName %>
56
+ - DB_PORT=3306
51
57
  <%_ } -%><%_ if (database === 'PostgreSQL') { -%>
52
58
  - DB_USER=postgres
53
59
  - DB_PASSWORD=root
54
60
  - DB_NAME=<%= dbName %>
61
+ - DB_PORT=5432
62
+ <%_ } -%><%_ if (database === 'MongoDB') { -%>
63
+ - DB_NAME=<%= dbName %>
64
+ - DB_PORT=27017
55
65
  <%_ } -%>
56
66
  <%_ } -%>
57
67
  <%_ } -%>
@@ -89,7 +99,7 @@ services:
89
99
  - mongodb_data:/data/db
90
100
 
91
101
  mongo-migrate:
92
- image: node:18-alpine
102
+ image: node:22-alpine
93
103
  working_dir: /app
94
104
  volumes:
95
105
  - .:/app
@@ -0,0 +1,40 @@
1
+ module.exports = {
2
+ apps: [{
3
+ name: "<%= projectName %>",
4
+ script: "<% if (language === 'TypeScript') { %>./dist/index.js<% } else { %>./src/index.js<% } %>", // Entry point
5
+ instances: "max", // Run in Cluster Mode to utilize all CPUs (Note: On Windows, cluster mode may throw `spawn wmic ENOENT` errors due to missing WMIC in Windows 11. To fix, change instances to 1, or install wmic)
6
+ exec_mode: "cluster",
7
+ watch: false, // Disable watch in production
8
+ max_memory_restart: "1G",
9
+ env_production: {
10
+ NODE_ENV: "production",
11
+ PORT: 3000,
12
+ <%_ if (caching === 'Redis') { -%>
13
+ REDIS_HOST: "127.0.0.1",
14
+ REDIS_PORT: 6379,
15
+ REDIS_PASSWORD: "",
16
+ <%_ } -%>
17
+ <%_ if (communication === 'Kafka') { -%>
18
+ KAFKA_BROKER: "127.0.0.1:9092",
19
+ KAFKAJS_NO_PARTITIONER_WARNING: 1,
20
+ <%_ } -%>
21
+ <%_ if (database !== 'None') { -%>
22
+ DB_HOST: "127.0.0.1",
23
+ <%_ if (database === 'MySQL') { -%>
24
+ DB_USER: "root",
25
+ DB_PASSWORD: "root",
26
+ DB_NAME: "<%= dbName %>",
27
+ DB_PORT: 3306
28
+ <%_ } else if (database === 'PostgreSQL') { -%>
29
+ DB_USER: "postgres",
30
+ DB_PASSWORD: "root",
31
+ DB_NAME: "<%= dbName %>",
32
+ DB_PORT: 5432
33
+ <%_ } else if (database === 'MongoDB') { -%>
34
+ DB_NAME: "<%= dbName %>",
35
+ DB_PORT: 27017
36
+ <%_ } -%>
37
+ <%_ } -%>
38
+ }
39
+ }]
40
+ };
@@ -0,0 +1,44 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const logger = require('<% if (architecture === "MVC") { %>../utils/logger<% } else { %>../../infrastructure/log/logger<% } %>');
4
+ const HTTP_STATUS = require('<% if (architecture === "MVC") { %>../utils/httpCodes<% } else { %>../../utils/httpCodes<% } %>');
5
+
6
+ router.get('/', async (req, res) => {
7
+ const healthData = {
8
+ status: 'UP',
9
+ uptime: process.uptime(),
10
+ memory: process.memoryUsage(),
11
+ database: 'disconnected',
12
+ timestamp: Date.now()
13
+ };
14
+ logger.info('Health Check');
15
+
16
+ <%_ if (database !== 'None') { -%>
17
+ try {
18
+ <%_ if (database === 'MongoDB') { -%>
19
+ const mongoose = require('mongoose');
20
+ if (mongoose.connection.readyState === 1) {
21
+ if (mongoose.connection.db && mongoose.connection.db.admin) {
22
+ await mongoose.connection.db.admin().ping();
23
+ }
24
+ healthData.database = 'connected';
25
+ }
26
+ <%_ } else { -%>
27
+ const sequelize = require('<% if (architecture === "MVC") { %>../config/database<% } else { %>../../infrastructure/database/database<% } %>');
28
+ await sequelize.authenticate();
29
+ healthData.database = 'connected';
30
+ <%_ } -%>
31
+ } catch (err) {
32
+ healthData.database = 'error';
33
+ healthData.status = 'DOWN';
34
+ logger.error('Health Check Database Ping Failed:', err);
35
+ return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json(healthData);
36
+ }
37
+ <%_ } else { -%>
38
+ healthData.database = 'None';
39
+ <%_ } -%>
40
+
41
+ res.status(HTTP_STATUS.OK).json(healthData);
42
+ });
43
+
44
+ module.exports = router;
@@ -0,0 +1,43 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import logger from '<% if (architecture === "MVC") { %>@/utils/logger<% } else { %>@/infrastructure/log/logger<% } %>';
3
+ import { HTTP_STATUS } from '@/utils/httpCodes';
4
+
5
+ const router = Router();
6
+
7
+ router.get('/', async (req: Request, res: Response) => {
8
+ const healthData: Record<string, unknown> = {
9
+ status: 'UP',
10
+ uptime: process.uptime(),
11
+ memory: process.memoryUsage(),
12
+ database: 'disconnected',
13
+ timestamp: Date.now()
14
+ };
15
+ logger.info('Health Check');
16
+
17
+ <%_ if (database !== 'None') { -%>
18
+ try {
19
+ <%_ if (database === 'MongoDB') { -%>
20
+ const mongoose = (await import('mongoose')).default;
21
+ if (mongoose.connection.readyState === 1) {
22
+ await mongoose.connection.db?.admin().ping();
23
+ healthData.database = 'connected';
24
+ }
25
+ <%_ } else { -%>
26
+ const sequelize = (await import('<% if (architecture === "MVC") { %>@/config/database<% } else { %>@/infrastructure/database/database<% } %>')).default;
27
+ await sequelize.authenticate();
28
+ healthData.database = 'connected';
29
+ <%_ } -%>
30
+ } catch (err) {
31
+ healthData.database = 'error';
32
+ healthData.status = 'DOWN';
33
+ logger.error('Health Check Database Ping Failed:', err);
34
+ return res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json(healthData);
35
+ }
36
+ <%_ } else { -%>
37
+ healthData.database = 'None';
38
+ <%_ } -%>
39
+
40
+ res.status(HTTP_STATUS.OK).json(healthData);
41
+ });
42
+
43
+ export default router;
@@ -27,4 +27,9 @@ const sendMessage = async (topic, message) => {
27
27
  });
28
28
  };
29
29
 
30
- module.exports = { connectKafka, sendMessage };
30
+ const disconnectKafka = async () => {
31
+ await producer.disconnect();
32
+ await consumer.disconnect();
33
+ };
34
+
35
+ module.exports = { connectKafka, sendMessage, disconnectKafka };
@@ -33,5 +33,10 @@ export class KafkaService {
33
33
  ],
34
34
  });
35
35
  }
36
+
37
+ async disconnect() {
38
+ await this.producer.disconnect();
39
+ await this.consumer.disconnect();
40
+ }
36
41
  }
37
42
 
@@ -7,6 +7,7 @@
7
7
  "start": "<% if (language === 'TypeScript') { %>node dist/index.js<% } else { %>node src/index.js<% } %>",
8
8
  "dev": "<% if (language === 'TypeScript') { %>nodemon --exec ts-node -r tsconfig-paths/register src/index.ts<% } else { %>nodemon src/index.js<% } %>"<% if (language === 'TypeScript') { %>,
9
9
  "build": "rimraf dist && tsc && tsc-alias<% if (viewEngine && viewEngine !== 'None') { %> && cpx \"src/views/**/*\" dist/views<% } %><% if (communication === 'REST APIs') { %> && cpx \"src/**/*.yml\" dist/<% } %>"<% } %>,
10
+ "deploy": "npx pm2 start ecosystem.config.js --env production",
10
11
  "lint": "eslint .",
11
12
  "lint:fix": "eslint . --fix",
12
13
  "format": "prettier --write .",
@@ -20,6 +21,7 @@
20
21
  "dependencies": {
21
22
  "express": "^4.18.2",
22
23
  "dotenv": "^16.3.1",
24
+ "zod": "^3.22.4",
23
25
  <% if (database === 'MySQL') { %> "mysql2": "^3.6.5",
24
26
  "sequelize": "^6.35.2",
25
27
  <% } -%>
@@ -81,7 +83,7 @@
81
83
  "lint-staged": "^15.4.3"<% if (language === 'TypeScript') { %>,
82
84
  "typescript-eslint": "^8.24.1",<%_ if (communication === 'REST APIs') { %>
83
85
  "@types/swagger-ui-express": "^4.1.6",
84
- "@types/yamljs": "^0.2.34",<%_ } -%>
86
+ "@types/yamljs": "^0.2.34",<%_ } %>
85
87
  "jest": "^29.7.0",
86
88
  "ts-jest": "^29.2.5",
87
89
  "@types/jest": "^29.5.14",
@@ -0,0 +1,61 @@
1
+ <%_
2
+ let loggerPath = './logger';
3
+ let dbPath = '../config/database';
4
+ let redisPath = '../config/redisClient';
5
+ let kafkaPath = '../services/kafkaService';
6
+
7
+ if (architecture === 'Clean Architecture') {
8
+ loggerPath = '../infrastructure/log/logger';
9
+ dbPath = '../infrastructure/database/database';
10
+ redisPath = '../infrastructure/caching/redisClient';
11
+ kafkaPath = '../infrastructure/messaging/kafkaClient';
12
+ }
13
+ _%>
14
+ const logger = require('<%- loggerPath %>');
15
+
16
+ const setupGracefulShutdown = (server) => {
17
+ const gracefulShutdown = async (signal) => {
18
+ logger.info(`Received ${signal}. Shutting down gracefully...`);
19
+ server.close(async () => {
20
+ logger.info('HTTP server closed.');
21
+ try {
22
+ <%_ if (database !== 'None') { -%>
23
+ <%_ if (database === 'MongoDB') { -%>
24
+ const mongoose = require('mongoose');
25
+ await mongoose.connection.close(false);
26
+ logger.info('MongoDB connection closed.');
27
+ <%_ } else { -%>
28
+ const sequelize = require('<%- dbPath %>');
29
+ await sequelize.close();
30
+ logger.info('Database connection closed.');
31
+ <%_ } -%>
32
+ <%_ } -%>
33
+ <%_ if (caching === 'Redis') { -%>
34
+ const redisService = require('<%- redisPath %>');
35
+ await redisService.quit();
36
+ logger.info('Redis connection closed.');
37
+ <%_ } -%>
38
+ <%_ if (communication === 'Kafka') { -%>
39
+ const { disconnectKafka } = require('<%- kafkaPath %>');
40
+ await disconnectKafka();
41
+ logger.info('Kafka connection closed.');
42
+ <%_ } -%>
43
+ logger.info('Graceful shutdown fully completed.');
44
+ process.exit(0);
45
+ } catch (err) {
46
+ logger.error('Error during shutdown:', err);
47
+ process.exit(1);
48
+ }
49
+ });
50
+
51
+ setTimeout(() => {
52
+ logger.error('Could not close connections in time, forcefully shutting down');
53
+ process.exit(1);
54
+ }, 15000);
55
+ };
56
+
57
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
58
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
59
+ };
60
+
61
+ module.exports = setupGracefulShutdown;
@@ -0,0 +1,58 @@
1
+ import { Server } from 'http';
2
+ <%_ if (architecture === 'MVC') { -%>
3
+ import logger from '@/utils/logger';
4
+ <%_ } else { -%>
5
+ import logger from '@/infrastructure/log/logger';
6
+ <%_ } -%>
7
+
8
+ export const setupGracefulShutdown = (server: Server<% if (communication === 'Kafka') { %>, kafkaService: { disconnect: () => Promise<void> }<% } %>) => {
9
+ const gracefulShutdown = async (signal: string) => {
10
+ logger.info(`Received ${signal}. Shutting down gracefully...`);
11
+ server.close(async () => {
12
+ logger.info('HTTP server closed.');
13
+ try {
14
+ <%_ if (database !== 'None') { -%>
15
+ <%_ if (database === 'MongoDB') { -%>
16
+ const mongoose = (await import('mongoose')).default;
17
+ await mongoose.connection.close(false);
18
+ logger.info('MongoDB connection closed.');
19
+ <%_ } else { -%>
20
+ <%_ if (architecture === 'MVC') { -%>
21
+ const sequelize = (await import('@/config/database')).default;
22
+ <%_ } else { -%>
23
+ const sequelize = (await import('@/infrastructure/database/database')).default;
24
+ <%_ } -%>
25
+ await sequelize.close();
26
+ logger.info('Database connection closed.');
27
+ <%_ } -%>
28
+ <%_ } -%>
29
+ <%_ if (caching === 'Redis') { -%>
30
+ <%_ if (architecture === 'MVC') { -%>
31
+ const redisService = (await import('@/config/redisClient')).default;
32
+ <%_ } else { -%>
33
+ const redisService = (await import('@/infrastructure/caching/redisClient')).default;
34
+ <%_ } -%>
35
+ await redisService.quit();
36
+ logger.info('Redis connection closed.');
37
+ <%_ } -%>
38
+ <%_ if (communication === 'Kafka') { -%>
39
+ await kafkaService.disconnect();
40
+ logger.info('Kafka connection closed.');
41
+ <%_ } -%>
42
+ logger.info('Graceful shutdown fully completed.');
43
+ process.exit(0);
44
+ } catch (err) {
45
+ logger.error('Error during shutdown:', err);
46
+ process.exit(1);
47
+ }
48
+ });
49
+
50
+ setTimeout(() => {
51
+ logger.error('Could not close connections in time, forcefully shutting down');
52
+ process.exit(1);
53
+ }, 15000);
54
+ };
55
+
56
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
57
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
58
+ };
@@ -1,14 +1,24 @@
1
1
  <% if (language === 'TypeScript') { %>import request from 'supertest';
2
- import express from 'express';<% } else { %>const request = require('supertest');
3
- const express = require('express');<% } %>
2
+ import express from 'express';
3
+ import { HTTP_STATUS } from '@/utils/httpCodes';<% } else { %>const request = require('supertest');
4
+ const express = require('express');
5
+ const HTTP_STATUS = require('../src/utils/httpCodes');<% } %>
4
6
 
5
7
  const app = express();
6
- app.get('/health', (req, res) => res.json({ status: 'UP' }));
8
+ app.get('/health', (req, res) => res.status(HTTP_STATUS.OK).json({
9
+ status: 'UP',
10
+ uptime: 120,
11
+ memory: { rss: 1024, heapTotal: 512, heapUsed: 256, external: 128 },
12
+ database: 'connected'
13
+ }));
7
14
 
8
15
  describe('Health Check', () => {
9
- it('should return 200 OK', async () => {
16
+ it('should return 200 OK with detailed metrics', async () => {
10
17
  const res = await request(app).get('/health');
11
- expect(res.status).toBe(200);
12
- expect(res.body).toEqual({ status: 'UP' });
18
+ expect(res.status).toBe(HTTP_STATUS.OK);
19
+ expect(res.body).toHaveProperty('status', 'UP');
20
+ expect(res.body).toHaveProperty('uptime');
21
+ expect(res.body).toHaveProperty('memory');
22
+ expect(res.body).toHaveProperty('database');
13
23
  });
14
24
  });