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
|
@@ -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
|
|
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/
|
|
10
|
-
|
|
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
|
-
|
|
26
|
+
import { env } from '@/config/env';
|
|
26
27
|
|
|
27
28
|
const app = express();
|
|
28
|
-
const port =
|
|
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.
|
|
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
|
|
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
|
|
93
|
-
app.use('/graphql', expressMiddleware(
|
|
91
|
+
await apolloServer.start();
|
|
92
|
+
app.use('/graphql', expressMiddleware(apolloServer, { context: gqlContext }));
|
|
94
93
|
<%_ } -%>
|
|
95
94
|
app.use(errorMiddleware);
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
kafkaService.
|
|
102
|
-
|
|
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') { -%>
|
package/templates/clean-architecture/ts/src/interfaces/graphql/resolvers/user.resolvers.ts.ejs
CHANGED
|
@@ -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
|
};
|
package/templates/clean-architecture/ts/src/utils/{error.middleware.ts.ejs → errorMiddleware.ts.ejs}
RENAMED
|
@@ -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
|
-
|
|
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)) {
|
|
@@ -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.
|
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
-
|
|
30
|
+
const disconnectKafka = async () => {
|
|
31
|
+
await producer.disconnect();
|
|
32
|
+
await consumer.disconnect();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
module.exports = { connectKafka, sendMessage, disconnectKafka };
|
|
@@ -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'
|
|
3
|
-
const
|
|
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({
|
|
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(
|
|
12
|
-
expect(res.body).
|
|
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
|
});
|