@catbee/utils 0.0.8-rc.3 → 1.0.1

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.
Files changed (129) hide show
  1. package/README.md +3 -2
  2. package/build/index.cjs +6912 -0
  3. package/build/index.d.ts +5284 -0
  4. package/build/index.mjs +6635 -0
  5. package/package.json +13 -292
  6. package/build/cjs/config.d.ts +0 -121
  7. package/build/cjs/config.js +0 -137
  8. package/build/cjs/index.d.ts +0 -26
  9. package/build/cjs/index.js +0 -68
  10. package/build/cjs/servers/server.builder.d.ts +0 -507
  11. package/build/cjs/servers/server.builder.js +0 -658
  12. package/build/cjs/servers/server.d.ts +0 -255
  13. package/build/cjs/servers/server.js +0 -970
  14. package/build/cjs/types/api-response.d.ts +0 -151
  15. package/build/cjs/types/api-response.js +0 -36
  16. package/build/cjs/types/index.d.ts +0 -124
  17. package/build/cjs/types/index.js +0 -25
  18. package/build/cjs/types/server.d.ts +0 -267
  19. package/build/cjs/types/server.js +0 -25
  20. package/build/cjs/utils/array.utils.d.ts +0 -167
  21. package/build/cjs/utils/array.utils.js +0 -363
  22. package/build/cjs/utils/async.utils.d.ts +0 -264
  23. package/build/cjs/utils/async.utils.js +0 -640
  24. package/build/cjs/utils/cache.utils.d.ts +0 -152
  25. package/build/cjs/utils/cache.utils.js +0 -298
  26. package/build/cjs/utils/context-store.utils.d.ts +0 -188
  27. package/build/cjs/utils/context-store.utils.js +0 -297
  28. package/build/cjs/utils/crypto.utils.d.ts +0 -159
  29. package/build/cjs/utils/crypto.utils.js +0 -295
  30. package/build/cjs/utils/date.utils.d.ts +0 -158
  31. package/build/cjs/utils/date.utils.js +0 -395
  32. package/build/cjs/utils/decorators.utils.d.ts +0 -511
  33. package/build/cjs/utils/decorators.utils.js +0 -1035
  34. package/build/cjs/utils/dir.utils.d.ts +0 -195
  35. package/build/cjs/utils/dir.utils.js +0 -500
  36. package/build/cjs/utils/env.utils.d.ts +0 -376
  37. package/build/cjs/utils/env.utils.js +0 -786
  38. package/build/cjs/utils/exception.utils.d.ts +0 -229
  39. package/build/cjs/utils/exception.utils.js +0 -408
  40. package/build/cjs/utils/fs.utils.d.ts +0 -163
  41. package/build/cjs/utils/fs.utils.js +0 -371
  42. package/build/cjs/utils/http-status-codes.d.ts +0 -265
  43. package/build/cjs/utils/http-status-codes.js +0 -297
  44. package/build/cjs/utils/id.utils.d.ts +0 -35
  45. package/build/cjs/utils/id.utils.js +0 -90
  46. package/build/cjs/utils/logger.utils.d.ts +0 -159
  47. package/build/cjs/utils/logger.utils.js +0 -350
  48. package/build/cjs/utils/middleware.utils.d.ts +0 -99
  49. package/build/cjs/utils/middleware.utils.js +0 -243
  50. package/build/cjs/utils/obj.utils.d.ts +0 -123
  51. package/build/cjs/utils/obj.utils.js +0 -428
  52. package/build/cjs/utils/performance.utils.d.ts +0 -135
  53. package/build/cjs/utils/performance.utils.js +0 -280
  54. package/build/cjs/utils/request.utils.d.ts +0 -85
  55. package/build/cjs/utils/request.utils.js +0 -199
  56. package/build/cjs/utils/response.utils.d.ts +0 -162
  57. package/build/cjs/utils/response.utils.js +0 -274
  58. package/build/cjs/utils/stream.utils.d.ts +0 -87
  59. package/build/cjs/utils/stream.utils.js +0 -217
  60. package/build/cjs/utils/string.utils.d.ts +0 -92
  61. package/build/cjs/utils/string.utils.js +0 -178
  62. package/build/cjs/utils/type.utils.d.ts +0 -89
  63. package/build/cjs/utils/type.utils.js +0 -195
  64. package/build/cjs/utils/url.utils.d.ts +0 -140
  65. package/build/cjs/utils/url.utils.js +0 -316
  66. package/build/cjs/utils/validate.utils.d.ts +0 -176
  67. package/build/cjs/utils/validate.utils.js +0 -344
  68. package/build/esm/config.d.ts +0 -121
  69. package/build/esm/config.js +0 -132
  70. package/build/esm/index.d.ts +0 -26
  71. package/build/esm/index.js +0 -49
  72. package/build/esm/servers/server.builder.d.ts +0 -507
  73. package/build/esm/servers/server.builder.js +0 -654
  74. package/build/esm/servers/server.d.ts +0 -255
  75. package/build/esm/servers/server.js +0 -963
  76. package/build/esm/types/api-response.d.ts +0 -151
  77. package/build/esm/types/api-response.js +0 -33
  78. package/build/esm/types/index.d.ts +0 -124
  79. package/build/esm/types/index.js +0 -24
  80. package/build/esm/types/server.d.ts +0 -267
  81. package/build/esm/types/server.js +0 -24
  82. package/build/esm/utils/array.utils.d.ts +0 -167
  83. package/build/esm/utils/array.utils.js +0 -344
  84. package/build/esm/utils/async.utils.d.ts +0 -264
  85. package/build/esm/utils/async.utils.js +0 -619
  86. package/build/esm/utils/cache.utils.d.ts +0 -152
  87. package/build/esm/utils/cache.utils.js +0 -294
  88. package/build/esm/utils/context-store.utils.d.ts +0 -188
  89. package/build/esm/utils/context-store.utils.js +0 -290
  90. package/build/esm/utils/crypto.utils.d.ts +0 -159
  91. package/build/esm/utils/crypto.utils.js +0 -278
  92. package/build/esm/utils/date.utils.d.ts +0 -158
  93. package/build/esm/utils/date.utils.js +0 -383
  94. package/build/esm/utils/decorators.utils.d.ts +0 -511
  95. package/build/esm/utils/decorators.utils.js +0 -1013
  96. package/build/esm/utils/dir.utils.d.ts +0 -195
  97. package/build/esm/utils/dir.utils.js +0 -476
  98. package/build/esm/utils/env.utils.d.ts +0 -376
  99. package/build/esm/utils/env.utils.js +0 -782
  100. package/build/esm/utils/exception.utils.d.ts +0 -229
  101. package/build/esm/utils/exception.utils.js +0 -382
  102. package/build/esm/utils/fs.utils.d.ts +0 -163
  103. package/build/esm/utils/fs.utils.js +0 -348
  104. package/build/esm/utils/http-status-codes.d.ts +0 -265
  105. package/build/esm/utils/http-status-codes.js +0 -294
  106. package/build/esm/utils/id.utils.d.ts +0 -35
  107. package/build/esm/utils/id.utils.js +0 -83
  108. package/build/esm/utils/logger.utils.d.ts +0 -159
  109. package/build/esm/utils/logger.utils.js +0 -304
  110. package/build/esm/utils/middleware.utils.d.ts +0 -99
  111. package/build/esm/utils/middleware.utils.js +0 -235
  112. package/build/esm/utils/obj.utils.d.ts +0 -123
  113. package/build/esm/utils/obj.utils.js +0 -412
  114. package/build/esm/utils/performance.utils.d.ts +0 -135
  115. package/build/esm/utils/performance.utils.js +0 -273
  116. package/build/esm/utils/request.utils.d.ts +0 -85
  117. package/build/esm/utils/request.utils.js +0 -190
  118. package/build/esm/utils/response.utils.d.ts +0 -162
  119. package/build/esm/utils/response.utils.js +0 -261
  120. package/build/esm/utils/stream.utils.d.ts +0 -87
  121. package/build/esm/utils/stream.utils.js +0 -209
  122. package/build/esm/utils/string.utils.d.ts +0 -92
  123. package/build/esm/utils/string.utils.js +0 -164
  124. package/build/esm/utils/type.utils.d.ts +0 -89
  125. package/build/esm/utils/type.utils.js +0 -186
  126. package/build/esm/utils/url.utils.d.ts +0 -140
  127. package/build/esm/utils/url.utils.js +0 -303
  128. package/build/esm/utils/validate.utils.d.ts +0 -176
  129. package/build/esm/utils/validate.utils.js +0 -319
