devdad-express-utils 1.5.0 → 1.7.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/README.md CHANGED
@@ -54,10 +54,10 @@ throw new AppError("Validation failed", 400, ["Email is required"]);
54
54
 
55
55
  ### Response Formatting
56
56
 
57
- Standardize API responses with consistent JSON structure.
57
+ Standardize API responses with consistent JSON structure and automatic response sending.
58
58
 
59
59
  ```typescript
60
- import { sendSuccess, sendError, sendPaginated } from "devdad-express-utils";
60
+ import { sendSuccess, sendError } from "devdad-express-utils";
61
61
 
62
62
  // Success response
63
63
  sendSuccess(res, { id: 1, name: "John" }, "User fetched", 200);
@@ -66,26 +66,86 @@ sendSuccess(res, { id: 1, name: "John" }, "User fetched", 200);
66
66
  sendSuccess(res, { id: 1, name: "John" })
67
67
  .cookie("session", "abc123")
68
68
  .setHeader("X-Custom", "value");
69
+ // Response sent automatically! 🎉
69
70
 
70
71
  // Error response
71
72
  sendError(res, "User not found", 404);
72
73
  ```
73
74
 
75
+ #### Response Format
76
+
77
+ All responses include a `success` field by default:
78
+
79
+ **Success Response:**
80
+
81
+ ```json
82
+ {
83
+ "status": "success",
84
+ "success": true,
85
+ "message": "User fetched",
86
+ "data": { "id": 1, "name": "John" }
87
+ }
88
+ ```
89
+
90
+ **Error Response:**
91
+
92
+ ```json
93
+ {
94
+ "status": "error",
95
+ "success": false,
96
+ "message": "User not found"
97
+ }
98
+ ```
99
+
100
+ #### Method Chaining
101
+
102
+ `sendSuccess` returns the Express response object, allowing you to chain any Express response methods:
103
+
104
+ ```typescript
105
+ sendSuccess(res, { token: "abc123" }, "Login successful", 200)
106
+ .cookie("authToken", "abc123", { httpOnly: true, secure: true })
107
+ .header("X-Rate-Limit-Remaining", "99")
108
+ .status(200); // Can even override status
109
+ // Response automatically sent after chaining completes
110
+ ```
111
+
74
112
  ### Database Connection
75
113
 
76
- MongoDB connection utility with automatic reconnection and retry logic.
114
+ MongoDB connection utility with automatic reconnection, exponential backoff, and configurable retry logic.
77
115
 
78
116
  ```typescript
79
- import { connectDB, getDBStatus } from "devdad-express-utils";
117
+ import {
118
+ connectDB,
119
+ getDBStatus,
120
+ resetDBConnection,
121
+ } from "devdad-express-utils";
80
122
 
81
123
  // Connect to MongoDB (ensure MONGO_URI is set in environment)
82
124
  await connectDB();
83
125
 
84
126
  // Check connection status
85
127
  const status = getDBStatus();
86
- console.log(status); // { isConnected: true, readyState: 1, host: '...', name: '...' }
128
+ console.log(status); // { isConnected: true, readyState: 1, host: '...', name: '...', retryCount: 0 }
129
+
130
+ // Manually reset and retry (useful after Docker container restarts)
131
+ await resetDBConnection();
87
132
  ```
88
133
 
134
+ #### Environment Variables
135
+
136
+ - **MONGO_URI**: MongoDB connection string (required)
137
+ - **DB_MAX_RETRIES**: Maximum connection retry attempts (default: 10)
138
+ - **DB_RETRY_INTERVAL**: Initial retry interval in milliseconds (default: 3000)
139
+ - **NODE_ENV**: Set to 'production' to exit after max retries, 'development' to keep process alive
140
+
141
+ #### Retry Behavior
142
+
143
+ - **Exponential backoff**: Retry intervals increase exponentially (3s → 6s → 12s → 24s → 30s max)
144
+ - **Random jitter**: Adds up to 1 second of random delay to prevent thundering herd
145
+ - **Docker-friendly**: Higher default retry count (10) accommodates container startup times
146
+ - **Development mode**: Process stays alive after max retries for manual recovery
147
+ - **Production mode**: Process exits after max retries to allow container restart
148
+
89
149
  ### Logging
90
150
 
91
151
  Winston-based logger with configurable service name and environment-aware transports.
@@ -197,20 +257,32 @@ errorHandler(err: any, req: Request, res: Response, next: NextFunction) => void
197
257
 
