@codeenthusiast09/create-express-app 1.0.3 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # @codeenthusiast09/create-express-app
2
2
 
3
- [![npm version](https://badge.fury.io/js/@codeenthusiast09%2Fcreate-express-app.svg)](https://www.npmjs.com/package/@codeenthusiast09/create-express-app)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
3
+ [![npm version](https://badge.fury.io/js/@codeenthusiast09%2Fcreate-express-app.svg?v=2)](https://www.npmjs.com/package/@codeenthusiast09/create-express-app)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?v=2)](https://opensource.org/licenses/MIT)
5
5
 
6
6
  CLI tool to generate production-ready Express TypeScript projects with flexible database options.
7
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codeenthusiast09/create-express-app",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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
- package-lock.json
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 files
12
+ # Environment
11
13
  .env
12
- .env.development
13
- .env.production
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
+
@@ -0,0 +1,7 @@
1
+ NODE_ENV=production
2
+ PORT=3000
3
+ DATABASE_URL=mongodb://mongodb:27017/myapp
4
+ JWT_SECRET=change-this-to-a-secure-random-string-min-32-characters-long
5
+ JWT_EXPIRES_IN=7d
6
+ LOG_LEVEL=info
7
+ CORS_ORIGIN=*
@@ -1,27 +1,49 @@
1
1
  module.exports = {
2
- parser: "@typescript-eslint/parser",
2
+ parser: '@typescript-eslint/parser',
3
3
  parserOptions: {
4
- project: "tsconfig.json",
4
+ project: 'tsconfig.json',
5
5
  tsconfigRootDir: __dirname,
6
- sourceType: "module",
6
+ sourceType: 'module',
7
7
  },
8
- plugins: ["@typescript-eslint"],
8
+ plugins: ['@typescript-eslint'],
9
9
  extends: [
10
- "eslint:recommended",
11
- "plugin:@typescript-eslint/recommended",
12
- "plugin:@typescript-eslint/recommended-requiring-type-checking",
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: [".eslintrc.js", "dist", "node_modules"],
19
+ ignorePatterns: ['.eslintrc.js', 'dist', 'node_modules'],
20
20
  rules: {
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: "^_" }],
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 = (err: Error, req: Request, res: Response, _next: NextFunction) => {
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
- return ResponseHelper.fail(res, err.code, err.message, err.statusCode);
56
+ ResponseHelper.fail(res, err.code, err.message, err.statusCode);
57
+ return;
51
58
  }
52
- // Server errors (5xx)
53
- return ResponseHelper.error(res, err.code, err.message, err.statusCode, err);
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
- return ResponseHelper.validationError(res, details);
70
+ ResponseHelper.validationError(res, details);
71
+ return;
64
72
  }
65
73
 
66
74
  // Handle JWT errors
67
75
  if (err.name === 'JsonWebTokenError') {
68
- return ResponseHelper.unauthorized(res, 'Invalid token');
76
+ ResponseHelper.unauthorized(res, 'Invalid token');
77
+ return;
69
78
  }
70
79
  if (err.name === 'TokenExpiredError') {
71
- return ResponseHelper.unauthorized(res, 'Token expired');
80
+ ResponseHelper.unauthorized(res, 'Token expired');
81
+ return;
72
82
  }
73
83
 
74
84
  // Handle Mongoose errors
75
85
  if (err.name === 'ValidationError') {
76
- return ResponseHelper.badRequest(res, 'Validation failed');
86
+ ResponseHelper.badRequest(res, 'Validation failed');
87
+ return;
77
88
  }
78
89
  if (err.name === 'CastError') {
79
- return ResponseHelper.badRequest(res, 'Invalid ID format');
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
- return ResponseHelper.conflict(res, `Duplicate value for ${field ?? 'unknown field'}`);
96
+ ResponseHelper.conflict(res, `Duplicate value for ${field ?? 'unknown field'}`);
97
+ return;
85
98
  }
86
99
 
87
100
  // Default: Unknown error
88
- return ResponseHelper.internalError(
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
- * Usage:
101
- * router.get('/users', asyncHandler(async (req, res) => {
102
- * const users = await userService.findAll();
103
- * ResponseHelper.success(res, users);
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
- fn: (req: Request, res: Response, next: NextFunction) => Promise<void>,
108
- ) => {
109
- return (req: Request, res: Response, next: NextFunction) => {
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
- return ResponseHelper.notFound(res, `Route ${req.method} ${req.url} not found`);
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
- * Usage:
12
- * router.post('/users', validate({ body: createUserSchema }), createUser);
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
- return next();
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
- return ResponseHelper.validationError(res, details);
44
+ ResponseHelper.validationError(res, details);
45
+ return;
45
46
  }
46
47
 
47
- return next(error);
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): Response {
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
- return res.status(statusCode).json(response);
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
- ): Response {
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
- return res.status(statusCode).json(response);
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
- ): Response {
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
- return res.status(statusCode).json(response);
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
- return ResponseHelper.success(res, data, message, 201);
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
- return res.status(204).send();
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
- return ResponseHelper.fail(res, 'BAD_REQUEST', message, 400, details);
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
- return ResponseHelper.fail(res, 'UNAUTHORIZED', message, 401);
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
- return ResponseHelper.fail(res, 'FORBIDDEN', message, 403);
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
- return ResponseHelper.fail(res, 'NOT_FOUND', message, 404);
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
- return ResponseHelper.fail(res, 'CONFLICT', message, 409);
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
- return ResponseHelper.fail(res, 'VALIDATION_ERROR', 'Validation failed', 422, details);
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
- return ResponseHelper.error(res, 'INTERNAL_SERVER_ERROR', message, 500, error);
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
- try {
11
- const options: mongoose.ConnectOptions = {
12
- maxPoolSize: 10,
13
- minPoolSize: 2,
14
- serverSelectionTimeoutMS: 5000,
15
- socketTimeoutMS: 45000,
16
- };
17
-
18
- await mongoose.connect(config.database.url, options);
19
- logger.info('󰆼 MongoDB connected successfully');
20
- } catch (error) {
21
- logger.error('󱙀 MongoDB connection error:', error);
22
- process.exit(1);
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: 'Hello World!',
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
- * app.use('/api/users', userRoutes);
56
- * app.use('/api/posts', postRoutes);
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 is running',
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
- // Graceful shutdown
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