@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
JavaScript
/*
* 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.