@naman_deep_singh/server-utils 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +238 -0
- package/dist/health.d.ts +5 -0
- package/dist/health.js +45 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +40 -0
- package/dist/middleware.d.ts +37 -0
- package/dist/middleware.js +217 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +43 -0
- package/dist/shutdown.d.ts +5 -0
- package/dist/shutdown.js +50 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +37 -0
- package/package.json +32 -0
- package/src/health.ts +47 -0
- package/src/index.ts +46 -0
- package/src/middleware.ts +272 -0
- package/src/server.ts +46 -0
- package/src/shutdown.ts +60 -0
- package/src/types.ts +29 -0
- package/src/utils.ts +34 -0
- package/tsconfig.json +17 -0
package/dist/shutdown.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createGracefulShutdown = createGracefulShutdown;
|
|
4
|
+
exports.withGracefulShutdown = withGracefulShutdown;
|
|
5
|
+
exports.startServerWithShutdown = startServerWithShutdown;
|
|
6
|
+
function createGracefulShutdown(server, config = {}) {
|
|
7
|
+
const { timeout = 10000, onShutdown } = config;
|
|
8
|
+
const shutdown = async (signal) => {
|
|
9
|
+
console.log(`🛑 Received ${signal}, shutting down gracefully...`);
|
|
10
|
+
const shutdownTimer = setTimeout(() => {
|
|
11
|
+
console.log('⏰ Shutdown timeout reached, forcing exit');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}, timeout);
|
|
14
|
+
try {
|
|
15
|
+
// Run custom shutdown logic
|
|
16
|
+
if (onShutdown) {
|
|
17
|
+
await onShutdown();
|
|
18
|
+
}
|
|
19
|
+
// Close server
|
|
20
|
+
server.close(() => {
|
|
21
|
+
clearTimeout(shutdownTimer);
|
|
22
|
+
console.log('👋 Server closed. Exiting now.');
|
|
23
|
+
process.exit(0);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
clearTimeout(shutdownTimer);
|
|
28
|
+
console.error('❌ Error during shutdown:', error);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
33
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
34
|
+
}
|
|
35
|
+
function withGracefulShutdown(config = {}) {
|
|
36
|
+
return (app, serverConfig) => {
|
|
37
|
+
// This plugin needs to be applied after server.listen()
|
|
38
|
+
// Store config for later use
|
|
39
|
+
app.__gracefulShutdownConfig = config;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function startServerWithShutdown(app, port, shutdownConfig = {}) {
|
|
43
|
+
const server = app.listen(port, () => {
|
|
44
|
+
console.log(`🚀 Server running on http://localhost:${port}`);
|
|
45
|
+
});
|
|
46
|
+
// Apply graceful shutdown from stored config or provided config
|
|
47
|
+
const config = app.__gracefulShutdownConfig || shutdownConfig;
|
|
48
|
+
createGracefulShutdown(server, config);
|
|
49
|
+
return server;
|
|
50
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { CorsOptions } from 'cors';
|
|
3
|
+
export interface ServerConfig {
|
|
4
|
+
port?: number;
|
|
5
|
+
cors?: boolean | CorsOptions;
|
|
6
|
+
helmet?: boolean;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
customMiddleware?: express.RequestHandler[];
|
|
9
|
+
healthCheck?: boolean | string;
|
|
10
|
+
gracefulShutdown?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface HealthCheckConfig {
|
|
13
|
+
path?: string;
|
|
14
|
+
customChecks?: HealthCheck[];
|
|
15
|
+
}
|
|
16
|
+
export interface HealthCheck {
|
|
17
|
+
name: string;
|
|
18
|
+
check: () => Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
export interface GracefulShutdownConfig {
|
|
21
|
+
timeout?: number;
|
|
22
|
+
onShutdown?: () => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export type ServerPlugin = (app: express.Application, config: ServerConfig) => void;
|
package/dist/types.js
ADDED
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getEnv = getEnv;
|
|
4
|
+
exports.getEnvNumber = getEnvNumber;
|
|
5
|
+
exports.getEnvBoolean = getEnvBoolean;
|
|
6
|
+
// Environment utilities
|
|
7
|
+
function getEnv(key, defaultValue) {
|
|
8
|
+
const value = process.env[key];
|
|
9
|
+
if (value === undefined && defaultValue === undefined) {
|
|
10
|
+
throw new Error(`Environment variable ${key} is required`);
|
|
11
|
+
}
|
|
12
|
+
return value || defaultValue;
|
|
13
|
+
}
|
|
14
|
+
function getEnvNumber(key, defaultValue) {
|
|
15
|
+
const value = process.env[key];
|
|
16
|
+
if (value === undefined) {
|
|
17
|
+
if (defaultValue === undefined) {
|
|
18
|
+
throw new Error(`Environment variable ${key} is required`);
|
|
19
|
+
}
|
|
20
|
+
return defaultValue;
|
|
21
|
+
}
|
|
22
|
+
const parsed = parseInt(value, 10);
|
|
23
|
+
if (isNaN(parsed)) {
|
|
24
|
+
throw new Error(`Environment variable ${key} must be a number`);
|
|
25
|
+
}
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
function getEnvBoolean(key, defaultValue) {
|
|
29
|
+
const value = process.env[key];
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
if (defaultValue === undefined) {
|
|
32
|
+
throw new Error(`Environment variable ${key} is required`);
|
|
33
|
+
}
|
|
34
|
+
return defaultValue;
|
|
35
|
+
}
|
|
36
|
+
return value.toLowerCase() === 'true';
|
|
37
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@naman_deep_singh/server-utils",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Extensible server utilities for Express.js microservices with TypeScript",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"server",
|
|
12
|
+
"express",
|
|
13
|
+
"microservices",
|
|
14
|
+
"utils",
|
|
15
|
+
"typescript"
|
|
16
|
+
],
|
|
17
|
+
"author": "Naman Deep Singh",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"packageManager": "pnpm@10.20.0",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"express": "^5.1.0",
|
|
22
|
+
"cors": "^2.8.5",
|
|
23
|
+
"helmet": "^8.1.0",
|
|
24
|
+
"cookie-parser": "^1.4.6"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/express": "^5.0.5",
|
|
28
|
+
"@types/cors": "^2.8.19",
|
|
29
|
+
"@types/cookie-parser": "^1.4.7",
|
|
30
|
+
"typescript": "^5.9.3"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/health.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { HealthCheckConfig, HealthCheck, ServerPlugin } from './types';
|
|
3
|
+
|
|
4
|
+
export function createHealthCheck(config: HealthCheckConfig = {}): express.RequestHandler {
|
|
5
|
+
const { customChecks = [] } = config;
|
|
6
|
+
|
|
7
|
+
return async (req: express.Request, res: express.Response) => {
|
|
8
|
+
try {
|
|
9
|
+
const checks: Record<string, boolean> = {
|
|
10
|
+
server: true,
|
|
11
|
+
timestamp: Date.now() as any
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Run custom health checks
|
|
15
|
+
for (const check of customChecks) {
|
|
16
|
+
try {
|
|
17
|
+
checks[check.name] = await check.check();
|
|
18
|
+
} catch (error) {
|
|
19
|
+
checks[check.name] = false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const isHealthy = Object.values(checks).every(status => status === true || typeof status === 'number');
|
|
24
|
+
|
|
25
|
+
res.status(isHealthy ? 200 : 503).json({
|
|
26
|
+
status: isHealthy ? 'healthy' : 'unhealthy',
|
|
27
|
+
checks
|
|
28
|
+
});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
res.status(503).json({
|
|
31
|
+
status: 'unhealthy',
|
|
32
|
+
error: 'Health check failed'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function withHealthCheck(path: string = '/health', config: HealthCheckConfig = {}): ServerPlugin {
|
|
39
|
+
return (app: express.Application) => {
|
|
40
|
+
app.get(path, createHealthCheck(config));
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Convenience function for direct use
|
|
45
|
+
export function addHealthCheck(app: express.Application, path: string = '/health', config: HealthCheckConfig = {}): void {
|
|
46
|
+
app.get(path, createHealthCheck(config));
|
|
47
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Core server utilities
|
|
2
|
+
export { createServer, withPlugin, createServerWithPlugins } from './server';
|
|
3
|
+
|
|
4
|
+
// Health check utilities
|
|
5
|
+
export { createHealthCheck, withHealthCheck, addHealthCheck } from './health';
|
|
6
|
+
|
|
7
|
+
// Graceful shutdown utilities
|
|
8
|
+
export { createGracefulShutdown, withGracefulShutdown, startServerWithShutdown } from './shutdown';
|
|
9
|
+
|
|
10
|
+
// Middleware utilities
|
|
11
|
+
export {
|
|
12
|
+
createLoggingMiddleware,
|
|
13
|
+
createErrorHandler,
|
|
14
|
+
createRequestIdMiddleware,
|
|
15
|
+
createValidationMiddleware,
|
|
16
|
+
createRateLimitMiddleware,
|
|
17
|
+
createAuthMiddleware,
|
|
18
|
+
withLogging,
|
|
19
|
+
withErrorHandler,
|
|
20
|
+
withRequestId,
|
|
21
|
+
withValidation,
|
|
22
|
+
withRateLimit,
|
|
23
|
+
withAuth,
|
|
24
|
+
validateFields,
|
|
25
|
+
rateLimit,
|
|
26
|
+
requireAuth,
|
|
27
|
+
type ValidationRule,
|
|
28
|
+
type RateLimitConfig,
|
|
29
|
+
type AuthConfig
|
|
30
|
+
} from './middleware';
|
|
31
|
+
|
|
32
|
+
// Utility functions
|
|
33
|
+
export {
|
|
34
|
+
getEnv,
|
|
35
|
+
getEnvNumber,
|
|
36
|
+
getEnvBoolean
|
|
37
|
+
} from './utils';
|
|
38
|
+
|
|
39
|
+
// Types
|
|
40
|
+
export type {
|
|
41
|
+
ServerConfig,
|
|
42
|
+
HealthCheckConfig,
|
|
43
|
+
HealthCheck,
|
|
44
|
+
GracefulShutdownConfig,
|
|
45
|
+
ServerPlugin
|
|
46
|
+
} from './types';
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { ServerPlugin } from './types';
|
|
3
|
+
|
|
4
|
+
// Logging middleware
|
|
5
|
+
export function createLoggingMiddleware(format: 'simple' | 'detailed' = 'simple'): express.RequestHandler {
|
|
6
|
+
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
|
|
9
|
+
res.on('finish', () => {
|
|
10
|
+
const duration = Date.now() - start;
|
|
11
|
+
|
|
12
|
+
if (format === 'detailed') {
|
|
13
|
+
console.log(`${req.method} ${req.url} - ${res.statusCode} - ${duration}ms - ${req.ip}`);
|
|
14
|
+
} else {
|
|
15
|
+
console.log(`${req.method} ${req.url} - ${res.statusCode}`);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
next();
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Error handling middleware
|
|
24
|
+
export function createErrorHandler(): express.ErrorRequestHandler {
|
|
25
|
+
return (err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
26
|
+
console.error('Error:', err);
|
|
27
|
+
|
|
28
|
+
if (res.headersSent) {
|
|
29
|
+
return next(err);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const status = err.status || err.statusCode || 500;
|
|
33
|
+
const message = process.env.NODE_ENV === 'production'
|
|
34
|
+
? 'Internal Server Error'
|
|
35
|
+
: err.message;
|
|
36
|
+
|
|
37
|
+
res.status(status).json({
|
|
38
|
+
status: false,
|
|
39
|
+
message,
|
|
40
|
+
...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Request ID middleware
|
|
46
|
+
export function createRequestIdMiddleware(): express.RequestHandler {
|
|
47
|
+
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
48
|
+
const requestId = Math.random().toString(36).substring(2, 15);
|
|
49
|
+
(req as any).requestId = requestId;
|
|
50
|
+
res.setHeader('X-Request-ID', requestId);
|
|
51
|
+
next();
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Validation middleware
|
|
56
|
+
export interface ValidationRule {
|
|
57
|
+
field: string;
|
|
58
|
+
required?: boolean;
|
|
59
|
+
type?: 'string' | 'number' | 'email' | 'boolean';
|
|
60
|
+
minLength?: number;
|
|
61
|
+
maxLength?: number;
|
|
62
|
+
pattern?: RegExp;
|
|
63
|
+
custom?: (value: any) => boolean | string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createValidationMiddleware(rules: ValidationRule[]): express.RequestHandler {
|
|
67
|
+
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
68
|
+
const errors: string[] = [];
|
|
69
|
+
|
|
70
|
+
for (const rule of rules) {
|
|
71
|
+
const value = req.body[rule.field];
|
|
72
|
+
|
|
73
|
+
if (rule.required && (value === undefined || value === null || value === '')) {
|
|
74
|
+
errors.push(`${rule.field} is required`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (value === undefined || value === null) continue;
|
|
79
|
+
|
|
80
|
+
if (rule.type) {
|
|
81
|
+
switch (rule.type) {
|
|
82
|
+
case 'email':
|
|
83
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
84
|
+
if (!emailRegex.test(value)) {
|
|
85
|
+
errors.push(`${rule.field} must be a valid email`);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case 'string':
|
|
89
|
+
if (typeof value !== 'string') {
|
|
90
|
+
errors.push(`${rule.field} must be a string`);
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
case 'number':
|
|
94
|
+
if (typeof value !== 'number' && isNaN(Number(value))) {
|
|
95
|
+
errors.push(`${rule.field} must be a number`);
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
case 'boolean':
|
|
99
|
+
if (typeof value !== 'boolean') {
|
|
100
|
+
errors.push(`${rule.field} must be a boolean`);
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (rule.minLength && value.length < rule.minLength) {
|
|
107
|
+
errors.push(`${rule.field} must be at least ${rule.minLength} characters`);
|
|
108
|
+
}
|
|
109
|
+
if (rule.maxLength && value.length > rule.maxLength) {
|
|
110
|
+
errors.push(`${rule.field} must be no more than ${rule.maxLength} characters`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (rule.pattern && !rule.pattern.test(value)) {
|
|
114
|
+
errors.push(`${rule.field} format is invalid`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (rule.custom) {
|
|
118
|
+
const result = rule.custom(value);
|
|
119
|
+
if (result !== true) {
|
|
120
|
+
errors.push(typeof result === 'string' ? result : `${rule.field} is invalid`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (errors.length > 0) {
|
|
126
|
+
return res.status(400).json({
|
|
127
|
+
status: false,
|
|
128
|
+
message: 'Validation failed',
|
|
129
|
+
errors
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
next();
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Rate limiting middleware
|
|
138
|
+
export interface RateLimitConfig {
|
|
139
|
+
windowMs?: number;
|
|
140
|
+
maxRequests?: number;
|
|
141
|
+
message?: string;
|
|
142
|
+
keyGenerator?: (req: express.Request) => string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
|
|
146
|
+
|
|
147
|
+
export function createRateLimitMiddleware(config: RateLimitConfig = {}): express.RequestHandler {
|
|
148
|
+
const {
|
|
149
|
+
windowMs = 15 * 60 * 1000,
|
|
150
|
+
maxRequests = 100,
|
|
151
|
+
message = 'Too many requests, please try again later',
|
|
152
|
+
keyGenerator = (req) => req.ip || 'unknown'
|
|
153
|
+
} = config;
|
|
154
|
+
|
|
155
|
+
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
156
|
+
const key = keyGenerator(req);
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
const record = rateLimitStore.get(key);
|
|
159
|
+
|
|
160
|
+
if (!record || now > record.resetTime) {
|
|
161
|
+
rateLimitStore.set(key, {
|
|
162
|
+
count: 1,
|
|
163
|
+
resetTime: now + windowMs
|
|
164
|
+
});
|
|
165
|
+
return next();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (record.count >= maxRequests) {
|
|
169
|
+
return res.status(429).json({
|
|
170
|
+
status: false,
|
|
171
|
+
message,
|
|
172
|
+
retryAfter: Math.ceil((record.resetTime - now) / 1000)
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
record.count++;
|
|
177
|
+
next();
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Authentication middleware helper
|
|
182
|
+
export interface AuthConfig {
|
|
183
|
+
tokenExtractor?: (req: express.Request) => string | null;
|
|
184
|
+
tokenValidator?: (token: string) => Promise<any> | any;
|
|
185
|
+
unauthorizedMessage?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function createAuthMiddleware(config: AuthConfig): express.RequestHandler {
|
|
189
|
+
const {
|
|
190
|
+
tokenExtractor = (req) => {
|
|
191
|
+
const authHeader = req.headers.authorization;
|
|
192
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
193
|
+
return authHeader.substring(7);
|
|
194
|
+
}
|
|
195
|
+
return req.cookies?.token || null;
|
|
196
|
+
},
|
|
197
|
+
tokenValidator = () => { throw new Error('Token validator not implemented'); },
|
|
198
|
+
unauthorizedMessage = 'Unauthorized access'
|
|
199
|
+
} = config;
|
|
200
|
+
|
|
201
|
+
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
202
|
+
try {
|
|
203
|
+
const token = tokenExtractor(req);
|
|
204
|
+
|
|
205
|
+
if (!token) {
|
|
206
|
+
return res.status(401).json({
|
|
207
|
+
status: false,
|
|
208
|
+
message: unauthorizedMessage
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const user = await tokenValidator(token);
|
|
213
|
+
(req as any).user = user;
|
|
214
|
+
next();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
return res.status(401).json({
|
|
217
|
+
status: false,
|
|
218
|
+
message: unauthorizedMessage
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Plugin versions
|
|
225
|
+
export function withLogging(format: 'simple' | 'detailed' = 'simple'): ServerPlugin {
|
|
226
|
+
return (app: express.Application) => {
|
|
227
|
+
app.use(createLoggingMiddleware(format));
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function withErrorHandler(): ServerPlugin {
|
|
232
|
+
return (app: express.Application) => {
|
|
233
|
+
app.use(createErrorHandler());
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function withRequestId(): ServerPlugin {
|
|
238
|
+
return (app: express.Application) => {
|
|
239
|
+
app.use(createRequestIdMiddleware());
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function withValidation(rules: ValidationRule[]): ServerPlugin {
|
|
244
|
+
return (app: express.Application) => {
|
|
245
|
+
app.use(createValidationMiddleware(rules));
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function withRateLimit(config: RateLimitConfig = {}): ServerPlugin {
|
|
250
|
+
return (app: express.Application) => {
|
|
251
|
+
app.use(createRateLimitMiddleware(config));
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function withAuth(config: AuthConfig): ServerPlugin {
|
|
256
|
+
return (app: express.Application) => {
|
|
257
|
+
app.use(createAuthMiddleware(config));
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Convenience functions for route-specific middleware
|
|
262
|
+
export function validateFields(rules: ValidationRule[]): express.RequestHandler {
|
|
263
|
+
return createValidationMiddleware(rules);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function rateLimit(config: RateLimitConfig = {}): express.RequestHandler {
|
|
267
|
+
return createRateLimitMiddleware(config);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function requireAuth(config: AuthConfig): express.RequestHandler {
|
|
271
|
+
return createAuthMiddleware(config);
|
|
272
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import helmet from 'helmet';
|
|
4
|
+
import { ServerConfig, ServerPlugin } from './types';
|
|
5
|
+
|
|
6
|
+
export function createServer(config: ServerConfig = {}): express.Application {
|
|
7
|
+
const app = express();
|
|
8
|
+
|
|
9
|
+
// Apply default middleware
|
|
10
|
+
if (config.helmet !== false) {
|
|
11
|
+
app.use(helmet());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (config.cors !== false) {
|
|
15
|
+
const corsOptions = typeof config.cors === 'object' ? config.cors : undefined;
|
|
16
|
+
app.use(cors(corsOptions));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (config.json !== false) {
|
|
20
|
+
app.use(express.json());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Apply custom middleware
|
|
24
|
+
if (config.customMiddleware) {
|
|
25
|
+
config.customMiddleware.forEach(middleware => {
|
|
26
|
+
app.use(middleware);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return app;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function withPlugin(app: express.Application, plugin: ServerPlugin, config: ServerConfig = {}): express.Application {
|
|
34
|
+
plugin(app, config);
|
|
35
|
+
return app;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createServerWithPlugins(config: ServerConfig = {}, ...plugins: ServerPlugin[]): express.Application {
|
|
39
|
+
const app = createServer(config);
|
|
40
|
+
|
|
41
|
+
plugins.forEach(plugin => {
|
|
42
|
+
plugin(app, config);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return app;
|
|
46
|
+
}
|
package/src/shutdown.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Server } from 'http';
|
|
2
|
+
import { GracefulShutdownConfig, ServerPlugin } from './types';
|
|
3
|
+
|
|
4
|
+
export function createGracefulShutdown(server: Server, config: GracefulShutdownConfig = {}): void {
|
|
5
|
+
const { timeout = 10000, onShutdown } = config;
|
|
6
|
+
|
|
7
|
+
const shutdown = async (signal: string) => {
|
|
8
|
+
console.log(`🛑 Received ${signal}, shutting down gracefully...`);
|
|
9
|
+
|
|
10
|
+
const shutdownTimer = setTimeout(() => {
|
|
11
|
+
console.log('⏰ Shutdown timeout reached, forcing exit');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}, timeout);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Run custom shutdown logic
|
|
17
|
+
if (onShutdown) {
|
|
18
|
+
await onShutdown();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Close server
|
|
22
|
+
server.close(() => {
|
|
23
|
+
clearTimeout(shutdownTimer);
|
|
24
|
+
console.log('👋 Server closed. Exiting now.');
|
|
25
|
+
process.exit(0);
|
|
26
|
+
});
|
|
27
|
+
} catch (error) {
|
|
28
|
+
clearTimeout(shutdownTimer);
|
|
29
|
+
console.error('❌ Error during shutdown:', error);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
35
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function withGracefulShutdown(config: GracefulShutdownConfig = {}): ServerPlugin {
|
|
39
|
+
return (app, serverConfig) => {
|
|
40
|
+
// This plugin needs to be applied after server.listen()
|
|
41
|
+
// Store config for later use
|
|
42
|
+
(app as any).__gracefulShutdownConfig = config;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function startServerWithShutdown(
|
|
47
|
+
app: any,
|
|
48
|
+
port: number,
|
|
49
|
+
shutdownConfig: GracefulShutdownConfig = {}
|
|
50
|
+
): Server {
|
|
51
|
+
const server = app.listen(port, () => {
|
|
52
|
+
console.log(`🚀 Server running on http://localhost:${port}`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Apply graceful shutdown from stored config or provided config
|
|
56
|
+
const config = app.__gracefulShutdownConfig || shutdownConfig;
|
|
57
|
+
createGracefulShutdown(server, config);
|
|
58
|
+
|
|
59
|
+
return server;
|
|
60
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { CorsOptions } from 'cors';
|
|
3
|
+
|
|
4
|
+
export interface ServerConfig {
|
|
5
|
+
port?: number;
|
|
6
|
+
cors?: boolean | CorsOptions;
|
|
7
|
+
helmet?: boolean;
|
|
8
|
+
json?: boolean;
|
|
9
|
+
customMiddleware?: express.RequestHandler[];
|
|
10
|
+
healthCheck?: boolean | string;
|
|
11
|
+
gracefulShutdown?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface HealthCheckConfig {
|
|
15
|
+
path?: string;
|
|
16
|
+
customChecks?: HealthCheck[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HealthCheck {
|
|
20
|
+
name: string;
|
|
21
|
+
check: () => Promise<boolean>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GracefulShutdownConfig {
|
|
25
|
+
timeout?: number;
|
|
26
|
+
onShutdown?: () => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ServerPlugin = (app: express.Application, config: ServerConfig) => void;
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Environment utilities
|
|
2
|
+
export function getEnv(key: string, defaultValue?: string): string {
|
|
3
|
+
const value = process.env[key];
|
|
4
|
+
if (value === undefined && defaultValue === undefined) {
|
|
5
|
+
throw new Error(`Environment variable ${key} is required`);
|
|
6
|
+
}
|
|
7
|
+
return value || defaultValue!;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getEnvNumber(key: string, defaultValue?: number): number {
|
|
11
|
+
const value = process.env[key];
|
|
12
|
+
if (value === undefined) {
|
|
13
|
+
if (defaultValue === undefined) {
|
|
14
|
+
throw new Error(`Environment variable ${key} is required`);
|
|
15
|
+
}
|
|
16
|
+
return defaultValue;
|
|
17
|
+
}
|
|
18
|
+
const parsed = parseInt(value, 10);
|
|
19
|
+
if (isNaN(parsed)) {
|
|
20
|
+
throw new Error(`Environment variable ${key} must be a number`);
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getEnvBoolean(key: string, defaultValue?: boolean): boolean {
|
|
26
|
+
const value = process.env[key];
|
|
27
|
+
if (value === undefined) {
|
|
28
|
+
if (defaultValue === undefined) {
|
|
29
|
+
throw new Error(`Environment variable ${key} is required`);
|
|
30
|
+
}
|
|
31
|
+
return defaultValue;
|
|
32
|
+
}
|
|
33
|
+
return value.toLowerCase() === 'true';
|
|
34
|
+
}
|