UNPKG

@catbee/utils

Version:

A modular, production-grade utility toolkit for Node.js and TypeScript, designed for robust, scalable applications (including Express-based services). All utilities are tree-shakable and can be imported independently.

1,647 lines (1,642 loc) 52.8 kB
/* * The MIT License * * Copyright (c) 2026 Catbee Technologies. https://catbee.in/license * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ 'use strict'; var express = require('express'); var https = require('https'); var httpStatusCodes = require('@catbee/utils/http-status-codes'); var response = require('@catbee/utils/response'); var middleware = require('@catbee/utils/middleware'); var env = require('@catbee/utils/env'); var logger = require('@catbee/utils/logger'); var exception = require('@catbee/utils/exception'); var config = require('@catbee/utils/config'); var object = require('@catbee/utils/object'); var fs = require('@catbee/utils/fs'); var validation = require('@catbee/utils/validation'); var async = require('@catbee/utils/async'); var id = require('@catbee/utils/id'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var express__default = /*#__PURE__*/_interopDefault(express); var https__default = /*#__PURE__*/_interopDefault(https); var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var BUILD_MARKER = Symbol.for("catbee.express.server.build"); var ServerConfigBuilder = class { static { __name(this, "ServerConfigBuilder"); } config = {}; /** * Validates that a port number is valid and usable. * * @private * @param port - The port number to validate * @throws {Error} If port is not an integer or is outside the valid range (1-65535) */ validatePort(port) { if (!validation.isPort(port)) { throw new Error(`Port must be a valid number between 1 and 65535, got: ${port}`); } } /** * Sets the port the server will listen on. * * @param port - The port number (1-65535) * @returns The builder instance for chaining * @throws {Error} If port is invalid * @default 3000 (can be overridden via PORT env variable) * * @example * ```typescript * builder.withPort(3000) * ``` */ withPort(port) { this.validatePort(port); this.config.port = port; return this; } /** * Sets the hostname the server will bind to. * * @param host - The hostname (e.g., 'localhost', '0.0.0.0', '127.0.0.1') * @returns The builder instance for chaining * @default '0.0.0.0' (can be overridden via HOST env variable) * * @example * ```typescript * builder.withHost('0.0.0.0') // Listen on all interfaces * ``` */ withHost(host) { this.config.host = host; return this; } /** * Configures Cross-Origin Resource Sharing (CORS) for the server. * * @param opts - CORS options object or boolean (true to enable with defaults, false to disable) * @returns The builder instance for chaining * @default false (CORS is disabled by default) * * @example * ```typescript * // Enable CORS with default options * builder.withCors(true) * * // Configure CORS with specific options * builder.withCors({ * origin: ['https://example.com'], * methods: ['GET', 'POST'] * }) * ``` */ withCors(opts) { this.config.cors = opts; return this; } /** * Enables CORS with default settings * * @returns The builder instance for chaining */ enableCors() { return this.withCors(true); } /** * Disables CORS * * @returns The builder instance for chaining */ disableCors() { return this.withCors(false); } /** * Configures the Helmet middleware for setting HTTP security headers. * * @param opts - Helmet options object or boolean (true to enable with defaults, false to disable) * @returns The builder instance for chaining * @default false (Helmet is disabled by default) * * @example * ```typescript * // Enable Helmet with default settings * builder.withHelmet(true) * * // Configure Helmet with specific options * builder.withHelmet({ * contentSecurityPolicy: false, * xssFilter: true * }) * ``` */ withHelmet(opts) { this.config.helmet = opts; return this; } /** * Enables Helmet with default settings * * @returns The builder instance for chaining */ enableHelmet() { return this.withHelmet(true); } /** * Disables Helmet * * @returns The builder instance for chaining */ disableHelmet() { return this.withHelmet(false); } /** * Configures response compression middleware. * * @param opts - Compression options object or boolean (true to enable with defaults, false to disable) * @returns The builder instance for chaining * @default false (Compression is disabled by default) * * @example * ```typescript * // Enable compression with default settings * builder.withCompression(true) * * // Configure compression with specific options * builder.withCompression({ * level: 6, * threshold: 1024 * }) * ``` */ withCompression(opts) { this.config.compression = opts; return this; } /** * Enables compression with default settings * * @returns The builder instance for chaining */ enableCompression() { return this.withCompression(true); } /** * Disables compression * * @returns The builder instance for chaining */ disableCompression() { return this.withCompression(false); } /** * Configures rate limiting to protect against brute-force attacks. * * @param opts - Rate limit configuration options * @returns The builder instance for chaining * @default - { enable: false, windowMs: 15 * 60 * 1000, max: 100, message: 'Too many requests', standardHeaders: true, legacyHeaders: false } * * @example * ```typescript * builder.withRateLimit({ * enable: true, * windowMs: 15 * 60 * 1000, // 15 minutes * max: 100 // limit each IP to 100 requests per windowMs * }) * ``` */ withRateLimit(opts) { this.mergeConfig("rateLimit", opts); return this; } /** * Enables rate limiting with default or custom settings * * @param opts - Optional rate limit configuration (max requests, window, etc.) * @returns The builder instance for chaining */ enableRateLimit(opts = {}) { return this.setEnabled("rateLimit", true, opts); } /** * Disables rate limiting * * @returns The builder instance for chaining */ disableRateLimit() { return this.setEnabled("rateLimit", false); } /** * Configures HTTP request logging middleware. * * @param opts - Request logging configuration options * @returns The builder instance for chaining * @default - { enable: true in dev/false in prod, ignorePaths: ['/healthz', '/favicon.ico', '/metrics', '/docs', '/.well-known'], skipNotFoundRoutes: false } * * @example * ```typescript * builder.withRequestLogging({ * enable: true, * ignorePaths: ['/health', '/metrics'], * skipNotFoundRoutes: true * }) * ``` */ withRequestLogging(opts) { this.mergeConfig("requestLogging", opts); return this; } /** * Enables request logging with default or custom settings * * @param opts - Optional request logging configuration * @returns The builder instance for chaining */ enableRequestLogging(opts = {}) { return this.setEnabled("requestLogging", true, opts); } /** * Disables request logging * * @returns The builder instance for chaining */ disableRequestLogging() { return this.setEnabled("requestLogging", false); } /** * Configures server metrics collection and endpoints. * * @param opts - Metrics configuration options * @returns The builder instance for chaining * @default - { enable: false, path: '/metrics', withGlobalPrefix: false } * * @example * ```typescript * builder.withMetrics({ * enable: true, * path: '/metrics' * }) * ``` */ withMetrics(opts) { this.mergeConfig("metrics", opts); return this; } /** * Enables Prometheus metrics collection and endpoint * * @param opts - Optional metrics configuration * @returns The builder instance for chaining */ enableMetrics(opts = {}) { return this.setEnabled("metrics", true, opts); } /** * Disables Prometheus metrics * * @returns The builder instance for chaining */ disableMetrics() { return this.setEnabled("metrics", false); } /** * Configures server health check endpoint. * * @param opts - Health check configuration options * @returns The builder instance for chaining * @default - { path: '/healthz', detailed: true, withGlobalPrefix: false } * * @example * ```typescript * builder.withHealthCheck({ * path: '/health', * detailed: true * }) * ``` */ withHealthCheck(opts) { this.mergeConfig("healthCheck", opts); return this; } /** * Configures OpenAPI/Swagger documentation for the API. * * @param opts - OpenAPI configuration options * @returns The builder instance for chaining * @default - { enable: false, mountPath: '/docs', verbose: false, withGlobalPrefix: false } * * @example * ```typescript * builder.withOpenApi({ * enable: true, * path: '/api-docs', * filePath: './openapi.yaml' * }) * ``` */ withOpenApi(opts) { this.mergeConfig("openApi", opts); return this; } /** * Enables OpenAPI documentation with required file path * * @param filePath - Path to OpenAPI specification file (required) * @param opts - Optional OpenAPI configuration * @returns The builder instance for chaining */ enableOpenApi(filePath, opts = {}) { this.setEnabled("openApi", true, { filePath, ...opts }); return this; } /** * Disables OpenAPI documentation * * @returns The builder instance for chaining */ disableOpenApi() { return this.setEnabled("openApi", false); } /** * Configures the server as a microservice with versioning. * * @param opts - Microservice configuration options including app name and service version * @returns The builder instance for chaining * @default - { isMicroservice: false, appName: 'express_app' } * * @example * ```typescript * builder.withMicroService({ * appName: 'user-service', * serviceVersion: { * enable: true, * version: '1.2.3' * } * }) * ``` */ withMicroService(opts) { this.config.isMicroservice = true; this.config.appName = opts.appName; this.mergeConfig("serviceVersion", opts.serviceVersion); return this; } /** * Configures the trust proxy settings to determine if X-Forwarded-* headers should be trusted. * * @param opts - Trust proxy configuration options * @returns The builder instance for chaining * @default false * * @example * ```typescript * // Trust proxy headers (useful when behind a load balancer) * builder.withTrustProxy(true) * ``` */ withTrustProxy(opts) { this.config.trustProxy = opts; return this; } /** * Configures the request ID middleware for tracing requests across services. * * @param opts - Request ID configuration options * @returns The builder instance for chaining * @default - { headerName: 'x-request-id', exposeHeader: true } * * @example * ```typescript * builder.withRequestId({ * headerName: 'X-Request-Id', * generator: () => crypto.randomUUID() * }) * ``` */ withRequestId(opts) { this.mergeConfig("requestId", opts); return this; } /** * Configures the response time middleware for measuring request processing times. * * @param opts - Response time configuration options * @returns The builder instance for chaining * @default - { enable: false, addHeader: true, logOnComplete: false } * * @example * ```typescript * builder.withResponseTime({ * enable: true, * addHeader: true, * logOnComplete: true * }) * ``` */ withResponseTime(opts) { this.mergeConfig("responseTime", opts); return this; } /** * Enables response time tracking with default or custom settings * * @param opts - Optional response time configuration * @returns The builder instance for chaining */ enableResponseTime(opts = {}) { this.setEnabled("responseTime", true, opts); return this; } /** * Disables response time tracking * * @returns The builder instance for chaining */ disableResponseTime() { return this.setEnabled("responseTime", false); } /** * Configures the body parser middleware options for parsing request bodies. * * @param opts - Body parser configuration options * @returns The builder instance for chaining * @default - { json: { limit: '1mb' }, urlencoded: { extended: true, limit: '1mb' } } * * @example * ```typescript * builder.withBodyParser({ * json: { * limit: '1mb' * }, * urlencoded: { * extended: true, * limit: '1mb' * } * }) * ``` */ withBodyParser(opts) { this.config.bodyParser = { ...this.config.bodyParser, ...opts }; return this; } /** * Configures cookie parsing middleware. * * @param opts - Cookie parser options or boolean (true to enable with defaults, false to disable) * @returns The builder instance for chaining * @default false * * @example * ```typescript * // Enable cookie parsing with default options * builder.withCookies(true) * * // Enable cookie parsing with specific options * builder.withCookies({ * secret: 'your-secret-key', * secure: true * }) * ``` */ withCookies(opts) { this.config.cookieParser = opts; return this; } /** * Adds a static folder to serve files from. * * @param folder - Static folder configuration * @returns The builder instance for chaining * * @example * ```typescript * builder.withStaticFolder({ * path: '/assets', * directory: './public', * options: { maxAge: '1d' } * }) * ``` */ withStaticFolder(folder) { if (!folder.path) throw new Error("Static folder requires a path"); const folders = [ ...this.config.staticFolders ?? [], folder ]; this.config.staticFolders = Array.from(new Map(folders.map((f) => [ f.path, f ])).values()); return this; } /** * Sets global headers to be included in all responses. * * @param headers - Object containing header name/value pairs or functions that return values * @returns The builder instance for chaining * @default - {} * * @example * ```typescript * builder.withGlobalHeaders({ * 'X-Powered-By': 'Catbee', * 'Server-Time': () => new Date().toISOString() * }) * ``` */ withGlobalHeaders(headers) { this.mergeConfig("globalHeaders", headers); return this; } /** * Sets a global prefix for all routes. * * @param prefix - The prefix to prepend to all routes (e.g., '/api/v1') * @returns The builder instance for chaining * @default '/' * * @example * ```typescript * builder.withGlobalPrefix('/api/v1') * ``` */ withGlobalPrefix(prefix) { this.config.globalPrefix = prefix; return this; } /** * Applies custom configuration overrides directly. * * @param overrides - Custom configuration options to merge * @returns The builder instance for chaining * * @example * ```typescript * builder.withCustom({ * port: 8080, * customMiddleware: myMiddlewareFunction * }) * ``` */ withCustom(overrides) { this.config = object.deepObjMerge({}, this.config, overrides); return this; } /** * Configures HTTPS server options. * * @param opts - HTTPS configuration (key, cert, ca, passphrase, etc.) * @returns The builder instance for chaining * * @example * ```typescript * builder.withHttps({ * key: './localhost-key.pem', * cert: './localhost-cert.pem' * }) * ``` */ withHttps(opts) { this.config.https = opts; return this; } /** * Builds and returns the final server configuration. * * This method merges the user-specified configuration with default values, * ensures all sections with 'enable' flags are properly structured, and * produces the final configuration to be used by the server. * * @returns The complete ServerConfig object * * @example * ```typescript * const config = new ServerConfigBuilder() * .withPort(3000) * .withHost('localhost') * .withCors(true) * .build(); * ``` */ build() { const config$1 = object.deepObjMerge({}, config.getCatbeeServerGlobalConfig(), this.config); if (config$1.openApi?.enable && !config$1.openApi.filePath) { throw new Error("OpenAPI is enabled but no filePath is specified"); } return Object.freeze({ ...config$1, [BUILD_MARKER]: true }); } mergeConfig(key, value) { const current = this.config[key] && typeof this.config[key] === "object" ? object.deepClone(this.config[key]) : {}; this.config[key] = object.deepObjMerge({}, current, value); } setEnabled(key, enable, overrides = {}) { this.mergeConfig(key, { ...overrides, enable }); return this; } }; var getDependencyErrorMessage = /* @__PURE__ */ __name((packageName, context) => { const ctxPart = context ? `for ${context}` : ""; const spacer = ctxPart ? ` ${ctxPart}` : ""; return `Missing required dependency${spacer}: ${packageName}. Please install it to proceed.`; }, "getDependencyErrorMessage"); var DependencyErrors = { express: getDependencyErrorMessage("express"), helmet: getDependencyErrorMessage("helmet"), cors: getDependencyErrorMessage("cors"), compression: getDependencyErrorMessage("compression"), "express-rate-limit": getDependencyErrorMessage("express-rate-limit"), "cookie-parser": getDependencyErrorMessage("cookie-parser"), "@scalar/express-api-reference": getDependencyErrorMessage("@scalar/express-api-reference"), "prom-client": getDependencyErrorMessage("prom-client") }; var ExpressServer = class { static { __name(this, "ExpressServer"); } /** Prometheus client registry for metrics collection */ register = null; /** HTTP server instance (null when not running) */ server = null; /** Merged configuration with defaults applied */ config; /** User-defined lifecycle hooks */ hooks; /** Global API prefix (from config) */ globalPrefix; /** Internal fallback router */ rootRouter; /** User-supplied router */ externalRouter; /** Internal Express app instance */ app; /** Set of active WebSocket connections */ connections = /* @__PURE__ */ new Set(); /** Flag indicating if the server is shutting down */ isShuttingDown = false; /** * Collection of registered health check functions. * These are executed when the health check endpoint is accessed. */ healthChecks = []; /** Prometheus metrics for monitoring */ requestCounter; routeTimings; requestSizes; clientIPs; /** Promise that resolves when initialization (middleware + routes) is complete */ initPromise; /** * Initializes server with intelligent defaults and security best practices. * All settings can be customized via config and hooks. * * Default Security: * - Secure headers (Helmet) * - Rate limiting * - Request timeouts * - Body size limits * - CORS protection * * Default Monitoring: * - Request/Response logging * - Prometheus metrics * - Health checks * - Request tracing */ constructor(config$1, hooks = {}) { if (this.hasBuildMarker(config$1)) { this.config = config$1; } else { this.config = object.deepObjMerge({}, config.getCatbeeServerGlobalConfig(), config$1); } if (!validation.isPort(this.config.port)) { const msg = `Port must be a valid number between 1 and 65535, got: ${this.config.port}`; logger.getLogger().error(msg); throw new Error(msg); } const safeAppName = (this.config.appName || "express_app").toLowerCase().replace(/[^a-z0-9_]/g, "_"); if (this.config.metrics?.enable) { const client = async.optionalRequire("prom-client"); if (!client) { this.throwDependancyError("prom-client"); } this.register = new client.Registry(); this.requestCounter = new client.Counter({ name: `${safeAppName}_http_requests_total`, help: "Total HTTP requests", labelNames: [ "method", "route", "status" ], registers: [ this.register ] }); this.routeTimings = new client.Histogram({ name: `${safeAppName}_http_request_duration_seconds`, help: "Duration of HTTP requests by route", labelNames: [ "method", "route", "status" ], buckets: [ 0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10 ], registers: [ this.register ] }); this.requestSizes = new client.Histogram({ name: `${safeAppName}_http_request_size_bytes`, help: "Size of HTTP request bodies", labelNames: [ "method", "route" ], buckets: [ 100, 1e3, 1e4, 1e5, 1e6 ], registers: [ this.register ] }); this.clientIPs = new client.Counter({ name: `${safeAppName}_http_client_ip_total`, help: "Client IP request counter", labelNames: [ "ip", "method" ], registers: [ this.register ] }); client.collectDefaultMetrics({ register: this.register, prefix: `${safeAppName}_` }); } if (config$1?.healthCheck?.checks) { this.healthChecks.push(...config$1.healthCheck.checks); } this.globalPrefix = this.normalizePath(this.config.globalPrefix ?? "", false); this.hooks = hooks; this.app = express__default.default(); this.rootRouter = express__default.default.Router(); this.initPromise = this.initialize(); } /** * Execute a lifecycle hook safely with comprehensive error handling. * Prevents hook failures from crashing the server while logging issues. * * @param hook Name of the lifecycle hook to execute * @param args Arguments to pass to the hook function */ async runHook(hook, ...args) { try { const fn = this.hooks[hook]; if (fn) await fn(...args); } catch (err) { logger.getLogger().error({ err, hook }, `Error executing ${hook} hook:`); } } /** * Initialize the Express server with middleware and routes. */ async initialize() { await this.runHook("beforeInit", this); await this.setupMiddleware(); await this.setupRoutes(); await this.runHook("afterInit", this); } /** * Configure and register all middlewares in the optimal order. * * Middleware Order (CRITICAL - don't change without understanding implications): * 1. Basic server configuration (trust proxy, x-powered-by) * 2. Request ID generation (for tracing) * 3. Request context setup (for logging correlation) * 4. Timeout protection (prevents hanging requests) * 5. Response time tracking (for performance monitoring) * 6. Request logging (after ID/context setup) * 7. Custom request hooks * 8. Security middleware (rate limiting, CORS, Helmet) * 9. Response compression * 10. Static file serving * 11. Request parsing (body parsing, cookies) * 12. API documentation (OpenAPI) * 13. Global headers * 14. Custom response hooks */ async setupMiddleware() { if (this.config.https) { await this.validateHttpsFiles(); } this.setupBasicMiddleware(); this.setupSecurityMiddleware(); this.setupGlobalHeaders(); this.setupTimeoutMiddleware(); this.setupResponseTimeMiddleware(); this.setupRateLimitingMiddleware(); this.setupRequestLoggingMiddleware(); this.setupCompressionMiddleware(); this.setupStaticFilesMiddleware(); this.setupBodyParsingMiddleware(); this.setupCookieParsingMiddleware(); await this.setupOpenApiMiddleware(); this.setupMetricsMiddleware(); } /** * Set up basic middleware (trust proxy, request ID, context). */ setupBasicMiddleware() { this.app.disable("x-powered-by"); if (this.config.trustProxy) { this.app.set("trust proxy", true); } this.app.use(middleware.requestId({ headerName: this.config.requestId?.headerName, exposeHeader: this.config.requestId?.exposeHeader, generator: this.config.requestId?.generator || id.uuid })); this.app.use(middleware.setupRequestContext({ headerName: this.config.requestId?.headerName, autoLog: false })); this.app.use((_req, res, next) => { if (this.isShuttingDown) { res.setHeader("Connection", "close"); res.status(httpStatusCodes.HttpStatusCodes.SERVICE_UNAVAILABLE).json(new exception.ServiceUnavailableException("Server is shutting down")); } next(); }); } /** * Set up security middleware (Helmet, CORS). */ setupSecurityMiddleware() { if (this.config.helmet) { const helmet = async.optionalRequire("helmet"); if (!helmet) { this.throwDependancyError("helmet"); } if (typeof this.config.helmet === "object") { this.app.use(helmet(this.config.helmet)); } else { this.app.use(helmet()); } } if (this.config.cors) { const cors = async.optionalRequire("cors"); if (!cors) { this.throwDependancyError("cors"); } this.app.use(cors(this.config.cors === true ? {} : this.config.cors)); } } /** * Set up global headers middleware. */ setupGlobalHeaders() { this.app.use((_req, res, next) => { if (this.config.globalHeaders) { for (const key in this.config.globalHeaders) { const value = this.config.globalHeaders[key]; res.setHeader(key, typeof value === "function" ? value() : value); } } if (this.config.isMicroservice) { res.setHeader("X-Microservice", this.config.appName || "express_app"); } if (this.config.serviceVersion?.enable) { const version = typeof this.config.serviceVersion?.version === "function" ? this.config.serviceVersion.version() : this.config.serviceVersion?.version; res.setHeader(this.config.serviceVersion?.headerName || "x-service-version", version || "0.0.0"); } next(); }); } /** * Set up request timeout middleware. */ setupTimeoutMiddleware() { if (this.config.requestTimeout) { this.app.use(middleware.timeout(this.config.requestTimeout)); } } /** * Set up response time tracking middleware. */ setupResponseTimeMiddleware() { if (this.config.responseTime?.enable) { this.app.use(middleware.responseTime({ addHeader: this.config.responseTime.addHeader, logOnComplete: this.config.responseTime.logOnComplete })); } } /** * Set up rate limiting middleware. */ setupRateLimitingMiddleware() { if (this.config.rateLimit?.enable) { const rateLimit = async.optionalRequire("express-rate-limit"); if (!rateLimit) { this.throwDependancyError("express-rate-limit"); } this.app.use(rateLimit({ windowMs: this.config.rateLimit.windowMs ?? 15 * 60 * 1e3, max: this.config.rateLimit.max ?? 100, handler: /* @__PURE__ */ __name((req, res) => { const status = httpStatusCodes.HttpStatusCodes.TOO_MANY_REQUESTS; const response$1 = response.createFinalErrorResponse(req, status, this.config.rateLimit?.message || "Too many requests"); res.status(status).json(response$1); }, "handler"), standardHeaders: this.config.rateLimit.standardHeaders ?? true, legacyHeaders: this.config.rateLimit.legacyHeaders ?? false })); } } /** * Set up request logging middleware. */ setupRequestLoggingMiddleware() { if (this.config.requestLogging?.enable) { this.app.use((req, res, next) => { if (typeof this.config.requestLogging?.ignorePaths === "function") { const skip = this.config.requestLogging?.ignorePaths?.(req, res); if (skip) return next(); } else if (Array.isArray(this.config.requestLogging?.ignorePaths)) { const skip = this.config.requestLogging?.ignorePaths?.some((path) => req.path.startsWith(path)); if (skip) return next(); } const logger$1 = logger.getLogger(); const incomingRequestMetaData = { requestId: req.id, method: req.method, url: req.originalUrl || req.url, ip: req.ip }; logger$1.info(incomingRequestMetaData, "Incoming Request"); next(); }); } if (this.hooks.onRequest) { this.app.use(this.hooks.onRequest); } } /** * Set up response compression middleware. */ setupCompressionMiddleware() { if (this.config.compression) { const compression = async.optionalRequire("compression"); if (!compression) { this.throwDependancyError("compression"); } if (typeof this.config.compression === "object") { this.app.use(compression(this.config.compression)); } else { this.app.use(compression()); } } } /** * Set up static file serving middleware. */ setupStaticFilesMiddleware() { if (this.config.staticFolders) { this.config.staticFolders.forEach((folder) => { this.app.use(this.normalizePath(folder.path ?? "/"), express__default.default.static(folder.directory, { maxAge: folder.maxAge || 0, etag: folder.etag !== false, immutable: folder.immutable === true, lastModified: folder.lastModified !== false, cacheControl: folder.cacheControl !== false })); logger.getLogger().info(`Serving static folder: ${folder.directory} at path ${folder.path || "/"}`); }); } } /** * Set up body parsing middleware. */ setupBodyParsingMiddleware() { if (this.config.bodyParser) { if (this.config.bodyParser.json) { this.app.use(express__default.default.json(this.config.bodyParser.json)); } if (this.config.bodyParser.urlencoded) { this.app.use(express__default.default.urlencoded(this.config.bodyParser.urlencoded)); } } } /** * Set up cookie parsing middleware. */ setupCookieParsingMiddleware() { if (this.config.cookieParser) { const cookieParser = async.optionalRequire("cookie-parser"); if (!cookieParser) { this.throwDependancyError("cookie-parser"); } if (typeof this.config.cookieParser === "object") { this.app.use(cookieParser(void 0, this.config.cookieParser)); } else { this.app.use(cookieParser()); } } } /** * Set up OpenAPI documentation middleware. */ async setupOpenApiMiddleware() { if (this.config.openApi?.enable) { try { const openApiMountPath = this.normalizePath(this.config.openApi.mountPath ?? "/docs", this.config.openApi.withGlobalPrefix); const openApiFilePath = this.config.openApi.filePath; if (!openApiFilePath) { const msg = "OpenAPI file path is required"; logger.getLogger().error(msg); throw new Error(msg); } const isOpenApiFilePathExists = await fs.fileExists(openApiFilePath); if (!isOpenApiFilePathExists) { const msg = `OpenAPI spec file not found at ${openApiFilePath}`; logger.getLogger().error(msg); throw new Error(msg); } if (this.config.openApi?.verbose) { logger.getLogger().info(`Mounting OpenAPI docs at ${openApiMountPath}`); logger.getLogger().info(`Using OpenAPI spec file at ${openApiFilePath}`); } const apiReference = async.optionalRequire("@scalar/express-api-reference")?.apiReference; if (!apiReference) { this.throwDependancyError("@scalar/express-api-reference", getDependencyErrorMessage("@scalar/express-api-reference", "OpenAPI docs")); } this.app.use(openApiMountPath, apiReference({ spec: { content: await fs.readFile(openApiFilePath, "utf8") } })); if (this.config.openApi?.verbose) { logger.getLogger().info(`Mounted OpenAPI docs at ${openApiMountPath}`); } } catch (err) { logger.getLogger().error({ err }, "Failed to mount OpenAPI docs"); } } if (this.hooks.onResponse) { this.app.use(this.globalPrefix, this.hooks.onResponse); } } /** * Set up metrics tracking middleware. */ setupMetricsMiddleware() { if (this.config.metrics?.enable) { this.app.use((req, res, next) => { const start = process.hrtime(); this.clientIPs?.inc({ ip: req.ip, method: req.method }); const cl = req.headers["content-length"]; if (cl) { const size = Number(cl); if (!Number.isNaN(size) && size >= 0) { const route = this.normalizeRouteForMetrics(req, res); this.requestSizes?.observe({ method: req.method, route }, size); } } res.once("finish", () => { const [seconds, nanoseconds] = process.hrtime(start); const finalRoute = this.normalizeRouteForMetrics(req, res); this.requestCounter?.inc({ method: req.method, route: finalRoute, status: res.statusCode.toString() }); this.routeTimings?.observe({ method: req.method, route: finalRoute, status: res.statusCode.toString() }, seconds + nanoseconds / 1e9); }); next(); }); } } /** * Configure server routes and error handling. * Sets up in following order: * * 1. Built-in routes (health, metrics) * 2. Application routes * 3. 404 handler * 4. Error handler */ async setupRoutes() { const healthCheckPath = this.normalizePath(this.config.healthCheck?.path || "/healthz", this.config.healthCheck?.withGlobalPrefix); this.app.get(healthCheckPath, async (_req, res) => { return this.handleHealthCheckRequest(res); }); if (this.config.metrics?.enable) { const metricsPath = this.normalizePath(this.config.metrics.path ?? "/metrics", this.config.metrics?.withGlobalPrefix); this.app.get(metricsPath, async (_req, res) => { res.set("Content-Type", this.register.contentType); res.end(await this.register.metrics()); }); } const routerToUse = this.externalRouter || this.rootRouter; this.app.use(this.globalPrefix, routerToUse); this.app.use((req, res) => { const status = httpStatusCodes.HttpStatusCodes.NOT_FOUND; const response$1 = response.createFinalErrorResponse(req, status, `Route ${req.method.toUpperCase()} ${req.path} not found`); res.status(status).json(response$1); }); this.app.use((err, req, res, next) => { const isNotFoundError = err instanceof exception.NotFoundException; const shouldSkipLogging = !this.hooks.onError && isNotFoundError && this.config.requestLogging?.enable && this.config.requestLogging.skipNotFoundRoutes === true; if (this.hooks.onError) { this.hooks.onError(err, req, res, next); } else { const errorHandlerMiddleware = middleware.errorHandler({ logErrors: !shouldSkipLogging, includeDetails: env.Env.isDev() // Only show stack traces in development }); errorHandlerMiddleware(err, req, res, next); } }); } /** * Execute health check and return response. */ async handleHealthCheckRequest(res) { try { if (!this.healthChecks.length || config.getCatbeeServerGlobalConfig().skipHealthzChecksValidation) { return res.status(httpStatusCodes.HttpStatusCodes.OK).json(new response.SuccessResponse("OK")); } const results = await this.executeHealthChecks(); const allOk = results.every((r) => r.status); const status = allOk ? httpStatusCodes.HttpStatusCodes.OK : httpStatusCodes.HttpStatusCodes.SERVICE_UNAVAILABLE; const response$1 = new response.SuccessResponse(allOk ? "OK" : "Service unavailable"); if (!allOk) response$1.error = true; if (this.config.healthCheck?.detailed) response$1.data = { checks: results }; return res.status(status).json(response$1); } catch { return res.status(httpStatusCodes.HttpStatusCodes.INTERNAL_SERVER_ERROR).json(new exception.InternalServerErrorException("Health check failed")); } } /** * Execute all registered health checks and return results. */ async executeHealthChecks() { const checkResults = await Promise.allSettled(this.healthChecks.map(async ({ name, check }) => { try { const status = await Promise.resolve(check()); return { name, status, error: null }; } catch (error) { return { name, status: false, error: error.message }; } })); return checkResults.map((result) => { if (result.status === "fulfilled") return result.value; return { name: "unknown", status: false, error: result.reason }; }); } /** * Register a new health check function for monitoring service dependencies. * * Health checks are executed when the health endpoint is accessed and * help determine if the service is ready to handle requests. * * Examples: * - Database connectivity * - External service availability * - File system access * - Memory/CPU usage checks * * @param name Unique identifier for the check (used in detailed responses) * @param check Function returning boolean or Promise<boolean> indicating health * @returns This instance for method chaining */ registerHealthCheck(name, check) { this.healthChecks.push({ name, check }); return this; } /** * Run registered health checks and return whether the service is ready. * Useful for readiness probes in deployment tooling. * * @returns Promise resolving to `true` when all checks pass, otherwise `false`. */ async ready() { try { if (!this.healthChecks.length || config.getCatbeeServerGlobalConfig().skipHealthzChecksValidation) return true; const results = await this.executeHealthChecks(); return results.every((r) => r.status === true); } catch (err) { logger.getLogger().error({ err }, "Error while running readiness checks"); return false; } } /** * Get the underlying Express application instance. * Use this for advanced Express features not exposed by this wrapper. * * @returns The raw Express app instance */ getApp() { return this.app; } /** * Get the active HTTP/HTTPS server instance. * Returns null if the server is not currently running. * * @returns The HTTP/HTTPS server instance or null */ getServer() { return this.server; } /** * Start the HTTP server and begin listening for requests. * * This method: * - Executes beforeStart hooks * - Binds to the configured host/port * - Sets up error handling for startup failures * - Executes afterStart hooks on success * - Logs startup information * * @returns Promise resolving to the running HTTP server instance * @throws Error if server fails to start or port is already in use */ async start() { await this.initPromise; await this.runHook("beforeStart", this.app); return new Promise((resolve, reject) => { try { const onListening = /* @__PURE__ */ __name(async () => { this.logServerStartInfo(); if (this.server) await this.runHook("afterStart", this.server); resolve(this.server); }, "onListening"); this.server = this.createServerInstance(onListening); this.setupConnectionTracking(); this.setupServerErrorHandling(reject); } catch (error) { reject(error); } }); } /** * Create HTTP or HTTPS server instance. */ createServerInstance(onListening) { const listenArgs = [ this.config.port, this.config.host, onListening ]; if (this.config.https) { const httpsOptions = { ...this.config.https, key: fs.readFileSync(this.config.https.key), cert: fs.readFileSync(this.config.https.cert) }; if (this.config.https.ca) { httpsOptions.ca = fs.readFileSync(this.config.https.ca); } if (this.config.https.passphrase) { httpsOptions.passphrase = this.config.https.passphrase; } return https__default.default.createServer(httpsOptions, this.app).listen(...listenArgs); } return this.app.listen(...listenArgs); } /** * Set up connection tracking for graceful shutdown. */ setupConnectionTracking() { this.server.on("connection", (conn) => { this.connections.add(conn); conn.on("close", () => this.connections.delete(conn)); }); } /** * Set up error handling for server startup. */ setupServerErrorHandling(reject) { this.server.on("error", (err) => { logger.getLogger().error({ err }, "Server failed to start"); reject(err); }); } /** * Log server startup information. */ logServerStartInfo() { const protocol = this.config.https ? "https" : "http"; const url = `${protocol}://${this.config.host}:${this.config.port}`; logger.getLogger().info(`Server running on ${url}`); if (this.config.healthCheck?.path) { logger.getLogger().info(`Health check available at ${url}${this.normalizePath(this.config.healthCheck.path, this.config.healthCheck.withGlobalPrefix)}`); } if (this.config.metrics?.enable && this.config.metrics.path) { logger.getLogger().info(`Metrics available at ${url}${this.normalizePath(this.config.metrics.path, this.config.metrics.withGlobalPrefix)}`); } if (this.config.openApi?.enable) { logger.getLogger().info(`API docs available at ${url}${this.normalizePath(this.config.openApi.mountPath, this.config.openApi.withGlobalPrefix)}`); } } /** * Stop the HTTP server gracefully. * * This method: * - Executes beforeStop hooks * - Stops accepting new connections * - Waits for existing connections to finish * - Closes the server * - Executes afterStop hooks * - Logs shutdown information * * Graceful shutdown ensures: * - No requests are dropped * - Resources are properly cleaned up * - Monitoring systems are notified */ async stop(force = false) { if (!this.server) { logger.getLogger().warn("Stop called but server is not running"); return; } if (this.isShuttingDown) { logger.getLogger().warn("Stop called while shutdown is already in progress"); return; } this.isShuttingDown = true; await this.runHook("beforeStop", this.server); try { await this.gracefulShutdown(); } catch (err) { logger.getLogger().error({ err }, "Graceful shutdown timed out"); if (force) { logger.getLogger().warn("Forcing connection destroy due to shutdown timeout"); } } finally { await this.destroyConnections(); } } /** * Perform graceful server shutdown with timeout. */ async gracefulShutdown() { const shutdownTimeout = 1e4; const serverClosePromise = new Promise((resolve, reject) => { this.server.close(async (err) => { if (err) { logger.getLogger().error({ err }, "Error while closing server"); reject(err); return; } this.server = null; this.isShuttingDown = false; logger.getLogger().info("Server stopped gracefully"); await this.runHook("afterStop"); resolve(); }); }); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Shutdown timeout")), shutdownTimeout)); await Promise.race([ serverClosePromise, timeoutPromise ]); } /** * Enable graceful shutdown on OS signals for production deployment. * * This is essential for: * - Container orchestration (Docker, Kubernetes) * - Process managers (PM2, systemd) * - Load balancer health checks * - Zero-downtime deployments * * @param signals Array of process signals to listen for (default: SIGINT, SIGTERM) */ enableGracefulShutdown(signals = [ "SIGINT", "SIGTERM" ]) { signals.forEach((signal) => { process.on(signal, async () => { logger.getLogger().info(`Received ${signal}, initiating graceful shutdown...`); try { await this.stop(); process.exit(0); } catch (err) { logger.getLogger().error({ err }, "Error during graceful shutdown, forcing stop..."); try { await this.stop(true); process.exit(1); } catch (forceError) { logger.getLogger().fatal({ forceError }, "Forced shutdown failed, exiting hard"); process.exit(1); } } }); }); return this; } /** * Set an externally created base router. * This will override the internal rootRouter. */ setBaseRouter(router) { this.externalRouter = router; return this; } /** * Create and register a new router (only used if not injecting one externally - use `setBaseRouter` instead). */ createRouter(prefix = "") { const router = express__default.default.Router(); const path = this.normalizePath(prefix, true); this.rootRouter.use(path, router); return router; } /** * Register a new route handler with support for multiple HTTP methods. * The route is automatically registered under the globalPrefix if set. * * @param methods Array of HTTP methods (get, post, put, delete, etc.) * @param path Route path with Express path patterns support * @param handlers One or more Express request handlers (middleware + final handler) * @returns This instance for method chaining */ registerRoute(methods, path, ...handlers) { const fullPath = this.normalizePath(path, true); const methodMap = { get: this.app.get.bind(this.app), post: this.app.post.bind(this.app), put: this.app.put.bind(this.app), delete: this.app.delete.bind(this.app), patch: this.app.patch.bind(this.app), options: this.app.options.bind(this.app), head: this.app.head.bind(this.app) }; methods.forEach((m) => { const fn = methodMap[m]; if (fn) { fn(fullPath, ...handlers); } else { throw new Error(`Unsupported HTTP method: ${m}`); } }); return this; } /** * Register custom middleware with optional path restriction. * * Use this for: * - Adding authentication to specific routes * - Custom logging or validation * - Request transformation * - Third-party middleware integration * * @param path Optional path prefix or middleware function if no path * @param middleware Middleware handler (required if path is provided) * @returns This instance for method chaining */ registerMiddleware(path, middleware) { if (typeof path === "string") { const normalizedPath = this.normalizePath(path); if (normalizedPath) { this.app.use(normalizedPath, middleware); } else { this.app.use(middleware); } } else { this.app.use(path); } return this; } /** * Register one or more middleware functions to be applied globally. * This is a simpler alternative to registerMiddleware when you just want * to add middleware without path restrictions. * * @param middlewares One or more Express middleware functions * @returns This instance for method chaining */ useMiddleware(...middlewares) { middlewares.forEach((middleware) => { this.app.use(middleware); }); return this; } /** * Get Prometheus registry (to add custom counters/histograms) * * @return {*} {client.Registry} */ getMetricsRegistry() { if (!this.config.metrics?.enable) { logger.getLogger().warn("Metrics are not enabled in the server configuration \nPlease enable metrics to use this feature."); } return this.register; } /** * Get server configuration * * @return {*} {CatbeeServerConfig} */ getConfig() { return this.config; } /** * Wait until server initialization (middleware + routes) has completed. * Useful for integration tests that inspect app before starting.