express-pro-toolkit 2.0.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.
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Wraps an async Express route handler to automatically catch
5
+ * rejected promises and forward them to Express error-handling middleware.
6
+ *
7
+ * Compatible with Express 4 and 5. Preserves the original function's
8
+ * `length` property so Express can distinguish middleware (3 args) from
9
+ * error handlers (4 args).
10
+ *
11
+ * @param {Function} fn - Async route handler `(req, res, next) => Promise<void>`
12
+ * @returns {Function} Express middleware that catches async errors
13
+ *
14
+ * @example
15
+ * const { asyncHandler } = require('express-pro-toolkit');
16
+ *
17
+ * router.get('/users', asyncHandler(async (req, res) => {
18
+ * const users = await User.find();
19
+ * res.json(users);
20
+ * }));
21
+ *
22
+ * // Also works with error-handling middleware (4 args)
23
+ * app.use(asyncHandler(async (err, req, res, next) => {
24
+ * await logErrorToService(err);
25
+ * next(err);
26
+ * }));
27
+ */
28
+ function asyncHandler(fn) {
29
+ if (typeof fn !== 'function') {
30
+ throw new TypeError('asyncHandler requires a function argument');
31
+ }
32
+
33
+ // Express uses fn.length to distinguish regular middleware (3) from
34
+ // error-handling middleware (4). We must preserve it.
35
+ if (fn.length === 4) {
36
+ // Error-handling middleware: (err, req, res, next)
37
+ return function asyncErrorHandlerWrapper(err, req, res, next) {
38
+ Promise.resolve().then(() => fn(err, req, res, next)).catch(next);
39
+ };
40
+ }
41
+
42
+ // Standard middleware / route handler: (req, res, next)
43
+ return function asyncHandlerWrapper(req, res, next) {
44
+ Promise.resolve().then(() => fn(req, res, next)).catch(next);
45
+ };
46
+ }
47
+
48
+ module.exports = asyncHandler;
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const { randomBytes } = require('crypto');
4
+
5
+ /**
6
+ * Default ID generator — 16 random hex characters.
7
+ * Highly performant; avoids UUID overhead.
8
+ * @returns {string}
9
+ */
10
+ function defaultGenerateId() {
11
+ return randomBytes(8).toString('hex');
12
+ }
13
+
14
+ /**
15
+ * Creates middleware that attaches a unique correlation / request ID
16
+ * to every incoming request, enabling distributed tracing.
17
+ *
18
+ * The ID is:
19
+ * 1. Read from an incoming header (default `x-request-id`) — honours
20
+ * upstream load-balancers / API gateways.
21
+ * 2. Generated if not present.
22
+ * 3. Stored on `req.id` for downstream use.
23
+ * 4. Echoed back on the response in the same header.
24
+ *
25
+ * @param {object} [options]
26
+ * @param {string} [options.header='x-request-id'] - Header to read / write
27
+ * @param {Function} [options.generator] - Custom ID generator `() => string`
28
+ * @param {boolean} [options.setResponseHeader=true] - Whether to echo ID on response
29
+ * @returns {Function} Express middleware
30
+ *
31
+ * @example
32
+ * const { correlationId } = require('express-pro-toolkit');
33
+ * app.use(correlationId());
34
+ * app.use(correlationId({ header: 'x-correlation-id' }));
35
+ */
36
+ function correlationId(options) {
37
+ const opts = options && typeof options === 'object' ? options : {};
38
+ const header = (typeof opts.header === 'string' && opts.header) || 'x-request-id';
39
+ const headerLower = header.toLowerCase();
40
+ const generate = typeof opts.generator === 'function' ? opts.generator : defaultGenerateId;
41
+ const setResponse = opts.setResponseHeader !== false;
42
+
43
+ return function correlationIdMiddleware(req, res, next) {
44
+ const incoming = req.headers[headerLower];
45
+ const id = (typeof incoming === 'string' && incoming.length > 0) ? incoming : generate();
46
+
47
+ /** @type {string} Unique request identifier */
48
+ req.id = id;
49
+
50
+ if (setResponse) {
51
+ res.setHeader(header, id);
52
+ }
53
+
54
+ next();
55
+ };
56
+ }
57
+
58
+ module.exports = correlationId;
@@ -0,0 +1,153 @@
1
+ 'use strict';
2
+
3
+ const AppError = require('./AppError');
4
+
5
+ /* ─── Default logger (console) ────────────────────────────────────── */
6
+
7
+ /**
8
+ * @param {object} payload
9
+ */
10
+ function defaultOnError(payload) {
11
+ console.error(
12
+ `[express-pro-toolkit] ERROR ${payload.statusCode} — ${payload.method} ${payload.url}`,
13
+ );
14
+ console.error(payload.stack || payload.message);
15
+ }
16
+
17
+ /* ─── Helpers ─────────────────────────────────────────────────────── */
18
+
19
+ /**
20
+ * Resolves the HTTP status code from an error object.
21
+ * Supports err.statusCode, err.status, and defaults to 500.
22
+ * @param {Error} err
23
+ * @returns {number}
24
+ */
25
+ function resolveStatus(err) {
26
+ const code = err.statusCode || err.status;
27
+ return typeof code === 'number' && code >= 100 && code < 600 ? code : 500;
28
+ }
29
+
30
+ /**
31
+ * Determines if an error message is safe to expose to clients.
32
+ * Non-operational / 5xx errors get a generic message in production.
33
+ * @param {Error} err
34
+ * @param {number} statusCode
35
+ * @param {boolean} isProduction
36
+ * @returns {string}
37
+ */
38
+ function resolveMessage(err, statusCode, isProduction) {
39
+ if (!isProduction) return err.message || 'Internal Server Error';
40
+
41
+ // In production, only expose messages for operational / client errors
42
+ if (err instanceof AppError && err.isOperational) return err.message;
43
+ if (statusCode < 500) return err.message || 'Error';
44
+
45
+ return 'Internal Server Error';
46
+ }
47
+
48
+ /* ─── Factory ─────────────────────────────────────────────────────── */
49
+
50
+ /**
51
+ * Creates a configurable global Express error-handling middleware.
52
+ *
53
+ * Options:
54
+ * - `includeStack` — Include stack traces in the response.
55
+ * Defaults to `true` outside production.
56
+ * - `onError(payload)` — Custom error logging / alerting hook.
57
+ * Receives `{ err, statusCode, method, url, requestId, message, stack, timestamp }`.
58
+ * - `defaultStatusCode` — Fallback status code (default `500`).
59
+ *
60
+ * @param {object} [options]
61
+ * @param {boolean} [options.includeStack]
62
+ * @param {Function} [options.onError]
63
+ * @param {number} [options.defaultStatusCode=500]
64
+ * @returns {Function} Express error-handling middleware `(err, req, res, next)`
65
+ *
66
+ * @example
67
+ * const { createErrorHandler } = require('express-pro-toolkit');
68
+ *
69
+ * // Basic
70
+ * app.use(createErrorHandler());
71
+ *
72
+ * // With external logging service
73
+ * app.use(createErrorHandler({
74
+ * onError: ({ err, requestId }) => Sentry.captureException(err, { tags: { requestId } }),
75
+ * }));
76
+ */
77
+ function createErrorHandler(options) {
78
+ const opts = options && typeof options === 'object' ? options : {};
79
+ const isProduction = process.env.NODE_ENV === 'production';
80
+ const showStack = typeof opts.includeStack === 'boolean' ? opts.includeStack : !isProduction;
81
+ const onError = typeof opts.onError === 'function' ? opts.onError : defaultOnError;
82
+
83
+ return function errorHandlerMiddleware(err, req, res, _next) {
84
+ const statusCode = resolveStatus(err) || (opts.defaultStatusCode || 500);
85
+ const message = resolveMessage(err, statusCode, isProduction);
86
+ const requestId = req.id || null;
87
+
88
+ // ── Build payload for the logging hook ────────────────────────
89
+ const logPayload = {
90
+ err,
91
+ statusCode,
92
+ method: req.method,
93
+ url: req.originalUrl || req.url,
94
+ requestId,
95
+ message,
96
+ stack: err.stack || null,
97
+ timestamp: new Date().toISOString(),
98
+ };
99
+
100
+ // Fire logging / alerting hook (non-blocking)
101
+ try { onError(logPayload); } catch (_) { /* never let logger crash the response */ }
102
+
103
+ // ── Build JSON response ───────────────────────────────────────
104
+ /** @type {object} */
105
+ const body = {
106
+ success: false,
107
+ message,
108
+ };
109
+
110
+ // Error code (machine-readable)
111
+ if (err.code) {
112
+ body.code = err.code;
113
+ }
114
+
115
+ // Detailed sub-errors (validation, etc.)
116
+ if (err.errors) {
117
+ body.errors = Array.isArray(err.errors) ? err.errors : [err.errors];
118
+ } else {
119
+ body.errors = null;
120
+ }
121
+
122
+ // Correlation ID for client-side tracing
123
+ if (requestId) {
124
+ body.requestId = requestId;
125
+ }
126
+
127
+ // Stack trace (dev only by default)
128
+ if (showStack) {
129
+ body.stack = err.stack || null;
130
+ }
131
+
132
+ // Prevent double-send if headers already flushed
133
+ if (res.headersSent) {
134
+ return;
135
+ }
136
+
137
+ res.status(statusCode).json(body);
138
+ };
139
+ }
140
+
141
+ /* ─── Backward-compatible default instance ────────────────────────── */
142
+
143
+ /**
144
+ * Pre-configured error handler using default options.
145
+ * Drop-in replacement for the v1 export.
146
+ *
147
+ * @type {Function}
148
+ */
149
+ const errorHandler = createErrorHandler();
150
+
151
+ module.exports = errorHandler;
152
+ module.exports.errorHandler = errorHandler;
153
+ module.exports.createErrorHandler = createErrorHandler;
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Common HTTP status code constants.
5
+ *
6
+ * Avoids magic numbers throughout application code.
7
+ *
8
+ * @example
9
+ * const { httpStatus } = require('express-pro-toolkit');
10
+ * res.status(httpStatus.CREATED).json(data);
11
+ *
12
+ * @readonly
13
+ * @enum {number}
14
+ */
15
+ const httpStatus = Object.freeze({
16
+ // ── 2xx Success ──────────────────────────────
17
+ OK: 200,
18
+ CREATED: 201,
19
+ ACCEPTED: 202,
20
+ NO_CONTENT: 204,
21
+
22
+ // ── 3xx Redirection ──────────────────────────
23
+ MOVED_PERMANENTLY: 301,
24
+ FOUND: 302,
25
+ NOT_MODIFIED: 304,
26
+ TEMPORARY_REDIRECT: 307,
27
+ PERMANENT_REDIRECT: 308,
28
+
29
+ // ── 4xx Client Errors ────────────────────────
30
+ BAD_REQUEST: 400,
31
+ UNAUTHORIZED: 401,
32
+ FORBIDDEN: 403,
33
+ NOT_FOUND: 404,
34
+ METHOD_NOT_ALLOWED: 405,
35
+ CONFLICT: 409,
36
+ GONE: 410,
37
+ UNPROCESSABLE_ENTITY: 422,
38
+ TOO_MANY_REQUESTS: 429,
39
+
40
+ // ── 5xx Server Errors ────────────────────────
41
+ INTERNAL_SERVER_ERROR: 500,
42
+ NOT_IMPLEMENTED: 501,
43
+ BAD_GATEWAY: 502,
44
+ SERVICE_UNAVAILABLE: 503,
45
+ GATEWAY_TIMEOUT: 504,
46
+ });
47
+
48
+ module.exports = httpStatus;
@@ -0,0 +1,34 @@
1
+ 'use strict';
2
+
3
+ const AppError = require('./AppError');
4
+
5
+ /**
6
+ * Middleware that catches requests which did not match any route
7
+ * and forwards a 404 AppError to the error handler.
8
+ *
9
+ * Must be registered **after** all routes and **before** the error handler.
10
+ *
11
+ * @param {object} [options]
12
+ * @param {string} [options.message] - Custom "not found" message
13
+ * @returns {Function} Express middleware
14
+ *
15
+ * @example
16
+ * const { notFoundHandler, errorHandler } = require('express-pro-toolkit');
17
+ *
18
+ * // After all routes
19
+ * app.use(notFoundHandler());
20
+ * app.use(errorHandler());
21
+ */
22
+ function notFoundHandler(options) {
23
+ const opts = options && typeof options === 'object' ? options : {};
24
+
25
+ return function notFoundMiddleware(req, res, next) {
26
+ const msg =
27
+ (typeof opts.message === 'string' && opts.message) ||
28
+ `Cannot ${req.method} ${req.originalUrl || req.url}`;
29
+
30
+ next(new AppError(msg, 404, { code: 'ROUTE_NOT_FOUND' }));
31
+ };
32
+ }
33
+
34
+ module.exports = notFoundHandler;
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ /* ─── Colour helpers (ANSI 256 — only when stdout is a TTY) ────── */
4
+
5
+ const isTTY = process.stdout.isTTY;
6
+
7
+ /**
8
+ * @param {number} code ANSI colour code
9
+ * @param {string} text
10
+ * @returns {string}
11
+ */
12
+ function colour(code, text) {
13
+ return isTTY ? `\x1b[${code}m${text}\x1b[0m` : text;
14
+ }
15
+
16
+ /**
17
+ * Pick a colour based on HTTP status code.
18
+ * @param {number} status
19
+ * @returns {string}
20
+ */
21
+ function colourStatus(status) {
22
+ if (status >= 500) return colour(31, String(status)); // red
23
+ if (status >= 400) return colour(33, String(status)); // yellow
24
+ if (status >= 300) return colour(36, String(status)); // cyan
25
+ if (status >= 200) return colour(32, String(status)); // green
26
+ return String(status);
27
+ }
28
+
29
+ /**
30
+ * Pick a colour for response time.
31
+ * @param {number} ms
32
+ * @returns {string}
33
+ */
34
+ function colourTime(ms) {
35
+ const text = ms.toFixed(2) + 'ms';
36
+ if (ms >= 1000) return colour(31, text); // red ≥ 1 s
37
+ if (ms >= 500) return colour(33, text); // yellow ≥ 500 ms
38
+ return colour(32, text); // green
39
+ }
40
+
41
+ /* ─── Default log function ────────────────────────────────────────── */
42
+
43
+ /**
44
+ * @param {object} info
45
+ */
46
+ function defaultLog(info) {
47
+ console.log(
48
+ `[${info.timestamp}] ${colour(1, info.method)} ${info.url} ${colourStatus(info.status)} — ${colourTime(info.duration)}${info.requestId ? ' id=' + info.requestId : ''}`,
49
+ );
50
+ }
51
+
52
+ /* ─── Factory ─────────────────────────────────────────────────────── */
53
+
54
+ /**
55
+ * Creates a configurable request-logging middleware.
56
+ *
57
+ * @param {object} [options]
58
+ * @param {Function} [options.log] - Custom log function `(info) => void`.
59
+ * Receives `{ method, url, status, duration, timestamp, requestId }`.
60
+ * @param {Function} [options.skip] - `(req, res) => boolean` — skip logging
61
+ * for certain requests (e.g. health checks).
62
+ * @param {boolean} [options.colorize=true] - Enable ANSI colours (auto-detected)
63
+ * @returns {Function} Express middleware
64
+ *
65
+ * @example
66
+ * const { createRequestLogger } = require('express-pro-toolkit');
67
+ *
68
+ * // Default
69
+ * app.use(createRequestLogger());
70
+ *
71
+ * // Skip health checks, custom logger
72
+ * app.use(createRequestLogger({
73
+ * skip: (req) => req.url === '/health',
74
+ * log: (info) => pinoLogger.info(info, 'request'),
75
+ * }));
76
+ */
77
+ function createRequestLogger(options) {
78
+ const opts = options && typeof options === 'object' ? options : {};
79
+ const logFn = typeof opts.log === 'function' ? opts.log : defaultLog;
80
+ const skipFn = typeof opts.skip === 'function' ? opts.skip : null;
81
+
82
+ return function requestLoggerMiddleware(req, res, next) {
83
+ // Use hrtime tuple for high-resolution, Node 12+ compatible timing
84
+ const start = process.hrtime();
85
+
86
+ res.on('finish', function onFinish() {
87
+ // Allow caller to skip specific requests (health, metrics, etc.)
88
+ if (skipFn && skipFn(req, res)) return;
89
+
90
+ const diff = process.hrtime(start);
91
+ const durationMs = diff[0] * 1e3 + diff[1] / 1e6;
92
+
93
+ logFn({
94
+ method: req.method,
95
+ url: req.originalUrl || req.url,
96
+ status: res.statusCode,
97
+ duration: durationMs,
98
+ timestamp: new Date().toISOString(),
99
+ requestId: req.id || null,
100
+ });
101
+ });
102
+
103
+ next();
104
+ };
105
+ }
106
+
107
+ /* ─── Backward-compatible default instance ────────────────────────── */
108
+
109
+ /**
110
+ * Pre-configured request logger using default options.
111
+ * Drop-in replacement for the v1 export.
112
+ *
113
+ * @type {Function}
114
+ */
115
+ const requestLogger = createRequestLogger();
116
+
117
+ module.exports = requestLogger;
118
+ module.exports.requestLogger = requestLogger;
119
+ module.exports.createRequestLogger = createRequestLogger;
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Send a consistent JSON **success** response.
5
+ *
6
+ * @param {import('express').Response} res - Express response object
7
+ * @param {*} data - Payload to return under `data`
8
+ * @param {string} [message] - Human-readable message (default: `"Success"`)
9
+ * @param {number} [statusCode=200] - HTTP status code
10
+ * @param {object} [meta] - Optional pagination / extra metadata
11
+ * @returns {void}
12
+ *
13
+ * @example
14
+ * const { sendSuccess } = require('express-pro-toolkit');
15
+ *
16
+ * sendSuccess(res, users, 'Users fetched', 200, {
17
+ * page: 1, limit: 25, total: 100,
18
+ * });
19
+ *
20
+ * // Response:
21
+ * // {
22
+ * // "success": true,
23
+ * // "message": "Users fetched",
24
+ * // "data": [...],
25
+ * // "meta": { "page": 1, "limit": 25, "total": 100 }
26
+ * // }
27
+ */
28
+ function sendSuccess(res, data, message, statusCode, meta) {
29
+ const code = typeof statusCode === 'number' ? statusCode : 200;
30
+ const msg = typeof message === 'string' && message.length > 0 ? message : 'Success';
31
+
32
+ const body = {
33
+ success: true,
34
+ message: msg,
35
+ data: data !== undefined ? data : null,
36
+ };
37
+
38
+ if (meta !== undefined && meta !== null) {
39
+ body.meta = meta;
40
+ }
41
+
42
+ res.status(code).json(body);
43
+ }
44
+
45
+ /**
46
+ * Send a consistent JSON **error** response.
47
+ *
48
+ * @param {import('express').Response} res - Express response object
49
+ * @param {string} [message] - Human-readable error message
50
+ * @param {number} [statusCode=500] - HTTP status code
51
+ * @param {Array|null} [errors=null] - Optional array of detailed error objects
52
+ * @param {string} [code] - Machine-readable error code
53
+ * @returns {void}
54
+ *
55
+ * @example
56
+ * sendError(res, 'Validation failed', 422, [
57
+ * { field: 'email', message: 'Email is required' },
58
+ * ], 'VALIDATION_ERROR');
59
+ */
60
+ function sendError(res, message, statusCode, errors, code) {
61
+ const httpCode = typeof statusCode === 'number' ? statusCode : 500;
62
+ const msg = typeof message === 'string' && message.length > 0 ? message : 'Internal Server Error';
63
+
64
+ const body = {
65
+ success: false,
66
+ message: msg,
67
+ errors: errors != null ? (Array.isArray(errors) ? errors : [errors]) : null,
68
+ };
69
+
70
+ if (typeof code === 'string' && code.length > 0) {
71
+ body.code = code;
72
+ }
73
+
74
+ res.status(httpCode).json(body);
75
+ }
76
+
77
+ /**
78
+ * Send a paginated JSON success response.
79
+ *
80
+ * @param {import('express').Response} res
81
+ * @param {Array} items
82
+ * @param {object} pagination
83
+ * @param {number} pagination.page
84
+ * @param {number} pagination.limit
85
+ * @param {number} pagination.total
86
+ * @param {string} [message]
87
+ * @returns {void}
88
+ *
89
+ * @example
90
+ * sendPaginated(res, users, { page: 2, limit: 25, total: 100 });
91
+ */
92
+ function sendPaginated(res, items, pagination, message) {
93
+ const page = pagination.page || 1;
94
+ const limit = pagination.limit || 25;
95
+ const total = pagination.total || 0;
96
+ const totalPages = Math.ceil(total / limit) || 1;
97
+
98
+ sendSuccess(res, items, message || 'Success', 200, {
99
+ page,
100
+ limit,
101
+ total,
102
+ totalPages,
103
+ hasNextPage: page < totalPages,
104
+ hasPrevPage: page > 1,
105
+ });
106
+ }
107
+
108
+ module.exports = { sendSuccess, sendError, sendPaginated };