@codeenthusiast09/create-express-app 1.0.2 → 1.0.4
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/package.json +1 -1
- package/templates/boilerplate/{docker/.dockerignore → .dockerignore} +22 -12
- package/templates/boilerplate/.env.docker +7 -0
- package/templates/boilerplate/.eslintrc.cjs +35 -13
- package/templates/boilerplate/docker-compose.yml +86 -0
- package/templates/boilerplate/package.json +4 -0
- package/templates/boilerplate/src/common/middleware/error.middleware.ts +59 -25
- package/templates/boilerplate/src/common/middleware/validation.middleware.ts +7 -6
- package/templates/boilerplate/src/common/utils/http-logger.ts +1 -1
- package/templates/boilerplate/src/common/utils/response-helper.ts +27 -25
- package/templates/boilerplate/src/database/mongoose.connection.ts +30 -23
- package/templates/boilerplate/src/server.ts +71 -20
- package/templates/boilerplate/docker/docker-compose.yml +0 -68
- /package/templates/boilerplate/{docker/Dockerfile → Dockerfile} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codeenthusiast09/create-express-app",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "CLI tool to generate production-ready Express TypeScript projects with flexible database options (MongoDB, PostgreSQL with Prisma or Drizzle)",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -1,46 +1,56 @@
|
|
|
1
1
|
# Dependencies
|
|
2
2
|
node_modules
|
|
3
|
-
npm-debug.log
|
|
4
|
-
|
|
3
|
+
npm-debug.log*
|
|
4
|
+
yarn-debug.log*
|
|
5
|
+
yarn-error.log*
|
|
5
6
|
|
|
6
7
|
# Build output
|
|
7
8
|
dist
|
|
8
9
|
build
|
|
10
|
+
*.tsbuildinfo
|
|
9
11
|
|
|
10
|
-
# Environment
|
|
12
|
+
# Environment
|
|
11
13
|
.env
|
|
12
|
-
.env
|
|
13
|
-
|
|
14
|
-
.env.local
|
|
15
|
-
.env.*.local
|
|
14
|
+
.env.*
|
|
15
|
+
!.env.example
|
|
16
16
|
|
|
17
17
|
# Git
|
|
18
18
|
.git
|
|
19
19
|
.gitignore
|
|
20
|
+
.gitattributes
|
|
20
21
|
|
|
21
22
|
# IDE
|
|
22
23
|
.vscode
|
|
23
24
|
.idea
|
|
24
25
|
*.swp
|
|
25
26
|
*.swo
|
|
27
|
+
*~
|
|
26
28
|
|
|
27
29
|
# Testing
|
|
28
30
|
coverage
|
|
29
31
|
.nyc_output
|
|
32
|
+
*.test.ts
|
|
33
|
+
*.spec.ts
|
|
34
|
+
__tests__
|
|
35
|
+
__mocks__
|
|
30
36
|
|
|
31
37
|
# Documentation
|
|
32
38
|
README.md
|
|
39
|
+
CHANGELOG.md
|
|
40
|
+
LICENSE
|
|
33
41
|
docs
|
|
34
42
|
|
|
35
|
-
# Docker files
|
|
36
|
-
Dockerfile
|
|
37
|
-
docker-compose.yml
|
|
38
|
-
.dockerignore
|
|
39
|
-
|
|
40
43
|
# CI/CD
|
|
41
44
|
.github
|
|
42
45
|
.gitlab-ci.yml
|
|
46
|
+
.travis.yml
|
|
47
|
+
|
|
48
|
+
# Docker
|
|
49
|
+
Dockerfile
|
|
50
|
+
docker-compose.yml
|
|
51
|
+
.dockerignore
|
|
43
52
|
|
|
44
53
|
# Misc
|
|
45
54
|
.DS_Store
|
|
46
55
|
*.log
|
|
56
|
+
|
|
@@ -1,27 +1,49 @@
|
|
|
1
1
|
module.exports = {
|
|
2
|
-
parser:
|
|
2
|
+
parser: '@typescript-eslint/parser',
|
|
3
3
|
parserOptions: {
|
|
4
|
-
project:
|
|
4
|
+
project: 'tsconfig.json',
|
|
5
5
|
tsconfigRootDir: __dirname,
|
|
6
|
-
sourceType:
|
|
6
|
+
sourceType: 'module',
|
|
7
7
|
},
|
|
8
|
-
plugins: [
|
|
8
|
+
plugins: ['@typescript-eslint'],
|
|
9
9
|
extends: [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
'eslint:recommended',
|
|
11
|
+
'plugin:@typescript-eslint/recommended',
|
|
12
|
+
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
|
13
13
|
],
|
|
14
14
|
root: true,
|
|
15
15
|
env: {
|
|
16
16
|
node: true,
|
|
17
17
|
jest: true,
|
|
18
18
|
},
|
|
19
|
-
ignorePatterns: [
|
|
19
|
+
ignorePatterns: ['.eslintrc.js', 'dist', 'node_modules'],
|
|
20
20
|
rules: {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
'@typescript-eslint/interface-name-prefix': 'off',
|
|
22
|
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
23
|
+
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
24
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
25
|
+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
26
|
+
|
|
27
|
+
// Fix for Express async middleware
|
|
28
|
+
'@typescript-eslint/no-misused-promises': [
|
|
29
|
+
'error',
|
|
30
|
+
{
|
|
31
|
+
checksVoidReturn: false,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
|
|
35
|
+
// Additional rules
|
|
36
|
+
'@typescript-eslint/require-await': 'off',
|
|
37
|
+
'@typescript-eslint/no-floating-promises': 'warn',
|
|
38
|
+
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
|
39
|
+
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
|
40
|
+
'@typescript-eslint/no-unsafe-call': 'warn',
|
|
41
|
+
'@typescript-eslint/no-unsafe-return': 'warn',
|
|
42
|
+
'@typescript-eslint/no-unsafe-argument': 'warn',
|
|
43
|
+
|
|
44
|
+
// Code quality
|
|
45
|
+
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
|
46
|
+
'prefer-const': 'error',
|
|
47
|
+
'no-var': 'error',
|
|
26
48
|
},
|
|
27
49
|
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
# MongoDB Service (Development - No Auth)
|
|
5
|
+
mongodb:
|
|
6
|
+
image: mongo:7
|
|
7
|
+
container_name: express-mongodb
|
|
8
|
+
restart: unless-stopped
|
|
9
|
+
ports:
|
|
10
|
+
- '27017:27017'
|
|
11
|
+
volumes:
|
|
12
|
+
- mongodb_data:/data/db
|
|
13
|
+
networks:
|
|
14
|
+
- app-network
|
|
15
|
+
healthcheck:
|
|
16
|
+
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
|
|
17
|
+
interval: 10s
|
|
18
|
+
timeout: 5s
|
|
19
|
+
retries: 5
|
|
20
|
+
|
|
21
|
+
# PostgreSQL Service (Uncomment if needed)
|
|
22
|
+
# postgres:
|
|
23
|
+
# image: postgres:16-alpine
|
|
24
|
+
# container_name: express-postgres
|
|
25
|
+
# restart: unless-stopped
|
|
26
|
+
# ports:
|
|
27
|
+
# - '5432:5432'
|
|
28
|
+
# environment:
|
|
29
|
+
# POSTGRES_USER: ${DB_USER:-postgres}
|
|
30
|
+
# POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
|
31
|
+
# POSTGRES_DB: ${DB_NAME:-myapp}
|
|
32
|
+
# volumes:
|
|
33
|
+
# - postgres_data:/var/lib/postgresql/data
|
|
34
|
+
# networks:
|
|
35
|
+
# - app-network
|
|
36
|
+
# healthcheck:
|
|
37
|
+
# test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
38
|
+
# interval: 10s
|
|
39
|
+
# timeout: 5s
|
|
40
|
+
# retries: 5
|
|
41
|
+
|
|
42
|
+
# Application Service
|
|
43
|
+
app:
|
|
44
|
+
build:
|
|
45
|
+
context: .
|
|
46
|
+
dockerfile: Dockerfile
|
|
47
|
+
target: production
|
|
48
|
+
container_name: express-app
|
|
49
|
+
restart: unless-stopped
|
|
50
|
+
ports:
|
|
51
|
+
- '${PORT:-3000}:${PORT:-3000}'
|
|
52
|
+
environment:
|
|
53
|
+
NODE_ENV: ${NODE_ENV:-production}
|
|
54
|
+
PORT: ${PORT:-3000}
|
|
55
|
+
DATABASE_URL: ${DATABASE_URL:-mongodb://mongodb:27017/myapp}
|
|
56
|
+
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required}
|
|
57
|
+
JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-7d}
|
|
58
|
+
LOG_LEVEL: ${LOG_LEVEL:-info}
|
|
59
|
+
CORS_ORIGIN: ${CORS_ORIGIN:-*}
|
|
60
|
+
depends_on:
|
|
61
|
+
mongodb:
|
|
62
|
+
condition: service_healthy
|
|
63
|
+
# postgres:
|
|
64
|
+
# condition: service_healthy
|
|
65
|
+
networks:
|
|
66
|
+
- app-network
|
|
67
|
+
healthcheck:
|
|
68
|
+
test:
|
|
69
|
+
[
|
|
70
|
+
'CMD',
|
|
71
|
+
'node',
|
|
72
|
+
'-e',
|
|
73
|
+
"require('http').get('http://localhost:${PORT:-3000}/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))",
|
|
74
|
+
]
|
|
75
|
+
interval: 30s
|
|
76
|
+
timeout: 3s
|
|
77
|
+
retries: 3
|
|
78
|
+
start_period: 40s
|
|
79
|
+
|
|
80
|
+
networks:
|
|
81
|
+
app-network:
|
|
82
|
+
driver: bridge
|
|
83
|
+
|
|
84
|
+
volumes:
|
|
85
|
+
mongodb_data:
|
|
86
|
+
# postgres_data:
|
|
@@ -28,9 +28,12 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"bcryptjs": "^2.4.3",
|
|
31
|
+
"compression": "^1.7.4",
|
|
31
32
|
"cors": "^2.8.5",
|
|
32
33
|
"dotenv": "^16.4.5",
|
|
33
34
|
"express": "^4.18.2",
|
|
35
|
+
"express-rate-limit": "^7.1.5",
|
|
36
|
+
"helmet": "^7.1.0",
|
|
34
37
|
"jsonwebtoken": "^9.0.2",
|
|
35
38
|
"pino": "^8.19.0",
|
|
36
39
|
"pino-http": "^9.0.0",
|
|
@@ -39,6 +42,7 @@
|
|
|
39
42
|
},
|
|
40
43
|
"devDependencies": {
|
|
41
44
|
"@types/bcryptjs": "^2.4.6",
|
|
45
|
+
"@types/compression": "^1.7.5",
|
|
42
46
|
"@types/cors": "^2.8.17",
|
|
43
47
|
"@types/express": "^4.17.21",
|
|
44
48
|
"@types/jest": "^29.5.12",
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Request, Response, NextFunction } from 'express';
|
|
1
|
+
import { Request, Response, NextFunction, RequestHandler } from 'express';
|
|
2
|
+
import { ParamsDictionary } from 'express-serve-static-core';
|
|
3
|
+
import { ParsedQs } from 'qs';
|
|
2
4
|
import { ZodError } from 'zod';
|
|
3
5
|
import { logger } from '@/common/utils/logger';
|
|
4
6
|
import { config } from '@/config';
|
|
@@ -39,18 +41,23 @@ interface MongoError extends Error {
|
|
|
39
41
|
* Catches all errors and sends appropriate responses.
|
|
40
42
|
* MUST be registered LAST in middleware chain.
|
|
41
43
|
*/
|
|
42
|
-
export const errorHandler = (
|
|
44
|
+
export const errorHandler = (
|
|
45
|
+
err: Error,
|
|
46
|
+
req: Request,
|
|
47
|
+
res: Response,
|
|
48
|
+
_next: NextFunction,
|
|
49
|
+
): void => {
|
|
43
50
|
// Log the error
|
|
44
51
|
logger.error({ err, req: { method: req.method, url: req.url } });
|
|
45
52
|
|
|
46
53
|
// Handle custom AppError
|
|
47
54
|
if (err instanceof AppError) {
|
|
48
|
-
// Client errors (4xx)
|
|
49
55
|
if (err.statusCode < 500) {
|
|
50
|
-
|
|
56
|
+
ResponseHelper.fail(res, err.code, err.message, err.statusCode);
|
|
57
|
+
return;
|
|
51
58
|
}
|
|
52
|
-
|
|
53
|
-
return
|
|
59
|
+
ResponseHelper.error(res, err.code, err.message, err.statusCode, err);
|
|
60
|
+
return;
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
// Handle Zod validation errors
|
|
@@ -60,32 +67,38 @@ export const errorHandler = (err: Error, req: Request, res: Response, _next: Nex
|
|
|
60
67
|
message: error.message,
|
|
61
68
|
code: error.code,
|
|
62
69
|
}));
|
|
63
|
-
|
|
70
|
+
ResponseHelper.validationError(res, details);
|
|
71
|
+
return;
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
// Handle JWT errors
|
|
67
75
|
if (err.name === 'JsonWebTokenError') {
|
|
68
|
-
|
|
76
|
+
ResponseHelper.unauthorized(res, 'Invalid token');
|
|
77
|
+
return;
|
|
69
78
|
}
|
|
70
79
|
if (err.name === 'TokenExpiredError') {
|
|
71
|
-
|
|
80
|
+
ResponseHelper.unauthorized(res, 'Token expired');
|
|
81
|
+
return;
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
// Handle Mongoose errors
|
|
75
85
|
if (err.name === 'ValidationError') {
|
|
76
|
-
|
|
86
|
+
ResponseHelper.badRequest(res, 'Validation failed');
|
|
87
|
+
return;
|
|
77
88
|
}
|
|
78
89
|
if (err.name === 'CastError') {
|
|
79
|
-
|
|
90
|
+
ResponseHelper.badRequest(res, 'Invalid ID format');
|
|
91
|
+
return;
|
|
80
92
|
}
|
|
81
93
|
const mongoError = err as MongoError;
|
|
82
94
|
if (mongoError.code === 11000) {
|
|
83
95
|
const field = mongoError.keyPattern ? Object.keys(mongoError.keyPattern)[0] : 'field';
|
|
84
|
-
|
|
96
|
+
ResponseHelper.conflict(res, `Duplicate value for ${field ?? 'unknown field'}`);
|
|
97
|
+
return;
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
// Default: Unknown error
|
|
88
|
-
|
|
101
|
+
ResponseHelper.internalError(
|
|
89
102
|
res,
|
|
90
103
|
config.nodeEnv === 'development' ? err.message : 'An unexpected error occurred',
|
|
91
104
|
err,
|
|
@@ -93,20 +106,41 @@ export const errorHandler = (err: Error, req: Request, res: Response, _next: Nex
|
|
|
93
106
|
};
|
|
94
107
|
|
|
95
108
|
/**
|
|
96
|
-
* Async Handler Wrapper
|
|
109
|
+
* Async Handler Wrapper with Generic Type Support
|
|
97
110
|
*
|
|
98
111
|
* Wraps async route handlers to catch errors automatically.
|
|
112
|
+
* Supports typed Request parameters for type-safe controllers.
|
|
99
113
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
114
|
+
* @example
|
|
115
|
+
* // Simple usage
|
|
116
|
+
* router.get('/users', asyncHandler(async (req, res) => {
|
|
117
|
+
* const users = await userService.findAll();
|
|
118
|
+
* ResponseHelper.success(res, users);
|
|
119
|
+
* }));
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* // With typed params
|
|
123
|
+
* router.get('/users/:id', asyncHandler<IdParam>(async (req, res) => {
|
|
124
|
+
* const { id } = req.params; // TypeScript knows this is string
|
|
125
|
+
* const user = await userService.findById(id);
|
|
126
|
+
* ResponseHelper.success(res, user);
|
|
127
|
+
* }));
|
|
105
128
|
*/
|
|
106
|
-
export const asyncHandler =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
export const asyncHandler = <
|
|
130
|
+
P = ParamsDictionary,
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
132
|
+
ResBody = any,
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
134
|
+
ReqBody = any,
|
|
135
|
+
ReqQuery = ParsedQs,
|
|
136
|
+
>(
|
|
137
|
+
fn: (
|
|
138
|
+
req: Request<P, ResBody, ReqBody, ReqQuery>,
|
|
139
|
+
res: Response<ResBody>,
|
|
140
|
+
next: NextFunction,
|
|
141
|
+
) => Promise<void>,
|
|
142
|
+
): RequestHandler<P, ResBody, ReqBody, ReqQuery> => {
|
|
143
|
+
return (req, res, next): void => {
|
|
110
144
|
Promise.resolve(fn(req, res, next)).catch(next);
|
|
111
145
|
};
|
|
112
146
|
};
|
|
@@ -116,6 +150,6 @@ export const asyncHandler = (
|
|
|
116
150
|
*
|
|
117
151
|
* Register BEFORE error handler.
|
|
118
152
|
*/
|
|
119
|
-
export const notFoundHandler = (req: Request, res: Response) => {
|
|
120
|
-
|
|
153
|
+
export const notFoundHandler = (req: Request, res: Response): void => {
|
|
154
|
+
ResponseHelper.notFound(res, `Route ${req.method} ${req.url} not found`);
|
|
121
155
|
};
|
|
@@ -8,8 +8,8 @@ import { ErrorDetail } from '@/types/response.types';
|
|
|
8
8
|
*
|
|
9
9
|
* Validates request body, query, or params against Zod schema.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* @example
|
|
12
|
+
* router.post('/users', validate({ body: createUserSchema }), createUser);
|
|
13
13
|
*/
|
|
14
14
|
interface ValidationSchemas {
|
|
15
15
|
body?: AnyZodObject;
|
|
@@ -18,7 +18,7 @@ interface ValidationSchemas {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export const validate = (schemas: ValidationSchemas) => {
|
|
21
|
-
return async (req: Request, res: Response, next: NextFunction) => {
|
|
21
|
+
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
22
22
|
try {
|
|
23
23
|
if (schemas.body) {
|
|
24
24
|
req.body = await schemas.body.parseAsync(req.body);
|
|
@@ -32,7 +32,7 @@ export const validate = (schemas: ValidationSchemas) => {
|
|
|
32
32
|
req.params = await schemas.params.parseAsync(req.params);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
next();
|
|
36
36
|
} catch (error) {
|
|
37
37
|
if (error instanceof ZodError) {
|
|
38
38
|
const details: ErrorDetail[] = error.errors.map((err) => ({
|
|
@@ -41,10 +41,11 @@ export const validate = (schemas: ValidationSchemas) => {
|
|
|
41
41
|
code: err.code,
|
|
42
42
|
}));
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
ResponseHelper.validationError(res, details);
|
|
45
|
+
return;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
next(error);
|
|
48
49
|
}
|
|
49
50
|
};
|
|
50
51
|
};
|
|
@@ -16,14 +16,15 @@ export class ResponseHelper {
|
|
|
16
16
|
/**
|
|
17
17
|
* Success Response (2xx)
|
|
18
18
|
*/
|
|
19
|
-
static success<T>(res: Response, data: T, message = 'Success', statusCode = 200):
|
|
19
|
+
static success<T>(res: Response, data: T, message = 'Success', statusCode = 200): void {
|
|
20
20
|
const response = {
|
|
21
21
|
success: true,
|
|
22
22
|
data,
|
|
23
23
|
message,
|
|
24
24
|
timestamp: new Date().toISOString(),
|
|
25
25
|
} satisfies ApiSuccessResponse<T>;
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
res.status(statusCode).json(response);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -36,7 +37,7 @@ export class ResponseHelper {
|
|
|
36
37
|
message: string,
|
|
37
38
|
statusCode = 400,
|
|
38
39
|
details?: ErrorDetail[],
|
|
39
|
-
):
|
|
40
|
+
): void {
|
|
40
41
|
const response = {
|
|
41
42
|
success: false,
|
|
42
43
|
error: {
|
|
@@ -46,7 +47,8 @@ export class ResponseHelper {
|
|
|
46
47
|
},
|
|
47
48
|
timestamp: new Date().toISOString(),
|
|
48
49
|
} satisfies ApiFailResponse;
|
|
49
|
-
|
|
50
|
+
|
|
51
|
+
res.status(statusCode).json(response);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/**
|
|
@@ -59,18 +61,18 @@ export class ResponseHelper {
|
|
|
59
61
|
message: string,
|
|
60
62
|
statusCode = 500,
|
|
61
63
|
error?: Error,
|
|
62
|
-
):
|
|
64
|
+
): void {
|
|
63
65
|
const response = {
|
|
64
66
|
success: false,
|
|
65
67
|
error: {
|
|
66
68
|
code,
|
|
67
69
|
message,
|
|
68
|
-
// Only include stack trace in development
|
|
69
70
|
stack: config.nodeEnv === 'development' ? error?.stack : undefined,
|
|
70
71
|
},
|
|
71
72
|
timestamp: new Date().toISOString(),
|
|
72
73
|
} satisfies ApiErrorResponse;
|
|
73
|
-
|
|
74
|
+
|
|
75
|
+
res.status(statusCode).json(response);
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
// ========== Convenience Methods ==========
|
|
@@ -78,63 +80,63 @@ export class ResponseHelper {
|
|
|
78
80
|
/**
|
|
79
81
|
* 201 Created
|
|
80
82
|
*/
|
|
81
|
-
static created<T>(res: Response, data: T, message = 'Resource created') {
|
|
82
|
-
|
|
83
|
+
static created<T>(res: Response, data: T, message = 'Resource created'): void {
|
|
84
|
+
ResponseHelper.success(res, data, message, 201);
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
/**
|
|
86
88
|
* 204 No Content
|
|
87
89
|
*/
|
|
88
|
-
static noContent(res: Response) {
|
|
89
|
-
|
|
90
|
+
static noContent(res: Response): void {
|
|
91
|
+
res.status(204).send();
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
/**
|
|
93
95
|
* 400 Bad Request (with validation errors)
|
|
94
96
|
*/
|
|
95
|
-
static badRequest(res: Response, message: string, details?: ErrorDetail[]) {
|
|
96
|
-
|
|
97
|
+
static badRequest(res: Response, message: string, details?: ErrorDetail[]): void {
|
|
98
|
+
ResponseHelper.fail(res, 'BAD_REQUEST', message, 400, details);
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
/**
|
|
100
102
|
* 401 Unauthorized
|
|
101
103
|
*/
|
|
102
|
-
static unauthorized(res: Response, message = 'Authentication required') {
|
|
103
|
-
|
|
104
|
+
static unauthorized(res: Response, message = 'Authentication required'): void {
|
|
105
|
+
ResponseHelper.fail(res, 'UNAUTHORIZED', message, 401);
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
/**
|
|
107
109
|
* 403 Forbidden
|
|
108
110
|
*/
|
|
109
|
-
static forbidden(res: Response, message = 'Access denied') {
|
|
110
|
-
|
|
111
|
+
static forbidden(res: Response, message = 'Access denied'): void {
|
|
112
|
+
ResponseHelper.fail(res, 'FORBIDDEN', message, 403);
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
/**
|
|
114
116
|
* 404 Not Found
|
|
115
117
|
*/
|
|
116
|
-
static notFound(res: Response, message = 'Resource not found') {
|
|
117
|
-
|
|
118
|
+
static notFound(res: Response, message = 'Resource not found'): void {
|
|
119
|
+
ResponseHelper.fail(res, 'NOT_FOUND', message, 404);
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
/**
|
|
121
123
|
* 409 Conflict
|
|
122
124
|
*/
|
|
123
|
-
static conflict(res: Response, message: string) {
|
|
124
|
-
|
|
125
|
+
static conflict(res: Response, message: string): void {
|
|
126
|
+
ResponseHelper.fail(res, 'CONFLICT', message, 409);
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
/**
|
|
128
130
|
* 422 Unprocessable Entity (validation errors)
|
|
129
131
|
*/
|
|
130
|
-
static validationError(res: Response, details: ErrorDetail[]) {
|
|
131
|
-
|
|
132
|
+
static validationError(res: Response, details: ErrorDetail[]): void {
|
|
133
|
+
ResponseHelper.fail(res, 'VALIDATION_ERROR', 'Validation failed', 422, details);
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
/**
|
|
135
137
|
* 500 Internal Server Error
|
|
136
138
|
*/
|
|
137
|
-
static internalError(res: Response, message = 'Internal server error', error?: Error) {
|
|
138
|
-
|
|
139
|
+
static internalError(res: Response, message = 'Internal server error', error?: Error): void {
|
|
140
|
+
ResponseHelper.error(res, 'INTERNAL_SERVER_ERROR', message, 500, error);
|
|
139
141
|
}
|
|
140
142
|
}
|
|
@@ -6,20 +6,37 @@ import { logger } from '@/common/utils/logger';
|
|
|
6
6
|
* MongoDB Connection (Mongoose)
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
const MAX_RETRIES = 5;
|
|
10
|
+
const RETRY_DELAY = 5000;
|
|
11
|
+
|
|
9
12
|
export const connectDatabase = async (): Promise<void> => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
const options: mongoose.ConnectOptions = {
|
|
14
|
+
maxPoolSize: 10,
|
|
15
|
+
minPoolSize: 2,
|
|
16
|
+
serverSelectionTimeoutMS: 5000,
|
|
17
|
+
socketTimeoutMS: 45000,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
21
|
+
try {
|
|
22
|
+
await mongoose.connect(config.database.url, options);
|
|
23
|
+
|
|
24
|
+
logger.info(' MongoDB connected successfully');
|
|
25
|
+
|
|
26
|
+
return;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
logger.error(` MongoDB connection attempt ${attempt}/${MAX_RETRIES} failed:`, error);
|
|
29
|
+
|
|
30
|
+
if (attempt < MAX_RETRIES) {
|
|
31
|
+
logger.info(` Retrying in ${RETRY_DELAY}ms...`);
|
|
32
|
+
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
|
|
34
|
+
} else {
|
|
35
|
+
logger.error(' Failed to connect to MongoDB after maximum retries');
|
|
36
|
+
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
23
40
|
}
|
|
24
41
|
};
|
|
25
42
|
|
|
@@ -44,13 +61,3 @@ mongoose.connection.on('disconnected', () => {
|
|
|
44
61
|
mongoose.connection.on('error', (error) => {
|
|
45
62
|
logger.error(' MongoDB connection error:', error);
|
|
46
63
|
});
|
|
47
|
-
|
|
48
|
-
// Graceful shutdown
|
|
49
|
-
const gracefulShutdown = async (signal: string) => {
|
|
50
|
-
logger.info(` ${signal} received. Closing MongoDB connection...`);
|
|
51
|
-
await disconnectDatabase();
|
|
52
|
-
process.exit(0);
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
process.on('SIGINT', () => void gracefulShutdown('SIGINT'));
|
|
56
|
-
process.on('SIGTERM', () => void gracefulShutdown('SIGTERM'));
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { config } from './config';
|
|
2
2
|
import express, { Application } from 'express';
|
|
3
3
|
import cors from 'cors';
|
|
4
|
+
import helmet from 'helmet';
|
|
5
|
+
import compression from 'compression';
|
|
6
|
+
// import rateLimit from 'express-rate-limit';
|
|
4
7
|
import { logger } from './common/utils/logger';
|
|
5
8
|
import { httpLogger } from './common/utils/http-logger';
|
|
6
9
|
import { errorHandler, notFoundHandler } from './common/middleware/error.middleware';
|
|
7
|
-
import { connectDatabase } from './database';
|
|
10
|
+
import { connectDatabase, disconnectDatabase } from './database';
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Express Application Setup
|
|
11
14
|
*/
|
|
12
|
-
|
|
13
15
|
const app: Application = express();
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
|
-
* Middleware
|
|
18
|
+
* Security Middleware
|
|
17
19
|
*/
|
|
20
|
+
app.use(helmet()); // Security headers
|
|
18
21
|
app.use(
|
|
19
22
|
cors({
|
|
20
23
|
origin: config.cors.origin,
|
|
@@ -22,12 +25,36 @@ app.use(
|
|
|
22
25
|
}),
|
|
23
26
|
);
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Performance Middleware
|
|
30
|
+
*/
|
|
31
|
+
app.use(compression()); // Compress responses
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Rate Limiting (Applied to API routes only)
|
|
35
|
+
* Comment it out to use it 👇🏽 and the import 👆🏽
|
|
36
|
+
*/
|
|
37
|
+
// const limiter = rateLimit({
|
|
38
|
+
// windowMs: 15 * 60 * 1000, // 15 minutes
|
|
39
|
+
// max: 100, // Limit each IP to 100 requests per window
|
|
40
|
+
// message: 'Too many requests from this IP, please try again later.',
|
|
41
|
+
// standardHeaders: true, // Return rate limit info in `RateLimit-*` headers
|
|
42
|
+
// legacyHeaders: false, // Disable `X-RateLimit-*` headers
|
|
43
|
+
// });
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Body Parsing Middleware
|
|
47
|
+
*/
|
|
25
48
|
app.use(express.json({ limit: '10mb' }));
|
|
26
49
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Logging Middleware
|
|
53
|
+
*/
|
|
27
54
|
app.use(httpLogger);
|
|
28
55
|
|
|
29
56
|
/**
|
|
30
|
-
* Health Check Endpoint
|
|
57
|
+
* Health Check Endpoint (No rate limiting)
|
|
31
58
|
*/
|
|
32
59
|
app.get('/health', (_req, res) => {
|
|
33
60
|
res.json({
|
|
@@ -43,17 +70,19 @@ app.get('/health', (_req, res) => {
|
|
|
43
70
|
*/
|
|
44
71
|
app.get('/', (_req, res) => {
|
|
45
72
|
res.json({
|
|
46
|
-
message: '
|
|
73
|
+
message: 'Express TypeScript API',
|
|
47
74
|
version: '1.0.0',
|
|
75
|
+
documentation: '/api/docs', // Add when you implement API docs
|
|
48
76
|
});
|
|
49
77
|
});
|
|
50
78
|
|
|
51
79
|
/**
|
|
52
80
|
* API Routes
|
|
53
81
|
*
|
|
82
|
+
* Apply rate limiting to all API routes
|
|
54
83
|
* Add your routes here:
|
|
55
|
-
*
|
|
56
|
-
*
|
|
84
|
+
* app.use('/api/users', limiter, userRoutes);
|
|
85
|
+
* app.use('/api/posts', limiter, postRoutes);
|
|
57
86
|
*/
|
|
58
87
|
|
|
59
88
|
/**
|
|
@@ -67,10 +96,11 @@ app.use(errorHandler);
|
|
|
67
96
|
*/
|
|
68
97
|
const startServer = async (): Promise<void> => {
|
|
69
98
|
try {
|
|
99
|
+
// Connect to database with retry logic
|
|
70
100
|
await connectDatabase();
|
|
71
101
|
|
|
72
102
|
const PORT = config.port;
|
|
73
|
-
app.listen(PORT, () => {
|
|
103
|
+
const server = app.listen(PORT, () => {
|
|
74
104
|
if (config.nodeEnv === 'development') {
|
|
75
105
|
logger.info(`
|
|
76
106
|
╔════════════════════════════════════════╗
|
|
@@ -80,41 +110,62 @@ const startServer = async (): Promise<void> => {
|
|
|
80
110
|
╚════════════════════════════════════════╝
|
|
81
111
|
`);
|
|
82
112
|
} else {
|
|
83
|
-
// Production: Simple, JSON-friendly message
|
|
84
113
|
logger.info({
|
|
85
|
-
message: 'Server
|
|
114
|
+
message: 'Server started successfully',
|
|
86
115
|
port: PORT,
|
|
87
116
|
environment: config.nodeEnv,
|
|
88
|
-
url: `http://localhost:${PORT}`,
|
|
89
117
|
});
|
|
90
118
|
}
|
|
91
119
|
});
|
|
120
|
+
|
|
121
|
+
// Graceful shutdown
|
|
122
|
+
const gracefulShutdown = async (signal: string) => {
|
|
123
|
+
logger.info(`${signal} received. Starting graceful shutdown...`);
|
|
124
|
+
|
|
125
|
+
// Stop accepting new requests
|
|
126
|
+
server.close(async () => {
|
|
127
|
+
logger.info('HTTP server closed');
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
// Close database connections
|
|
131
|
+
await disconnectDatabase();
|
|
132
|
+
logger.info('Database disconnected');
|
|
133
|
+
process.exit(0);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
logger.error('Error during graceful shutdown:', error);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Force shutdown after 10 seconds
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
logger.error('Forced shutdown after timeout');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}, 10000);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
process.on('SIGINT', () => void gracefulShutdown('SIGINT'));
|
|
148
|
+
process.on('SIGTERM', () => void gracefulShutdown('SIGTERM'));
|
|
92
149
|
} catch (error) {
|
|
93
150
|
logger.error('Failed to start server:', error);
|
|
94
151
|
process.exit(1);
|
|
95
152
|
}
|
|
96
153
|
};
|
|
97
154
|
|
|
98
|
-
//
|
|
99
|
-
const gracefulShutdown = (signal: string) => {
|
|
100
|
-
logger.info(`${signal} received. Starting graceful shutdown...`);
|
|
101
|
-
process.exit(0);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
105
|
-
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
155
|
+
// Unhandled errors
|
|
106
156
|
process.on('unhandledRejection', (reason: Error) => {
|
|
107
157
|
logger.error('Unhandled Promise Rejection:', reason);
|
|
108
158
|
process.exit(1);
|
|
109
159
|
});
|
|
160
|
+
|
|
110
161
|
process.on('uncaughtException', (error: Error) => {
|
|
111
162
|
logger.error('Uncaught Exception:', error);
|
|
112
163
|
process.exit(1);
|
|
113
164
|
});
|
|
114
165
|
|
|
166
|
+
// Start the server
|
|
115
167
|
startServer().catch((error) => {
|
|
116
168
|
logger.error('Failed to start server:', error);
|
|
117
|
-
|
|
118
169
|
process.exit(1);
|
|
119
170
|
});
|
|
120
171
|
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
version: '3.8'
|
|
2
|
-
|
|
3
|
-
services:
|
|
4
|
-
# MongoDB Service
|
|
5
|
-
mongodb:
|
|
6
|
-
image: mongo:7
|
|
7
|
-
container_name: express-mongodb
|
|
8
|
-
restart: unless-stopped
|
|
9
|
-
ports:
|
|
10
|
-
- '27017:27017'
|
|
11
|
-
environment:
|
|
12
|
-
MONGO_INITDB_ROOT_USERNAME: admin
|
|
13
|
-
MONGO_INITDB_ROOT_PASSWORD: password123
|
|
14
|
-
MONGO_INITDB_DATABASE: myapp
|
|
15
|
-
volumes:
|
|
16
|
-
- mongodb_data:/data/db
|
|
17
|
-
networks:
|
|
18
|
-
- app-network
|
|
19
|
-
|
|
20
|
-
# PostgreSQL Service (uncomment if using PostgreSQL)
|
|
21
|
-
# postgres:
|
|
22
|
-
# image: postgres:16-alpine
|
|
23
|
-
# container_name: express-postgres
|
|
24
|
-
# restart: unless-stopped
|
|
25
|
-
# ports:
|
|
26
|
-
# - '5432:5432'
|
|
27
|
-
# environment:
|
|
28
|
-
# POSTGRES_USER: postgres
|
|
29
|
-
# POSTGRES_PASSWORD: password123
|
|
30
|
-
# POSTGRES_DB: myapp
|
|
31
|
-
# volumes:
|
|
32
|
-
# - postgres_data:/var/lib/postgresql/data
|
|
33
|
-
# networks:
|
|
34
|
-
# - app-network
|
|
35
|
-
|
|
36
|
-
# Application Service
|
|
37
|
-
app:
|
|
38
|
-
build:
|
|
39
|
-
context: .
|
|
40
|
-
dockerfile: Dockerfile
|
|
41
|
-
target: production
|
|
42
|
-
container_name: express-app
|
|
43
|
-
restart: unless-stopped
|
|
44
|
-
ports:
|
|
45
|
-
- '3000:3000'
|
|
46
|
-
environment:
|
|
47
|
-
NODE_ENV: production
|
|
48
|
-
PORT: 3000
|
|
49
|
-
# MongoDB connection
|
|
50
|
-
DATABASE_URL: mongodb://admin:password123@mongodb:27017/myapp?authSource=admin
|
|
51
|
-
# PostgreSQL connection (uncomment if using PostgreSQL)
|
|
52
|
-
# DATABASE_URL: postgresql://postgres:password123@postgres:5432/myapp
|
|
53
|
-
JWT_SECRET: your-super-secret-jwt-key-change-this-in-production-min-32-chars
|
|
54
|
-
JWT_EXPIRES_IN: 7d
|
|
55
|
-
LOG_LEVEL: info
|
|
56
|
-
depends_on:
|
|
57
|
-
- mongodb
|
|
58
|
-
# - postgres # Uncomment if using PostgreSQL
|
|
59
|
-
networks:
|
|
60
|
-
- app-network
|
|
61
|
-
|
|
62
|
-
networks:
|
|
63
|
-
app-network:
|
|
64
|
-
driver: bridge
|
|
65
|
-
|
|
66
|
-
volumes:
|
|
67
|
-
mongodb_data:
|
|
68
|
-
# postgres_data: # Uncomment if using PostgreSQL
|
|
File without changes
|