From 484fcde93aab1de36f0ef80854ea54d5f1a328e9 Mon Sep 17 00:00:00 2001 From: Zaiba Machhaliya Date: Sat, 4 Jul 2026 11:51:35 +0530 Subject: [PATCH] fix: enhance server.js with graceful shutdown, env validation, and improved error handling --- backend/.env.example | 15 +++- backend/src/server.js | 182 +++++++++++++++++++++++++++++++++++------- 2 files changed, 167 insertions(+), 30 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index 057e9f8..d688170 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,7 +1,16 @@ -# Copy this file to .env and fill in your values -MONGO_URI=your_mongodb_connection_string_here +# Database +MONGO_URI=mongodb://localhost:27017/your_database_name + +# Server PORT=5001 +NODE_ENV=development +# Redis UPSTASH_REDIS_REST_URL=your_upstash_redis_url_here UPSTASH_REDIS_REST_TOKEN=your_upstash_redis_token_here -JWT_SECRET=any_random_string_for_jwt_secret \ No newline at end of file + +# JWT +JWT_SECRET=any_random_string_for_jwt_secret + +# CORS - Production ke liye +ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000 \ No newline at end of file diff --git a/backend/src/server.js b/backend/src/server.js index 7484eaf..b5b4db9 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -6,6 +6,7 @@ import cookieParser from "cookie-parser"; import { fileURLToPath } from "url"; import dns from "dns"; import jwt from "jsonwebtoken"; +import mongoose from "mongoose"; // Routes import notesRoutes from "./routes/notesRoutes.js"; @@ -25,24 +26,43 @@ dotenv.config(); dns.setServers(["1.1.1.1", "8.8.8.8"]); const app = express(); -// const PORT = process.env.PORT || 5001; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +// ==================== ENVIRONMENT VALIDATION ==================== +const requiredEnvVars = ["PORT", "MONGO_URI", "JWT_SECRET", "NODE_ENV"]; + +const validateEnv = () => { + const missing = requiredEnvVars.filter((varName) => !process.env[varName]); + if (missing.length > 0) { + console.error("Missing required environment variables:"); + missing.forEach((varName) => console.error(` - ${varName}`)); + process.exit(1); + } + console.log("All required environment variables are present"); +}; + +validateEnv(); + +// ==================== REDIS CLIENT REFERENCE ==================== +let redisClientInstance = null; + // ==================== MIDDLEWARE ==================== // CORS Configuration -if (process.env.NODE_ENV !== "production") { - app.use( - cors({ - origin: "http://localhost:5173", - credentials: true, // Allow cookies to be sent - methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - allowedHeaders: ["Content-Type", "Authorization"], - }), - ); -} +const corsOptions = { + origin: + process.env.NODE_ENV === "production" + ? process.env.ALLOWED_ORIGINS?.split(",") || [] + : ["http://localhost:5173", "http://localhost:3000"], + credentials: true, + methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + allowedHeaders: ["Content-Type", "Authorization"], + optionsSuccessStatus: 200, +}; + +app.use(cors(corsOptions)); // Body parsing middleware app.use(express.json()); @@ -70,13 +90,42 @@ app.use("/api", optionalAuthenticateUser); app.use("/api/notes", notesRoutes); app.use("/api/auth", authRoutes); -// ==================== HEALTH CHECK ==================== -app.get("/health", (req, res) => { - res.status(200).json({ +// ==================== IMPROVED HEALTH CHECK ==================== +app.get("/health", async (req, res) => { + let mongodbStatus = "Disconnected"; + let redisStatus = "Disconnected"; + + try { + const mongoose = await import("mongoose"); + mongodbStatus = + mongoose.default.connection.readyState === 1 + ? "Connected" + : "Disconnected"; + } catch (error) { + mongodbStatus = "Error"; + } + + try { + if (redisClientInstance && redisClientInstance.isReady) { + redisStatus = "Connected"; + } + } catch (error) { + redisStatus = "Error"; + } + + const healthStatus = { status: "OK", - message: "Server is running", timestamp: new Date().toISOString(), - }); + uptime: process.uptime(), + services: { + mongodb: mongodbStatus, + redis: redisStatus, + }, + environment: process.env.NODE_ENV || "development", + }; + + const isHealthy = mongodbStatus === "Connected"; + res.status(isHealthy ? 200 : 503).json(healthStatus); }); // ==================== PRODUCTION STATIC FILES ==================== @@ -87,36 +136,115 @@ if (process.env.NODE_ENV === "production") { }); } -// ==================== ERROR HANDLING ==================== +// ==================== IMPROVED ERROR HANDLING ==================== +class AppError extends Error { + constructor(message, statusCode, details = null) { + super(message); + this.statusCode = statusCode; + this.details = details; + this.isOperational = true; + Error.captureStackTrace(this, this.constructor); + } +} + app.use((err, req, res, next) => { console.error("Error:", err.message); + + if (err instanceof AppError) { + return res.status(err.statusCode).json({ + success: false, + message: err.message, + details: err.details, + ...(process.env.NODE_ENV === "development" && { stack: err.stack }), + }); + } + res.status(500).json({ - error: "Internal server error", - message: process.env.NODE_ENV === "development" ? err.message : undefined, + success: false, + message: + process.env.NODE_ENV === "production" + ? "Internal server error" + : err.message, + ...(process.env.NODE_ENV === "development" && { stack: err.stack }), }); }); +// ==================== GRACEFUL SHUTDOWN ==================== +let server = null; + +const gracefulShutdown = async (signal) => { + console.log(`Received ${signal}. Shutting down gracefully...`); + + const timeout = setTimeout(() => { + console.error("Forced shutdown after timeout"); + process.exit(1); + }, 10000); + + try { + const mongoose = await import("mongoose"); + if (mongoose.default.connection.readyState === 1) { + await mongoose.default.connection.close(); + console.log("MongoDB connection closed"); + } + + if (redisClientInstance && redisClientInstance.isReady) { + await redisClientInstance.quit(); + console.log("Redis connection closed"); + } + + if (server) { + server.close(() => { + console.log("Server closed successfully"); + clearTimeout(timeout); + process.exit(0); + }); + } else { + clearTimeout(timeout); + process.exit(0); + } + } catch (error) { + console.error("Error during shutdown:", error.message); + clearTimeout(timeout); + process.exit(1); + } +}; + // ==================== START SERVER ==================== const startServer = async () => { try { - // Connect to MongoDB await connectDB(); console.log("MongoDB connected successfully"); - // Connect to Redis try { - await connectRedis(); - console.log(" Redis connected successfully"); + const redis = await connectRedis(); + redisClientInstance = redis; + console.log("Redis connected successfully"); } catch (redisError) { - console.warn(" Redis connection failed, continuing without Redis"); + console.warn("Redis connection failed, continuing without Redis"); } - // Start Express server - app.listen(process.env.PORT, () => { + server = app.listen(process.env.PORT, () => { console.log("Server listening at port number: " + process.env.PORT); + console.log("Environment: " + (process.env.NODE_ENV || "development")); + console.log( + "Health check: http://localhost:" + process.env.PORT + "/health", + ); + }); + + process.on("SIGINT", () => gracefulShutdown("SIGINT")); + process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); + + process.on("uncaughtException", (error) => { + console.error("Uncaught Exception:", error); + gracefulShutdown("uncaughtException"); + }); + + process.on("unhandledRejection", (reason, promise) => { + console.error("Unhandled Rejection:", reason); + gracefulShutdown("unhandledRejection"); }); } catch (error) { - console.error(" Server startup failed:", error.message); + console.error("Server startup failed:", error.message); process.exit(1); } };