198
258
  ### sendSuccess
199
259
 
200
- Sends a standardized success response. Returns the Response object for method chaining.
260
+ Sends a standardized success response with automatic sending after method chaining. Returns the Response object for chaining Express response methods like `.cookie()`, `.header()`, etc.
201
261
 
202
262
  ```typescript
203
263
  sendSuccess(res: Response, data: any, message?: string, statusCode?: number) => Response
204
264
  ```
205
265
 
266
+ **Features:**
267
+
268
+ - Includes `success: true` field by default
269
+ - Automatically sends response after chaining completes
270
+ - Supports all Express response method chaining
271
+ - No extra `.send()` call needed
272
+
206
273
  ### sendError
207
274
 
208
- Sends a standardized error response.
275
+ Sends a standardized error response immediately.
209
276
 
210
277
  ```typescript
211
278
  sendError(res: Response, message: string, statusCode?: number, data?: any) => void
212
279
  ```
213
280
 
281
+ **Features:**
282
+
283
+ - Includes `success: false` field by default
284
+ - Sends response immediately (no chaining needed for errors)
285
+
214
286
  ### connectDB
215
287
 
216
288
  Connects to MongoDB with retry logic and automatic reconnection.
@@ -224,7 +296,15 @@ connectDB() => Promise<void>
224
296
  Gets the current MongoDB connection status.
225
297
 
226
298
  ```typescript
227
- getDBStatus() => { isConnected: boolean; readyState: number; host: string; name: string; }
299
+ getDBStatus() => { isConnected: boolean; readyState: number; host: string; name: string; retryCount: number; }
300
+ ```
301
+
302
+ ### resetDBConnection
303
+
304
+ Manually resets retry count and attempts reconnection. Useful for recovering from Docker container restarts.
305
+
306
+ ```typescript
307
+ resetDBConnection() => Promise<void>
228
308
  ```
229
309
 
230
310
  ### logger
@@ -17,11 +17,12 @@ declare class DatabaseConnection {
17
17
  connect(): Promise<void>;
18
18
  /**
19
19
  * Handles connection errors by retrying the connection up to MAX_RETRIES times.
20
- * Exits the process if all retries fail.
20
+ * Uses exponential backoff for retry intervals.
21
21
  */
22
22
  private handleConnectionError;
23
23
  /**
24
24
  * Handles disconnection events by attempting to reconnect if not already connected.
25
+ * Resets retry count for fresh reconnection attempts.
25
26
  */
26
27
  private handleDisconnection;
27
28
  /**
@@ -37,7 +38,13 @@ declare class DatabaseConnection {
37
38
  readyState: number;
38
39
  host: string;
39
40
  name: string;
41
+ retryCount: number;
40
42
  };
43
+ /**
44
+ * Manually reset retry count and attempt reconnection
45
+ * Useful for recovering from Docker container restarts
46
+ */
47
+ resetAndRetry(): Promise<void>;
41
48
  }
42
49
  declare const _default: () => Promise<void>;
43
50
  export default _default;
@@ -46,5 +53,7 @@ export declare const getDBStatus: () => {
46
53
  readyState: number;
47
54
  host: string;
48
55
  name: string;
56
+ retryCount: number;
49
57
  };
58
+ export declare const resetDBConnection: () => Promise<void>;
50
59
  export { DatabaseConnection };
@@ -1,12 +1,18 @@
1
1
  import mongoose from "mongoose";
2
2
  /**
3
3
  * Maximum number of connection retry attempts
4
+ * Can be overridden with DB_MAX_RETRIES environment variable
4
5
  */
5
- const MAX_RETRIES = 3;
6
+ const MAX_RETRIES = parseInt(process.env.DB_MAX_RETRIES || '10');
6
7
  /**
7
- * Interval between retry attempts in milliseconds
8
+ * Initial interval between retry attempts in milliseconds
9
+ * Can be overridden with DB_RETRY_INTERVAL environment variable
8
10
  */
9
- const RETRY_INTERVAL = 5000; // 5 seconds
11
+ const INITIAL_RETRY_INTERVAL = parseInt(process.env.DB_RETRY_INTERVAL || '3000'); // 3 seconds
12
+ /**
13
+ * Maximum retry interval (caps exponential backoff)
14
+ */
15
+ const MAX_RETRY_INTERVAL = 30000; // 30 seconds
10
16
  /**
11
17
  * Database connection class for MongoDB using Mongoose.
12
18
  * Provides automatic reconnection, retry logic, and connection status monitoring.
@@ -69,27 +75,39 @@ class DatabaseConnection {
69
75
  }
70
76
  /**
71
77
  * Handles connection errors by retrying the connection up to MAX_RETRIES times.
72
- * Exits the process if all retries fail.
78
+ * Uses exponential backoff for retry intervals.
73
79
  */
