lsh-framework
Version:
A powerful, extensible shell with advanced job management, database persistence, and modern CLI features
291 lines (290 loc) • 8.7 kB
JavaScript
/**
* Base API Server
* Abstract base class for all API servers to eliminate duplication in:
* - Express middleware setup
* - Server lifecycle management
* - Signal handling
* - Error handling
*/
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import { EventEmitter } from 'events';
import { createLogger } from './logger.js';
/**
* Abstract base class for API servers
*
* Provides:
* - Express app setup with common middleware
* - Server lifecycle (start/stop)
* - Signal handling (SIGTERM, SIGINT, SIGHUP)
* - Error handling (uncaughtException, unhandledRejection)
* - Request logging
* - Structured logging
*
* @example
* ```typescript
* class MyAPIServer extends BaseAPIServer {
* constructor() {
* super({ port: 3000 }, 'MyAPI');
* }
*
* protected setupRoutes(): void {
* this.app.get('/api/hello', (req, res) => {
* res.json({ message: 'Hello World' });
* });
* }
* }
* ```
*/
export class BaseAPIServer extends EventEmitter {
app;
server;
config;
logger;
isShuttingDown = false;
/**
* Create a new API server
*
* @param config - Server configuration
* @param loggerName - Name for the logger context
*/
constructor(config, loggerName) {
super();
this.config = {
port: 3000,
corsOrigins: '*',
enableHelmet: true,
jsonLimit: '10mb',
enableRequestLogging: true,
enableSignalHandlers: true,
enableErrorHandlers: true,
...config
};
this.logger = createLogger(loggerName);
this.app = express();
this.setupMiddleware();
if (this.config.enableErrorHandlers) {
this.setupErrorHandlers();
}
if (this.config.enableSignalHandlers) {
this.setupSignalHandlers();
}
}
/**
* Setup Express middleware
* Can be overridden for custom middleware setup
*/
setupMiddleware() {
// Security middleware
if (this.config.enableHelmet) {
this.app.use(helmet({
crossOriginEmbedderPolicy: false,
}));
}
// CORS
this.app.use(this.configureCORS());
// Body parsing
this.app.use(express.json({ limit: this.config.jsonLimit }));
this.app.use(express.urlencoded({ extended: true }));
// Request logging
if (this.config.enableRequestLogging) {
this.app.use(this.requestLogger.bind(this));
}
}
/**
* Configure CORS middleware
* Can be overridden for custom CORS configuration
*/
configureCORS() {
const origins = this.config.corsOrigins;
if (origins === '*') {
return cors();
}
if (Array.isArray(origins)) {
return cors({
origin: (origin, callback) => {
if (!origin || origins.some(pattern => {
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
return regex.test(origin);
})) {
callback(null, true);
}
else {
callback(new Error('Not allowed by CORS'));
}
}
});
}
return cors({ origin: origins });
}
/**
* Request logging middleware
*/
requestLogger(req, res, next) {
this.logger.info(`${req.method} ${req.path}`);
next();
}
/**
* Setup error handlers for uncaught exceptions and unhandled rejections
*/
setupErrorHandlers() {
process.on('uncaughtException', (error) => {
this.logger.error('Uncaught Exception', error);
this.handleFatalError(error);
});
process.on('unhandledRejection', (reason, promise) => {
this.logger.error('Unhandled Rejection', reason instanceof Error ? reason : new Error(String(reason)), {
promise: String(promise)
});
});
}
/**
* Setup signal handlers for graceful shutdown
*/
setupSignalHandlers() {
const signals = ['SIGTERM', 'SIGINT'];
signals.forEach(signal => {
process.on(signal, () => {
this.logger.info(`Received ${signal}, initiating graceful shutdown`);
this.gracefulShutdown(signal);
});
});
process.on('SIGHUP', () => {
this.logger.info('Received SIGHUP');
this.handleSIGHUP();
});
}
/**
* Handle SIGHUP signal - can be overridden for custom behavior (e.g., reload config)
*/
handleSIGHUP() {
this.logger.info('SIGHUP handler not implemented, ignoring');
}
/**
* Handle fatal errors
* @param error - The fatal error
*/
handleFatalError(error) {
this.logger.error('Fatal error, shutting down', error);
process.exit(1);
}
/**
* Perform graceful shutdown
* @param signal - The signal that triggered the shutdown
*/
async gracefulShutdown(signal) {
if (this.isShuttingDown) {
this.logger.warn('Shutdown already in progress');
return;
}
this.isShuttingDown = true;
this.logger.info(`Graceful shutdown initiated by ${signal}`);
try {
// Give ongoing requests time to complete
await this.stop();
this.logger.info('Server stopped successfully');
process.exit(0);
}
catch (error) {
this.logger.error('Error during shutdown', error);
process.exit(1);
}
}
/**
* Start the API server
* @returns Promise that resolves when server is listening
*/
async start() {
return new Promise((resolve, reject) => {
try {
// Setup routes before starting server
this.setupRoutes();
this.server = this.app.listen(this.config.port, () => {
this.logger.info(`Server started on port ${this.config.port}`);
this.emit('started');
resolve();
});
this.server.on('error', (error) => {
this.logger.error('Server error', error);
this.emit('error', error);
reject(error);
});
}
catch (error) {
this.logger.error('Failed to start server', error);
reject(error);
}
});
}
/**
* Stop the API server
* @param timeout - Maximum time to wait for connections to close (ms)
* @returns Promise that resolves when server is stopped
*/
async stop(timeout = 5000) {
return new Promise((resolve, reject) => {
if (!this.server) {
resolve();
return;
}
this.logger.info('Stopping server...');
// Set timeout for force shutdown
const forceShutdownTimer = setTimeout(() => {
this.logger.warn('Force closing server after timeout');
this.server?.close();
resolve();
}, timeout);
this.server?.close((error) => {
clearTimeout(forceShutdownTimer);
if (error) {
this.logger.error('Error stopping server', error);
reject(error);
}
else {
this.logger.info('Server stopped');
this.emit('stopped');
resolve();
}
});
// Allow subclasses to cleanup
this.onStop();
});
}
/**
* Hook called when server is stopping
* Override this to cleanup resources, close connections, etc.
*/
onStop() {
// Override in subclasses if needed
}
/**
* Get the Express application
* @returns The Express app instance
*/
getApp() {
return this.app;
}
/**
* Get the HTTP server
* @returns The HTTP server instance
*/
getServer() {
return this.server;
}
/**
* Check if server is running
* @returns True if server is running
*/
isRunning() {
return !!this.server?.listening;
}
/**
* Get server configuration
* @returns The server configuration
*/
getConfig() {
return { ...this.config };
}
}
export default BaseAPIServer;