From bb19edd7e31b22776858eca2a09d7d1e1f3db599 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sun, 5 Oct 2025 01:43:58 +0300 Subject: [PATCH] feat: add health check endpoint for Docker compatibility --- docs/CONFIGURATION.md | 1 + src/backend/handlers/healthHandler.ts | 35 +++++++++++++++++++++++++++ src/backend/handlers/index.ts | 1 + src/backend/index.ts | 4 +++ src/backend/routes/api.ts | 30 ++--------------------- 5 files changed, 43 insertions(+), 28 deletions(-) create mode 100644 src/backend/handlers/healthHandler.ts create mode 100644 src/backend/handlers/index.ts diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 57999b1..95a8f44 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -109,6 +109,7 @@ ENABLE_HTTPS_SECURITY=false - Useful for reverse proxy scenarios or hosting multiple apps on same domain - **Example**: `/geoip-ui` - **Note**: All API routes are prefixed with this path automatically +- **Health Check**: The `/api/health` endpoint is ALWAYS available at root (`/api/health`) for Docker HEALTHCHECK compatibility, AND also at `{BASE_PATH}/api/health` for consistency **`NODE_ENV`** diff --git a/src/backend/handlers/healthHandler.ts b/src/backend/handlers/healthHandler.ts new file mode 100644 index 0000000..a308b4b --- /dev/null +++ b/src/backend/handlers/healthHandler.ts @@ -0,0 +1,35 @@ +import { Request, Response } from 'express'; +import geoIPService from '../services/geoip.js'; +import logger from '../services/logger.js'; + +export const healthCheckHandler = async ( + _req: Request, + res: Response +): Promise => { + try { + const isHealthy = await geoIPService.healthCheck(); + + if (isHealthy) { + res.json({ + status: 'healthy', + timestamp: new Date().toISOString(), + service: 'Bitip GeoIP Service', + }); + } else { + res.status(503).json({ + status: 'unhealthy', + timestamp: new Date().toISOString(), + service: 'Bitip GeoIP Service', + error: 'GeoIP service health check failed', + }); + } + } catch (error) { + logger.error('Health check failed', error as Error); + res.status(503).json({ + status: 'unhealthy', + timestamp: new Date().toISOString(), + service: 'Bitip GeoIP Service', + error: 'Health check endpoint failed', + }); + } +}; diff --git a/src/backend/handlers/index.ts b/src/backend/handlers/index.ts new file mode 100644 index 0000000..118f06d --- /dev/null +++ b/src/backend/handlers/index.ts @@ -0,0 +1 @@ +export * from './healthHandler.js'; diff --git a/src/backend/index.ts b/src/backend/index.ts index 21c06b2..b27d56d 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -12,6 +12,7 @@ import dynamicRateLimit from './middleware/rateLimit.js'; import config from './services/config.js'; import logger from './services/logger.js'; import { generateRuntimeConfig } from './services/runtimeConfig.js'; +import { healthCheckHandler } from './handlers'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -76,6 +77,9 @@ const frontendPath = path.join(__dirname, '../frontend'); // Generate runtime configuration (env.js) at startup generateRuntimeConfig(frontendPath, basePath); +// Health check endpoint at root (always accessible for Docker HEALTHCHECK) +app.get('/api/health', healthCheckHandler); + // API routes with authentication and rate limiting app.use(`${basePath}/api`, apiKeyAuth, dynamicRateLimit, apiRoutes); diff --git a/src/backend/routes/api.ts b/src/backend/routes/api.ts index d973a87..2e1c384 100644 --- a/src/backend/routes/api.ts +++ b/src/backend/routes/api.ts @@ -6,6 +6,7 @@ import { dirname, join } from 'path'; import geoIPService from '../services/geoip.js'; import logger from '../services/logger.js'; import config from '../services/config.js'; +import { healthCheckHandler } from '../handlers'; import { BatchGeoIPRequest, BatchGeoIPResponse, @@ -252,34 +253,7 @@ router.post('/lookup/batch', async (req: Request, res: Response) => { }); // Health check -router.get('/health', async (req: Request, res: Response) => { - try { - const isHealthy = await geoIPService.healthCheck(); - - if (isHealthy) { - res.json({ - status: 'healthy', - timestamp: new Date().toISOString(), - service: 'Bitip GeoIP Service', - }); - } else { - res.status(503).json({ - status: 'unhealthy', - timestamp: new Date().toISOString(), - service: 'Bitip GeoIP Service', - error: 'GeoIP service health check failed', - }); - } - } catch (error) { - logger.error('Health check failed', error as Error); - res.status(503).json({ - status: 'unhealthy', - timestamp: new Date().toISOString(), - service: 'Bitip GeoIP Service', - error: 'Health check endpoint failed', - }); - } -}); +router.get('/health', healthCheckHandler); // Get app version router.get('/version', (_req: Request, res: Response): void => {