@@ -1,970 +0,0 @@
1
- "use strict";
2
- /*
3
- * The MIT License
4
- *
5
- * Copyright (c) 2025 Catbee Technologies. https://catbee-utils.npm.hprasath.com/license
6
- *
7
- * Permission is hereby granted, free of charge, to any person obtaining a copy
8
- * of this software and associated documentation files (the "Software"), to deal
9
- * in the Software without restriction, including without limitation the rights
10
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
- * copies of the Software, and to permit persons to whom the Software is
12
- * furnished to do so, subject to the following conditions:
13
- *
14
- * The above copyright notice and this permission notice shall be included in all
15
- * copies or substantial portions of the Software.
16
- *
17
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
- * SOFTWARE.
24
- */
25
- var __importDefault = (this && this.__importDefault) || function (mod) {
26
- return (mod && mod.__esModule) ? mod : { "default": mod };
27
- };
28
- Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.ExpressServer = void 0;
30
- const express_1 = __importDefault(require("express"));
31
- const https_1 = __importDefault(require("https"));
32
- const http_status_codes_1 = require("../utils/http-status-codes");
33
- const response_utils_1 = require("../utils/response.utils");
34
- const middleware_utils_1 = require("../utils/middleware.utils");
35
- const env_utils_1 = require("../utils/env.utils");
36
- const logger_utils_1 = require("../utils/logger.utils");
37
- const exception_utils_1 = require("../utils/exception.utils");
38
- const exception_utils_2 = require("../utils/exception.utils");
39
- const fs_1 = __importDefault(require("fs"));
40
- const config_1 = require("../config");
41
- const obj_utils_1 = require("../utils/obj.utils");
42
- const fs_utils_1 = require("../utils/fs.utils");
43
- const server_builder_1 = require("./server.builder");
44
- const validate_utils_1 = require("../utils/validate.utils");
45
- /**
46
- * Production-ready Express server with enterprise features.
47
- *
48
- * Core Features:
49
- * - Security: Helmet, CORS, rate limiting, timeouts
50
- * - Monitoring: Request logs, metrics, health checks
51
- * - Performance: Compression, caching, static files
52
- * - Reliability: Graceful shutdown, error handling
53
- * - Developer UX: OpenAPI docs, debugging tools
54
- * - Extensibility: Hooks, middleware, custom routes
55
- *
56
- * Designed for microservices and production workloads.
57
- * Includes K8s readiness probes and zero-downtime support.
58
- */
59
- class ExpressServer {
60
- /** Prometheus client registry for metrics collection */
61
- register = null;
62
- /** HTTP server instance (null when not running) */
63
- server = null;
64
- /** Merged configuration with defaults applied */
65
- config;
66
- /** User-defined lifecycle hooks */
67
- hooks;
68
- /** Global API prefix (from config) */
69
- globalPrefix;
70
- /** Internal fallback router */
71
- rootRouter;
72
- /** User-supplied router */
73
- externalRouter;
74
- /** Internal Express app instance */
75
- app;
76
- /** Set of active WebSocket connections */
77
- connections = new Set();
78
- /** Flag indicating if the server is shutting down */
79
- isShuttingDown = false;
80
- /**
81
- * Collection of registered health check functions.
82
- * These are executed when the health check endpoint is accessed.
83
- */
84
- healthChecks = [];
85
- /** Prometheus metrics for monitoring */
86
- requestCounter;
87
- routeTimings;
88
- requestSizes;
89
- clientIPs;
90
- /** Promise that resolves when initialization (middleware + routes) is complete */
91
- initPromise;
92
- /**
93
- * Initializes server with intelligent defaults and security best practices.
94
- * All settings can be customized via config and hooks.
95
- *
96
- * Default Security:
97
- * - Secure headers (Helmet)
98
- * - Rate limiting
99
- * - Request timeouts
100
- * - Body size limits
101
- * - CORS protection
102
- *
103
- * Default Monitoring:
104
- * - Request/Response logging
105
- * - Prometheus metrics
106
- * - Health checks
107
- * - Request tracing
108
- */
109
- constructor(config, hooks = {}) {
110
- if (ExpressServer.isBuiltServerConfig(config)) {
111
- this.config = config;
112
- }
113
- else {
114
- // Deep merge config with user overrides
115
- this.config = (0, obj_utils_1.deepObjMerge)({}, config_1.defaultServerConfig, config);
116
- }
117
- if (!(0, validate_utils_1.isPort)(this.config.port)) {
118
- (0, logger_utils_1.getLogger)().error(`Port must be a valid number between 1 and 65535, got: ${this.config.port}`);
119
- process.exit(1);
120
- }
121
- // Sanitize app name for metrics (replace invalid characters with underscore)
122
- const safeAppName = (this.config.appName || 'express_app').toLowerCase().replace(/[^a-z0-9_]/g, '_');
123
- if (this.config.metrics?.enable) {
124
- const client = ExpressServer.optionalRequire('prom-client');
125
- if (!client) {
126
- (0, logger_utils_1.getLogger)().error({ command: 'npm install prom-client' }, 'prom-client is required for metrics but not installed. Please add it to your dependencies');
127
- process.exit(1);
128
- }
129
- this.register = new client.Registry();
130
- // Initialize Prometheus metrics with sanitized names
131
- this.requestCounter = new client.Counter({
132
- name: `${safeAppName}_http_requests_total`,
133
- help: 'Total HTTP requests',
134
- labelNames: ['method', 'route', 'status'],
135
- registers: [this.register]
136
- });
137
- this.routeTimings = new client.Histogram({
138
- name: `${safeAppName}_http_request_duration_seconds`,
139
- help: 'Duration of HTTP requests by route',
140
- labelNames: ['method', 'route', 'status'],
141
- buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10],
142
- registers: [this.register]
143
- });
144
- this.requestSizes = new client.Histogram({
145
- name: `${safeAppName}_http_request_size_bytes`,
146
- help: 'Size of HTTP request bodies',
147
- labelNames: ['method', 'route'],
148
- buckets: [100, 1000, 10000, 100000, 1000000],
149
- registers: [this.register]
150
- });
151
- this.clientIPs = new client.Counter({
152
- name: `${safeAppName}_http_client_ip_total`,
153
- help: 'Client IP request counter',
154
- labelNames: ['ip', 'method'],
155
- registers: [this.register]
156
- });
157
- // Default system metrics (CPU, memory, event loop lag, etc.)
158
- client.collectDefaultMetrics({
159
- register: this.register,
160
- prefix: `${safeAppName}_`
161
- });
162
- }
163
- // Health checks
164
- if (config.healthCheck?.checks) {
165
- this.healthChecks.push(...config.healthCheck.checks);
166
- }
167
- // Set global prefix (normalize to empty string or "/prefix" without trailing slash)
168
- this.globalPrefix = this.normalizePath(this.config.globalPrefix ?? '', false);
169
- this.hooks = hooks;
170
- this.app = (0, express_1.default)();
171
- this.rootRouter = express_1.default.Router();
172
- // Store initialization promise to prevent race conditions with start()
173
- this.initPromise = this.initialize();
174
- }
175
- /**
176
- * Execute a lifecycle hook safely with comprehensive error handling.
177
- * Prevents hook failures from crashing the server while logging issues.
178
- *
179
- * @param hook Name of the lifecycle hook to execute
180
- * @param args Arguments to pass to the hook function
181
- */
182
- async runHook(hook, ...args) {
183
- try {
184
- const fn = this.hooks[hook];
185
- if (fn)
186
- await fn.apply(null, args);
187
- }
188
- catch (err) {
189
- (0, logger_utils_1.getLogger)().error({ err, hook }, `Error executing ${hook} hook:`);
190
- }
191
- }
192
- /**
193
- * Initialize the Express server with middleware and routes.
194
- */
195
- async initialize() {
196
- await this.runHook('beforeInit', this);
197
- // Set up middleware stack (order is critical)
198
- await this.setupMiddleware();
199
- // Set up default routes and error handling
200
- await this.setupRoutes();
201
- await this.runHook('afterInit', this);
202
- }
203
- /**
204
- * Configure and register all middlewares in the optimal order.
205
- *
206
- * Middleware Order (CRITICAL - don't change without understanding implications):
207
- * 1. Basic server configuration (trust proxy, x-powered-by)
208
- * 2. Request ID generation (for tracing)
209
- * 3. Request context setup (for logging correlation)
210
- * 4. Timeout protection (prevents hanging requests)
211
- * 5. Response time tracking (for performance monitoring)
212
- * 6. Request logging (after ID/context setup)
213
- * 7. Custom request hooks
214
- * 8. Security middleware (rate limiting, CORS, Helmet)
215
- * 9. Response compression
216
- * 10. Static file serving
217
- * 11. Request parsing (body parsing, cookies)
218
- * 12. API documentation (OpenAPI)
219
- * 13. Global headers
220
- * 14. Custom response hooks
221
- */
222
- async setupMiddleware() {
223
- if (this.config.https) {
224
- await this.validateHttpsFiles();
225
- }
226
- // Basic middleware should be first
227
- this.app.disable('x-powered-by');
228
- if (this.config.trustProxy) {
229
- this.app.set('trust proxy', true);
230
- }
231
- // Request ID generation - must be first for proper tracing
232
- this.app.use((0, middleware_utils_1.requestId)({
233
- headerName: this.config.requestId?.headerName,
234
- exposeHeader: this.config.requestId?.exposeHeader,
235
- generator: this.config.requestId?.generator
236
- }));
237
- // Request context setup for logging correlation
238
- this.app.use((0, middleware_utils_1.setupRequestContext)({
239
- headerName: this.config.requestId?.headerName,
240
- autoLog: false
241
- }));
242
- // Early shutdown-awareness middleware (lets load balancers drain connections gracefully)
243
- this.app.use((_req, res, next) => {
244
- if (this.isShuttingDown) {
245
- res.setHeader('Connection', 'close');
246
- return res
247
- .status(http_status_codes_1.HttpStatusCodes.SERVICE_UNAVAILABLE)
248
- .json(new exception_utils_1.ServiceUnavailableException('Server is shutting down'));
249
- }
250
- next();
251
- return;
252
- });
253
- // Security middleware should come early
254
- if (this.config.helmet) {
255
- const helmet = ExpressServer.optionalRequire('helmet');
256
- if (!helmet) {
257
- (0, logger_utils_1.getLogger)().error({ command: 'npm install helmet' }, 'helmet is required but not installed. Please add it to your dependencies');
258
- process.exit(1);
259
- }
260
- if (typeof this.config.helmet === 'object') {
261
- this.app.use(helmet(this.config.helmet));
262
- }
263
- else {
264
- this.app.use(helmet());
265
- }
266
- }
267
- // CORS middleware should be early
268
- if (this.config.cors) {
269
- const cors = ExpressServer.optionalRequire('cors');
270
- if (!cors) {
271
- (0, logger_utils_1.getLogger)().error({ command: 'npm install cors' }, 'cors is required but not installed. Please add it to your dependencies');
272
- process.exit(1);
273
- }
274
- this.app.use(cors(this.config.cors === true ? {} : this.config.cors));
275
- }
276
- // Global headers
277
- this.app.use((_req, res, next) => {
278
- if (this.config.globalHeaders) {
279
- for (const key in this.config.globalHeaders) {
280
- const value = this.config.globalHeaders[key];
281
- res.setHeader(key, typeof value === 'function' ? value() : value);
282
- }
283
- }
284
- if (this.config.isMicroservice) {
285
- res.setHeader('X-Microservice', this.config.appName || 'express_app');
286
- }
287
- if (this.config.serviceVersion?.enable) {
288
- const version = typeof this.config.serviceVersion?.version === 'function'
289
- ? this.config.serviceVersion.version()
290
- : this.config.serviceVersion?.version;
291
- res.setHeader(this.config.serviceVersion?.headerName || 'x-service-version', version || '0.0.0');
292
- }
293
- next();
294
- });
295
- // Global request timeout protection
296
- if (this.config.requestTimeout) {
297
- this.app.use((0, middleware_utils_1.timeout)(this.config.requestTimeout));
298
- }
299
- // Response time tracking for performance monitoring
300
- if (this.config.responseTime?.enable) {
301
- this.app.use((0, middleware_utils_1.responseTime)({
302
- addHeader: this.config.responseTime.addHeader,
303
- logOnComplete: this.config.responseTime.logOnComplete
304
- }));
305
- }
306
- // Rate limiting should be early to prevent unnecessary processing
307
- if (this.config.rateLimit?.enable) {
308
- const rateLimit = ExpressServer.optionalRequire('express-rate-limit');
309
- if (!rateLimit) {
310
- (0, logger_utils_1.getLogger)().error({ command: 'npm install express-rate-limit' }, 'express-rate-limit is required but not installed. Please add it to your dependencies');
311
- process.exit(1);
312
- }
313
- this.app.use(rateLimit({
314
- windowMs: this.config.rateLimit.windowMs ?? 15 * 60 * 1000,
315
- max: this.config.rateLimit.max ?? 100,
316
- handler: (req, res) => {
317
- const status = http_status_codes_1.HttpStatusCodes.TOO_MANY_REQUESTS;
318
- const response = (0, response_utils_1.createFinalErrorResponse)(req, status, this.config.rateLimit?.message || 'Too many requests');
319
- res.status(status).json(response);
320
- },
321
- standardHeaders: this.config.rateLimit.standardHeaders ?? true,
322
- legacyHeaders: this.config.rateLimit.legacyHeaders ?? false
323
- }));
324
- }
325
- // Request logging with filtering
326
- if (this.config.requestLogging?.enable) {
327
- this.app.use((req, res, next) => {
328
- if (typeof this.config.requestLogging?.ignorePaths === 'function') {
329
- const skip = this.config.requestLogging?.ignorePaths?.(req, res);
330
- if (skip)
331
- return next();
332
- }
333
- else if (Array.isArray(this.config.requestLogging?.ignorePaths)) {
334
- const skip = this.config.requestLogging?.ignorePaths?.includes(req.path);
335
- if (skip)
336
- return next();
337
- }
338
- const logger = (0, logger_utils_1.getLogger)();
339
- const incomingRequestMetaData = {
340
- requestId: req.id,
341
- method: req.method,
342
- url: req.originalUrl || req.url,
343
- ip: req.ip
344
- };
345
- logger.info(incomingRequestMetaData, 'Incoming Request');
346
- next();
347
- });
348
- }
349
- // Custom request preprocessing hook
350
- if (this.hooks.onRequest) {
351
- this.app.use(this.hooks.onRequest);
352
- }
353
- // Response compression for better performance
354
- if (this.config.compression) {
355
- const compression = ExpressServer.optionalRequire('compression');
356
- if (!compression) {
357
- (0, logger_utils_1.getLogger)().error({ command: 'npm install compression' }, 'compression is required but not installed. Please add it to your dependencies');
358
- process.exit(1);
359
- }
360
- if (typeof this.config.compression === 'object') {
361
- this.app.use(compression(this.config.compression));
362
- }
363
- else {
364
- this.app.use(compression());
365
- }
366
- }
367
- // Static file serving (do NOT normalize filesystem path; only normalize route)
368
- if (this.config.staticFolders) {
369
- this.config.staticFolders.forEach(folder => {
370
- this.app.use(this.normalizePath(folder.path ?? '/'), express_1.default.static(folder.directory, {
371
- maxAge: folder.maxAge || 0,
372
- etag: folder.etag !== false,
373
- immutable: folder.immutable === true,
374
- lastModified: folder.lastModified !== false,
375
- cacheControl: folder.cacheControl !== false
376
- }));
377
- (0, logger_utils_1.getLogger)().info(`Serving static folder: ${folder.directory} at path ${folder.path || '/'}`);
378
- });
379
- }
380
- // Request body parsing with size limits
381
- if (this.config.bodyParser) {
382
- if (this.config.bodyParser.json) {
383
- this.app.use(express_1.default.json(this.config.bodyParser.json));
384
- }
385
- if (this.config.bodyParser.urlencoded) {
386
- this.app.use(express_1.default.urlencoded(this.config.bodyParser.urlencoded));
387
- }
388
- }
389
- // Cookie parser middleware
390
- if (this.config.cookieParser) {
391
- const cookieParser = ExpressServer.optionalRequire('cookie-parser');
392
- if (!cookieParser) {
393
- (0, logger_utils_1.getLogger)().error({ command: 'npm install cookie-parser' }, 'cookie-parser is required but not installed. Please add it to your dependencies');
394
- process.exit(1);
395
- }
396
- if (typeof this.config.cookieParser === 'object') {
397
- this.app.use(cookieParser(undefined, this.config.cookieParser));
398
- }
399
- else {
400
- this.app.use(cookieParser());
401
- }
402
- }
403
- // OpenAPI docs via @scalar/express-api-reference
404
- if (this.config.openApi?.enable) {
405
- try {
406
- const openApiMountPath = this.normalizePath(this.config.openApi.mountPath ?? '/docs', this.config.openApi.withGlobalPrefix);
407
- const openApiFilePath = this.config.openApi.filePath;
408
- if (!openApiFilePath) {
409
- (0, logger_utils_1.getLogger)().error('OpenAPI file path is required');
410
- process.exit(1);
411
- }
412
- const isOpenApiFilePathExists = await (0, fs_utils_1.fileExists)(openApiFilePath);
413
- if (!isOpenApiFilePathExists) {
414
- (0, logger_utils_1.getLogger)().error(`OpenAPI spec file not found at ${openApiFilePath}`);
415
- process.exit(1);
416
- }
417
- if (this.config.openApi?.verbose) {
418
- (0, logger_utils_1.getLogger)().info(`Mounting OpenAPI docs at ${openApiMountPath}`);
419
- (0, logger_utils_1.getLogger)().info(`Using OpenAPI spec file at ${openApiFilePath}`);
420
- }
421
- const apiReference = ExpressServer.optionalRequire('@scalar/express-api-reference').apiReference;
422
- if (!apiReference) {
423
- (0, logger_utils_1.getLogger)().error({ command: 'npm install @scalar/express-api-reference' }, '@scalar/express-api-reference is required for OpenAPI docs but not installed. Please add it to your dependencies');
424
- process.exit(1);
425
- }
426
- this.app.use(openApiMountPath, apiReference({
427
- spec: {
428
- content: await fs_1.default.promises.readFile(openApiFilePath, 'utf8')
429
- }
430
- }));
431
- if (this.config.openApi?.verbose) {
432
- (0, logger_utils_1.getLogger)().info(`Mounted OpenAPI docs at ${openApiMountPath}`);
433
- }
434
- }
435
- catch (err) {
436
- (0, logger_utils_1.getLogger)().error({ err }, 'Failed to mount OpenAPI docs');
437
- }
438
- }
439
- // Custom response preprocessing hook (apply global prefix if set)
440
- if (this.hooks.onResponse) {
441
- this.app.use(this.globalPrefix, this.hooks.onResponse);
442
- }
443
- if (this.config.metrics?.enable) {
444
- // Add metrics tracking middleware
445
- this.app.use((req, res, next) => {
446
- const start = process.hrtime();
447
- // Track client IPs
448
- this.clientIPs?.inc({ ip: req.ip, method: req.method });
449
- // Track request sizes (parse safely)
450
- const cl = req.headers['content-length'];
451
- if (cl) {
452
- const size = Number(cl);
453
- if (!Number.isNaN(size) && size >= 0) {
454
- const route = this.normalizeRouteForMetrics(req, res);
455
- this.requestSizes?.observe({ method: req.method, route }, size);
456
- }
457
- }
458
- res.once('finish', () => {
459
- const [seconds, nanoseconds] = process.hrtime(start);
460
- const finalRoute = this.normalizeRouteForMetrics(req, res);
461
- this.requestCounter?.inc({
462
- method: req.method,
463
- route: finalRoute,
464
- status: res.statusCode.toString()
465
- });
466
- this.routeTimings?.observe({
467
- method: req.method,
468
- route: finalRoute,
469
- status: res.statusCode.toString()
470
- }, seconds + nanoseconds / 1e9);
471
- });
472
- next();
473
- });
474
- }
475
- }
476
- /**
477
- * Configure server routes and error handling.
478
- * Sets up in following order:
479
- *
480
- * 1. Built-in routes (health, metrics)
481
- * 2. Application routes
482
- * 3. 404 handler
483
- * 4. Error handler
484
- */
485
- async setupRoutes() {
486
- // Health check endpoint
487
- const healthCheckPath = this.normalizePath(this.config.healthCheck?.path || '/healthz', this.config.healthCheck?.withGlobalPrefix);
488
- this.app.get(healthCheckPath, async (_req, res) => {
489
- try {
490
- if (!this.healthChecks.length) {
491
- return res.status(http_status_codes_1.HttpStatusCodes.OK).json(new response_utils_1.SuccessResponse('OK'));
492
- }
493
- const checkResults = await Promise.allSettled(this.healthChecks.map(async ({ name, check }) => {
494
- try {
495
- const status = await Promise.resolve(check());
496
- return { name, status, error: null };
497
- }
498
- catch (error) {
499
- return { name, status: false, error: error.message };
500
- }
501
- }));
502
- const results = checkResults.map(result => {
503
- if (result.status === 'fulfilled')
504
- return result.value;
505
- return { name: 'unknown', status: false, error: result.reason };
506
- });
507
- const allOk = results.every(r => r.status);
508
- const status = allOk ? http_status_codes_1.HttpStatusCodes.OK : http_status_codes_1.HttpStatusCodes.SERVICE_UNAVAILABLE;
509
- const response = new response_utils_1.SuccessResponse(allOk ? 'OK' : 'Service unavailable');
510
- if (!allOk)
511
- response.error = true;
512
- if (this.config.healthCheck?.detailed)
513
- response.data = { checks: results };
514
- return res.status(status).json(response);
515
- }
516
- catch {
517
- return res
518
- .status(http_status_codes_1.HttpStatusCodes.INTERNAL_SERVER_ERROR)
519
- .json(new exception_utils_1.InternalServerErrorException('Health check failed'));
520
- }
521
- });
522
- // Metrics endpoint
523
- if (this.config.metrics?.enable) {
524
- const metricsPath = this.normalizePath(this.config.metrics.path ?? '/metrics', this.config.metrics?.withGlobalPrefix);
525
- this.app.get(metricsPath, async (_req, res) => {
526
- res.set('Content-Type', this.register.contentType);
527
- res.end(await this.register.metrics());
528
- });
529
- }
530
- // Application routes
531
- const routerToUse = this.externalRouter || this.rootRouter;
532
- this.app.use(this.globalPrefix, routerToUse);
533
- // 404 handler (must be after all other routes)
534
- this.app.use((req, res) => {
535
- const status = http_status_codes_1.HttpStatusCodes.NOT_FOUND;
536
- const response = (0, response_utils_1.createFinalErrorResponse)(req, status, `Route ${req.method.toUpperCase()} ${req.path} not found`);
537
- res.status(status).json(response);
538
- });
539
- // Global error handler (must be the last middleware)
540
- this.app.use((err, req, res, next) => {
541
- // Check if this is a 404 error that should be handled with special logging rules
542
- const isNotFoundError = err instanceof exception_utils_2.NotFoundException;
543
- const shouldSkipLogging = !this.hooks.onError &&
544
- isNotFoundError &&
545
- this.config.requestLogging?.enable &&
546
- this.config.requestLogging.skipNotFoundRoutes === true;
547
- if (this.hooks.onError) {
548
- // Use custom error handler if provided
549
- this.hooks.onError(err, req, res, next);
550
- }
551
- else {
552
- // Default error handler with logging
553
- const errorHandlerMiddleware = (0, middleware_utils_1.errorHandler)({
554
- logErrors: !shouldSkipLogging,
555
- includeDetails: env_utils_1.Env.isDev() // Only show stack traces in development
556
- });
557
- errorHandlerMiddleware(err, req, res, next);
558
- }
559
- });
560
- }
561
- /**
562
- * Register a new health check function for monitoring service dependencies.
563
- *
564
- * Health checks are executed when the health endpoint is accessed and
565
- * help determine if the service is ready to handle requests.
566
- *
567
- * Examples:
568
- * - Database connectivity
569
- * - External service availability
570
- * - File system access
571
- * - Memory/CPU usage checks
572
- *
573
- * @param name Unique identifier for the check (used in detailed responses)
574
- * @param check Function returning boolean or Promise<boolean> indicating health
575
- * @returns This instance for method chaining
576
- */
577
- registerHealthCheck(name, check) {
578
- this.healthChecks.push({ name, check });
579
- return this;
580
- }
581
- /**
582
- * Get the underlying Express application instance.
583
- * Use this for advanced Express features not exposed by this wrapper.
584
- *
585
- * @returns The raw Express app instance
586
- */
587
- getApp() {
588
- return this.app;
589
- }
590
- /**
591
- * Get the active HTTP/HTTPS server instance.
592
- * Returns null if the server is not currently running.
593
- *
594
- * @returns The HTTP/HTTPS server instance or null
595
- */
596
- getServer() {
597
- return this.server;
598
- }
599
- /**
600
- * Start the HTTP server and begin listening for requests.
601
- *
602
- * This method:
603
- * - Executes beforeStart hooks
604
- * - Binds to the configured host/port
605
- * - Sets up error handling for startup failures
606
- * - Executes afterStart hooks on success
607
- * - Logs startup information
608
- *
609
- * @returns Promise resolving to the running HTTP server instance
610
- * @throws Error if server fails to start or port is already in use
611
- */
612
- async start() {
613
- // Ensure initialization (middleware + routes) completed before starting
614
- await this.initPromise;
615
- await this.runHook('beforeStart', this.app);
616
- return new Promise((resolve, reject) => {
617
- try {
618
- // Prepare listen arguments with optional host parameter
619
- const listenArgs = [
620
- this.config.port,
621
- this.config.host,
622
- async () => {
623
- const protocol = this.config.https ? 'https' : 'http';
624
- const url = `${protocol}://${this.config.host}:${this.config.port}`;
625
- (0, logger_utils_1.getLogger)().info(`Server running on ${url}`);
626
- if (this.config.healthCheck?.path) {
627
- (0, logger_utils_1.getLogger)().info(`Health check available at ${url}${this.normalizePath(this.config.healthCheck.path, this.config.healthCheck.withGlobalPrefix)}`);
628
- }
629
- if (this.config.metrics?.enable && this.config.metrics.path) {
630
- (0, logger_utils_1.getLogger)().info(`Metrics available at ${url}${this.normalizePath(this.config.metrics.path, this.config.metrics.withGlobalPrefix)}`);
631
- }
632
- if (this.config.openApi?.enable) {
633
- (0, logger_utils_1.getLogger)().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`);
634
- }
635
- if (this.server)
636
- await this.runHook('afterStart', this.server);
637
- resolve(this.server);
638
- }
639
- ];
640
- if (this.config.https) {
641
- const httpsOptions = {
642
- ...this.config.https,
643
- key: fs_1.default.readFileSync(this.config.https.key),
644
- cert: fs_1.default.readFileSync(this.config.https.cert)
645
- };
646
- if (this.config.https.ca) {
647
- httpsOptions.ca = fs_1.default.readFileSync(this.config.https.ca);
648
- }
649
- if (this.config.https.passphrase) {
650
- httpsOptions.passphrase = this.config.https.passphrase;
651
- }
652
- this.server = https_1.default.createServer(httpsOptions, this.app).listen(...listenArgs);
653
- }
654
- else {
655
- // Start the HTTP server
656
- this.server = this.app.listen(...listenArgs);
657
- }
658
- // Track connections
659
- this.server.on('connection', (conn) => {
660
- this.connections.add(conn);
661
- conn.on('close', () => this.connections.delete(conn));
662
- });
663
- // Handle server startup errors (port in use, permission denied, etc.)
664
- this.server.on('error', err => {
665
- (0, logger_utils_1.getLogger)().error({ err }, 'Server failed to start');
666
- reject(err);
667
- });
668
- }
669
- catch (error) {
670
- reject(error);
671
- }
672
- });
673
- }
674
- /**
675
- * Stop the HTTP server gracefully.
676
- *
677
- * This method:
678
- * - Executes beforeStop hooks
679
- * - Stops accepting new connections
680
- * - Waits for existing connections to finish
681
- * - Closes the server
682
- * - Executes afterStop hooks
683
- * - Logs shutdown information
684
- *
685
- * Graceful shutdown ensures:
686
- * - No requests are dropped
687
- * - Resources are properly cleaned up
688
- * - Monitoring systems are notified
689
- */
690
- async stop(force = false) {
691
- if (!this.server) {
692
- (0, logger_utils_1.getLogger)().warn('Stop called but server is not running');
693
- return;
694
- }
695
- if (this.isShuttingDown) {
696
- (0, logger_utils_1.getLogger)().warn('Stop called while shutdown is already in progress');
697
- return;
698
- }
699
- this.isShuttingDown = true;
700
- await this.runHook('beforeStop', this.server);
701
- const shutdownTimeout = 10_000; // 10s max wait
702
- const serverClosePromise = new Promise((resolve, reject) => {
703
- this.server.close(async (err) => {
704
- if (err) {
705
- (0, logger_utils_1.getLogger)().error({ err }, 'Error while closing server');
706
- reject(err);
707
- return;
708
- }
709
- this.server = null;
710
- this.isShuttingDown = false;
711
- (0, logger_utils_1.getLogger)().info('Server stopped gracefully');
712
- await this.runHook('afterStop');
713
- resolve();
714
- });
715
- });
716
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Shutdown timeout')), shutdownTimeout));
717
- try {
718
- await Promise.race([serverClosePromise, timeoutPromise]);
719
- }
720
- catch (err) {
721
- (0, logger_utils_1.getLogger)().error({ err }, 'Graceful shutdown timed out');
722
- if (force) {
723
- (0, logger_utils_1.getLogger)().warn('Forcing connection destroy due to shutdown timeout');
724
- }
725
- }
726
- finally {
727
- // Always clean up connections
728
- await this.destroyConnections();
729
- }
730
- }
731
- /**
732
- * Enable graceful shutdown on OS signals for production deployment.
733
- *
734
- * This is essential for:
735
- * - Container orchestration (Docker, Kubernetes)
736
- * - Process managers (PM2, systemd)
737
- * - Load balancer health checks
738
- * - Zero-downtime deployments
739
- *
740
- * @param signals Array of process signals to listen for (default: SIGINT, SIGTERM)
741
- */
742
- enableGracefulShutdown(signals = ['SIGINT', 'SIGTERM']) {
743
- signals.forEach(signal => {
744
- process.on(signal, async () => {
745
- (0, logger_utils_1.getLogger)().info(`Received ${signal}, initiating graceful shutdown...`);
746
- try {
747
- await this.stop();
748
- process.exit(0);
749
- }
750
- catch (err) {
751
- (0, logger_utils_1.getLogger)().error({ err }, 'Error during graceful shutdown, forcing stop...');
752
- try {
753
- await this.stop(true); // fallback to forced shutdown
754
- process.exit(1);
755
- }
756
- catch (forceError) {
757
- (0, logger_utils_1.getLogger)().fatal({ forceError }, 'Forced shutdown failed, exiting hard');
758
- process.exit(1);
759
- }
760
- }
761
- });
762
- });
763
- return this;
764
- }
765
- /**
766
- * Set an externally created base router.
767
- * This will override the internal rootRouter.
768
- */
769
- setBaseRouter(router) {
770
- this.externalRouter = router;
771
- return this;
772
- }
773
- /**
774
- * Create and register a new router (only used if not injecting one externally - use `setBaseRouter` instead).
775
- */
776
- createRouter(prefix = '') {
777
- const router = express_1.default.Router();
778
- const path = this.normalizePath(prefix, true);
779
- this.rootRouter.use(path, router);
780
- return router;
781
- }
782
- /**
783
- * Register a new route handler with support for multiple HTTP methods.
784
- * The route is automatically registered under the globalPrefix if set.
785
- *
786
- * @param methods Array of HTTP methods (get, post, put, delete, etc.)
787
- * @param path Route path with Express path patterns support
788
- * @param handlers One or more Express request handlers (middleware + final handler)
789
- * @returns This instance for method chaining
790
- */
791
- registerRoute(methods, path, ...handlers) {
792
- const fullPath = this.normalizePath(path, true);
793
- const methodMap = {
794
- get: this.app.get.bind(this.app),
795
- post: this.app.post.bind(this.app),
796
- put: this.app.put.bind(this.app),
797
- delete: this.app.delete.bind(this.app),
798
- patch: this.app.patch.bind(this.app),
799
- options: this.app.options.bind(this.app),
800
- head: this.app.head.bind(this.app)
801
- };
802
- methods.forEach(m => {
803
- const fn = methodMap[m];
804
- if (fn) {
805
- fn(fullPath, ...handlers);
806
- }
807
- else {
808
- throw new Error(`Unsupported HTTP method: ${m}`);
809
- }
810
- });
811
- return this;
812
- }
813
- /**
814
- * Register custom middleware with optional path restriction.
815
- *
816
- * Use this for:
817
- * - Adding authentication to specific routes
818
- * - Custom logging or validation
819
- * - Request transformation
820
- * - Third-party middleware integration
821
- *
822
- * @param path Optional path prefix or middleware function if no path
823
- * @param middleware Middleware handler (required if path is provided)
824
- * @returns This instance for method chaining
825
- */
826
- registerMiddleware(path, middleware) {
827
- if (typeof path === 'string') {
828
- const normalizedPath = this.normalizePath(path);
829
- if (normalizedPath) {
830
- this.app.use(normalizedPath, middleware);
831
- }
832
- else {
833
- this.app.use(middleware);
834
- }
835
- }
836
- else {
837
- this.app.use(path);
838
- }
839
- return this;
840
- }
841
- /**
842
- * Register one or more middleware functions to be applied globally.
843
- * This is a simpler alternative to registerMiddleware when you just want
844
- * to add middleware without path restrictions.
845
- *
846
- * @param middlewares One or more Express middleware functions
847
- * @returns This instance for method chaining
848
- */
849
- useMiddleware(...middlewares) {
850
- middlewares.forEach(middleware => {
851
- this.app.use(middleware);
852
- });
853
- return this;
854
- }
855
- /**
856
- * Get Prometheus registry (to add custom counters/histograms)
857
- *
858
- * @return {*} {client.Registry}
859
- */
860
- getMetricsRegistry() {
861
- if (!this.config.metrics?.enable) {
862
- throw new Error('Metrics are not enabled in the server configuration');
863
- }
864
- return this.register;
865
- }
866
- /**
867
- * Get server configuration
868
- *
869
- * @return {*} {ServerConfig}
870
- */
871
- getConfig() {
872
- return this.config;
873
- }
874
- /**
875
- * Wait until server initialization (middleware + routes) has completed.
876
- * Useful for integration tests that inspect app before starting.
877
- */
878
- async waitUntilReady() {
879
- await this.initPromise;
880
- }
881
- normalizePath(path, withGlobalPrefix = false) {
882
- const sanitize = (p) => {
883
- return ('/' +
884
- p
885
- .trim()
886
- .replace(/^\/+/, '') // remove leading slashes
887
- .replace(/\/{2,}/g, '/') // collapse multiple slashes
888
- .replace(/\/+$/, '')); // remove trailing slash
889
- };
890
- // Resolve global prefix if enabled
891
- const prefix = withGlobalPrefix && this.globalPrefix ? sanitize(this.globalPrefix) : '';
892
- // If path is invalid, default to prefix or root
893
- if (typeof path !== 'string' || !path.trim()) {
894
- return prefix || '/';
895
- }
896
- return sanitize(prefix + '/' + path);
897
- }
898
- normalizeRouteForMetrics(req, res) {
899
- // Prevent high cardinality metrics by normalizing routes
900
- if (req?.route?.path) {
901
- // Use Express route pattern instead of actual URL
902
- return req.route.path;
903
- }
904
- // Group common patterns
905
- if (res.statusCode === 404)
906
- return '/404';
907
- const path = (req.path || 'unknown').split('?')[0];
908
- // Replace IDs and UUIDs with placeholders
909
- return path
910
- .replace(/\/[0-9]+/g, '/:id')
911
- .replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g, '/:uuid');
912
- }
913
- /**
914
- * Destroy all active connections (gracefully if possible).
915
- * If a connection does not close cleanly, it will be force-destroyed.
916
- */
917
- async destroyConnections() {
918
- const total = this.connections.size;
919
- if (total === 0) {
920
- (0, logger_utils_1.getLogger)().debug('No active connections to close');
921
- return;
922
- }
923
- const timeoutMs = 5000;
924
- await Promise.race([
925
- Promise.all(Array.from(this.connections).map(conn => new Promise(resolve => {
926
- conn.end(() => {
927
- if (!conn.destroyed)
928
- conn.destroy();
929
- resolve();
930
- });
931
- conn.on('error', () => {
932
- conn.destroy();
933
- resolve();
934
- });
935
- }))),
936
- new Promise(resolve => setTimeout(resolve, timeoutMs))
937
- ]);
938
- this.connections.clear();
939
- (0, logger_utils_1.getLogger)().info(`Closed ${total} active connections`);
940
- }
941
- async validateHttpsFiles() {
942
- if (!(await (0, fs_utils_1.fileExists)(this.config.https.key))) {
943
- (0, logger_utils_1.getLogger)().error(`HTTPS key file not found: ${this.config.https.key}`);
944
- process.exit(1);
945
- }
946
- if (!(await (0, fs_utils_1.fileExists)(this.config.https.cert))) {
947
- (0, logger_utils_1.getLogger)().error(`HTTPS cert file not found: ${this.config.https.cert}`);
948
- process.exit(1);
949
- }
950
- if (this.config.https.ca && !(await (0, fs_utils_1.fileExists)(this.config.https.ca))) {
951
- (0, logger_utils_1.getLogger)().error(`HTTPS CA file not found: ${this.config.https.ca}`);
952
- process.exit(1);
953
- }
954
- }
955
- static isBuiltServerConfig(config) {
956
- if (config[server_builder_1.BUILD_MARKER]) {
957
- return true;
958
- }
959
- return false;
960
- }
961
- static optionalRequire(name) {
962
- try {
963
- return require(name);
964
- }
965
- catch {
966
- return null;
967
- }
968
- }
969
- }
970
- exports.ExpressServer = ExpressServer;