74
80
  async handleConnectionError() {
75
81
  if (this.retryCount < MAX_RETRIES) {
76
82
  this.retryCount++;
77
- console.log(`Retrying connection... Attempt ${this.retryCount} of ${MAX_RETRIES}`);
78
- await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL));
83
+ // Calculate exponential backoff with jitter
84
+ const exponentialDelay = Math.min(INITIAL_RETRY_INTERVAL * Math.pow(2, this.retryCount - 1), MAX_RETRY_INTERVAL);
85
+ const jitter = Math.random() * 1000; // Add up to 1 second of random jitter
86
+ const delay = exponentialDelay + jitter;
87
+ console.log(`Retrying connection... Attempt ${this.retryCount} of ${MAX_RETRIES} (waiting ${Math.round(delay)}ms)`);
88
+ await new Promise((resolve) => setTimeout(resolve, delay));
79
89
  return this.connect();
80
90
  }
81
91
  else {
82
- console.error(`Failed to connect to MongoDB after ${MAX_RETRIES} attempts`);
83
- process.exit(1);
92
+ console.error(`Failed to connect to MongoDB after ${MAX_RETRIES} attempts. Please check:`, `1. Docker container is running: docker ps`, `2. MongoDB is accessible: ${process.env.MONGO_URI}`, `3. Network connectivity between containers`, `4. Environment variables are correctly set`);
93
+ // Only exit in production, allow recovery in development
94
+ if (process.env.NODE_ENV === 'production') {
95
+ process.exit(1);
96
+ }
97
+ else {
98
+ console.log('Development mode: keeping process alive for manual retry');
99
+ }
84
100
  }
85
101
  }
86
102
  /**
87
103
  * Handles disconnection events by attempting to reconnect if not already connected.
104
+ * Resets retry count for fresh reconnection attempts.
88
105
  */
89
- handleDisconnection() {
106
+ async handleDisconnection() {
90
107
  if (!this.isConnected) {
91
108
  console.log("Attempting to reconnect to MongoDB...");
92
- this.connect();
109
+ this.retryCount = 0; // Reset retry count for reconnection attempts
110
+ await this.connect();
93
111
  }
94
112
  }
95
113
  /**
@@ -116,12 +134,23 @@ class DatabaseConnection {
116
134
  readyState: mongoose.connection.readyState,
117
135
  host: mongoose.connection.host,
118
136
  name: mongoose.connection.name,
137
+ retryCount: this.retryCount,
119
138
  };
120
139
  }
140
+ /**
141
+ * Manually reset retry count and attempt reconnection
142
+ * Useful for recovering from Docker container restarts
143
+ */
144
+ async resetAndRetry() {
145
+ console.log('Resetting connection state and retrying...');
146
+ this.retryCount = 0;
147
+ await this.connect();
148
+ }
121
149
  }
122
150
  // Create a singleton instance
123
151
  const dbConnection = new DatabaseConnection();
124
152
  // Export the connect function and the instance
125
153
  export default dbConnection.connect.bind(dbConnection);
126
154
  export const getDBStatus = dbConnection.getConnectionStatus.bind(dbConnection);
155
+ export const resetDBConnection = dbConnection.resetAndRetry.bind(dbConnection);
127
156
  export { DatabaseConnection };
@@ -1,18 +1,53 @@
1
1
  import { Response } from "express";
2
2
  /**
3
- * Sends a success response.
3
+ * Sends a success response and returns the response object for chaining.
4
+ * Automatically sends the response after chaining operations complete - no extra .send() needed!
5
+ *
6
+ * Features:
7
+ * - Includes success: true field by default
8
+ * - Supports method chaining (.cookie(), .header(), etc.)
9
+ * - Automatically sends response after chaining completes
10
+ * - Returns Express response object for chaining
11
+ *
4
12
  * @param {Response} res - Express response object.
5
13
  * @param {any} data - Response data.
6
14
  * @param {string} [message='Success'] - Response message.
7
15
  * @param {number} [statusCode=200] - HTTP status code.
8
16
  * @returns {Response} The Express response object for chaining.
17
+ *
18
+ * @example
19
+ * // Basic usage
20
+ * sendSuccess(res, { userId: 123 }, "Login successful");
21
+ *
22
+ * @example
23
+ * // Chain methods without calling .send()
24
+ * sendSuccess(res, { userId: 123 }, "Login successful", 200)
25
+ * .cookie("authToken", "xyz789", { httpOnly: true })
26
+ * .header("X-Custom", "value");
27
+ * // Response sent automatically!
9
28
  */
10
29
  export declare const sendSuccess: (res: Response, data: any, message?: string, statusCode?: number) => Response;
11
30
  /**
12
- * Sends an error response.
31
+ * Sends an error response immediately.
32
+ *
33
+ * Features:
34
+ * - Includes success: false field by default
35
+ * - Sends response immediately (no chaining needed)
36
+ * - Supports optional error data
37
+ *
13
38
  * @param {Response} res - Express response object.
14
39
  * @param {string} message - Error message.
15
40
  * @param {number} [statusCode=400] - HTTP status code.
16
41
  * @param {any} [data] - Additional error data.
42
+ *
43
+ * @example
44
+ * // Basic error
45
+ * sendError(res, "User not found", 404);
46
+ *
47
+ * @example
48
+ * // Error with additional data
49
+ * sendError(res, "Validation failed", 400, {
50
+ * errors: ["Email is required", "Password too short"]
51
+ * });
17
52
  */
18
53
  export declare const sendError: (res: Response, message: string, statusCode?: number, data?: any) => void;
@@ -1,29 +1,74 @@
1
1
  /**
2
- * Sends a success response.
2
+ * Sends a success response and returns the response object for chaining.
3
+ * Automatically sends the response after chaining operations complete - no extra .send() needed!
4
+ *
5
+ * Features:
6
+ * - Includes success: true field by default
7
+ * - Supports method chaining (.cookie(), .header(), etc.)
8
+ * - Automatically sends response after chaining completes
9
+ * - Returns Express response object for chaining
10
+ *
3
11
  * @param {Response} res - Express response object.
4
12
  * @param {any} data - Response data.
5
13
  * @param {string} [message='Success'] - Response message.
6
14
  * @param {number} [statusCode=200] - HTTP status code.
7
15
  * @returns {Response} The Express response object for chaining.
16
+ *
17
+ * @example
18
+ * // Basic usage
19
+ * sendSuccess(res, { userId: 123 }, "Login successful");
20
+ *
21
+ * @example
22
+ * // Chain methods without calling .send()
23
+ * sendSuccess(res, { userId: 123 }, "Login successful", 200)
24
+ * .cookie("authToken", "xyz789", { httpOnly: true })
25
+ * .header("X-Custom", "value");
26
+ * // Response sent automatically!
8
27
  */
9
28
  export const sendSuccess = (res, data, message = "Success", statusCode = 200) => {
10
29
  const response = {
11
30
  status: "success",
31
+ success: true,
12
32
  message,
13
33
  data,
14
34
  };
15
- return res.status(statusCode).json(response);
35
+ res.status(statusCode);
36
+ // Use setImmediate to send response after current execution stack
37
+ setImmediate(() => {
38
+ if (!res._responseSent) {
39
+ res._responseSent = true;
40
+ res.json(response);
41
+ }
42
+ });
43
+ return res;
16
44
  };
17
45
  /**
18
- * Sends an error response.
46
+ * Sends an error response immediately.
47
+ *
48
+ * Features:
49
+ * - Includes success: false field by default
50
+ * - Sends response immediately (no chaining needed)
51
+ * - Supports optional error data
52
+ *
19
53
  * @param {Response} res - Express response object.
20
54
  * @param {string} message - Error message.
21
55
  * @param {number} [statusCode=400] - HTTP status code.
22
56
  * @param {any} [data] - Additional error data.
57
+ *
58
+ * @example
59
+ * // Basic error
60
+ * sendError(res, "User not found", 404);
61
+ *
62
+ * @example
63
+ * // Error with additional data
64
+ * sendError(res, "Validation failed", 400, {
65
+ * errors: ["Email is required", "Password too short"]
66
+ * });
23
67
  */
24
68
  export const sendError = (res, message, statusCode = 400, data) => {
25
69
  const response = {
26
70
  status: "error",
71
+ success: false,
27
72
  message,
28
73
  ...(data && { data }),
29
74
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devdad-express-utils",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Reusable Express.js utilities for error handling, async wrapping, and more",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
File without changes
@@ -1 +0,0 @@
1
- "use strict";
File without changes
@@ -1 +0,0 @@
1
- "use strict";
@@ -1,17 +0,0 @@
1
- import { JwtPayload, Algorithm } from "jsonwebtoken";
2
- import { Request, Response, NextFunction } from "express";
3
- interface AuthOptions {
4
- secret: string;
5
- algorithms?: Algorithm[];
6
- }
7
- interface AuthenticatedRequest extends Request {
8
- user?: JwtPayload | string;
9
- }
10
- /**
11
- * Middleware to require JWT authentication.
12
- * Verifies the JWT token from Authorization header and attaches decoded payload to req.user.
13
- * @param {AuthOptions} options - JWT verification options.
14
- * @returns {(req: Request, res: Response, next: NextFunction) => void} - Middleware function.
15
- */
16
- export declare const requireAuth: (options: AuthOptions) => (req: AuthenticatedRequest, res: Response, next: NextFunction) => void;
17
- export {};
@@ -1,48 +0,0 @@
1
- import jwt, { JsonWebTokenError, TokenExpiredError, } from "jsonwebtoken";
2
- import { AppError } from "./AppError.js";
3
- const defaultAlgorithms = ["HS256"];
4
- /**
5
- * Middleware to require JWT authentication.
6
- * Verifies the JWT token from Authorization header and attaches decoded payload to req.user.
7
- * @param {AuthOptions} options - JWT verification options.
8
- * @returns {(req: Request, res: Response, next: NextFunction) => void} - Middleware function.
9
- */
10
- export const requireAuth = (options) => {
11
- return (req, res, next) => {
12
- const authHeader = req.headers.authorization;
13
- if (!authHeader) {
14
- throw new AppError("Access denied. No token provided.", 401);
15
- }
16
- if (!authHeader.startsWith("Bearer ")) {
17
- throw new AppError("Malformed authorization header.", 401);
18
- }
19
- const token = authHeader.substring(7);
20
- if (!token) {
21
- throw new AppError("Access denied. No token provided.", 401);
22
- }
23
- try {
24
- let decoded;
25
- if (options.algorithms) {
26
- decoded = jwt.verify(token, options.secret, {
27
- algorithms: options.algorithms,
28
- });
29
- }
30
- else {
31
- decoded = jwt.verify(token, options.secret);
32
- }
33
- req.user = decoded;
34
- next();
35
- }
36
- catch (error) {
37
- if (error instanceof JsonWebTokenError) {
38
- throw new AppError("Invalid token.", 401);
39
- }
40
- else if (error instanceof TokenExpiredError) {
41
- throw new AppError("Token expired.", 401);
42
- }
43
- else {
44
- throw new AppError("Authentication failed.", 401);
45
- }
46
- }
47
- };
48
- };
@@ -1,9 +0,0 @@
1
- import { ValidationChain } from 'express-validator';
2
- import { Request, Response, NextFunction } from 'express';
3
- /**
4
- * Middleware wrapper for express-validator validations.
5
- * Runs the provided validations and throws AppError if any fail.
6
- * @param {ValidationChain[]} validations - Array of express-validator validation chains.
7
- * @returns {(req: Request, res: Response, next: NextFunction) => Promise<void>} - Middleware function.
8
- */
9
- export declare const validateRequest: (validations: ValidationChain[]) => (req: Request, res: Response, next: NextFunction) => Promise<void>;
@@ -1,19 +0,0 @@
1
- import { validationResult } from 'express-validator';
2
- import { AppError } from './AppError.js';
3
- /**
4
- * Middleware wrapper for express-validator validations.
5
- * Runs the provided validations and throws AppError if any fail.
6
- * @param {ValidationChain[]} validations - Array of express-validator validation chains.
7
- * @returns {(req: Request, res: Response, next: NextFunction) => Promise<void>} - Middleware function.
8
- */
9
- export const validateRequest = (validations) => {
10
- return async (req, res, next) => {
11
- await Promise.all(validations.map(validation => validation.run(req)));
12
- const errors = validationResult(req);
13
- if (!errors.isEmpty()) {
14
- const errorMessages = errors.array().map(err => err.msg);
15
- throw new AppError('Validation failed', 400, errorMessages);
16
- }
17
- next();
18
- };
19
- };