package main import ( "context" "fmt" "log" "net" "net/http" "os" "os/signal" "syscall" "time" "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" // IMPORTANT: Adjust these import paths to match your Go module path // (e.g., github.com/your-username/your-repo-name/internal/...) "github.com/axyut/niyamAPI/internal/config" "github.com/axyut/niyamAPI/internal/db" "github.com/axyut/niyamAPI/internal/handler" "github.com/axyut/niyamAPI/internal/service" "github.com/axyut/niyamAPI/internal/utils" _ "github.com/danielgtaylor/huma/v2/formats/cbor" // Huma format package for CBOR ) func main() { // 1. Initialize Uptime Tracking // Call this as early as possible in the main function to accurately track // the application's uptime from its true starting point. utils.InitUptime() // 2. Load Application Configuration // This step reads environment variables (including those from .env file via godotenv) // and parses them into a structured AppConfig. Fatal exit if essential config is missing. cfg, err := config.LoadConfig() if err != nil { log.Fatalf("FATAL: Failed to load application configuration: %v", err) } log.Printf("INFO: Application environment: %s", cfg.Environment) log.Printf("INFO: Configured API Public URL: %s", cfg.PublicURL) // 3. Initialize Database Client // Establish connection to MongoDB using the loaded configuration. // This is a critical dependency, so a fatal exit occurs on failure. dbClient, err := db.NewDBClient(cfg) // Pass configuration to DB client for connection URI if err != nil { log.Fatalf("FATAL: Failed to initialize database client: %v", err) } // Defer closing the database connection. This ensures the connection is // gracefully closed when the main function exits (e.g., on shutdown signal). defer func() { if err := dbClient.Close(); err != nil { log.Printf("ERROR: Error closing database client: %v", err) } }() // 4. Initialize Services (Business Logic Layer) // Services encapsulate the core business logic of your application. They depend on // external resources like the database client. svc := service.NewServices(dbClient, cfg) // Pass dbClient to the service layer // 5. Create new Chi router // Chi is a lightweight, idiomatic router for building HTTP services in Go. router := chi.NewMux() // Apply Chi middleware for request logging and panic recovery. // `middleware.Logger` provides structured logging for each incoming HTTP request, // showing method, path, status, and duration. router.Use(middleware.Logger) // `middleware.Recoverer` catches any panics that occur within handlers, // logs the stack trace, and prevents the entire server from crashing, // returning a 500 Internal Server Error to the client. router.Use(middleware.Recoverer) // 6. Create Huma API instance // Huma builds on top of Chi (via humachi adapter) to provide powerful features // like automatic OpenAPI 3.0 documentation generation and request/response validation. apiConfig := huma.DefaultConfig("Niyam API", "1.0.0") apiConfig.Info.Description = "API/Backend service for the Niyam application." api := humachi.New(router, apiConfig) // 7. Register API Handlers // Instantiate the Handlers struct, passing it all its essential dependencies: // application configuration, business services, and the database client. // Then, call its method to register all defined API endpoints with the Huma API. hndlrs := handler.NewHandlers(cfg, svc, dbClient) // Pass config, services, and dbClient to handlers hndlrs.RegisterHandlers(api) // // 8. Start HTTP Server // // The server listens on the port specified in the configuration. listenAddr := fmt.Sprintf(":%d", cfg.Port) // listener, err := net.Listen("tcp", listenAddr) // if err != nil { // log.Fatalf("FATAL: Server failed to listen on %s: %v", listenAddr, err) // } // defer listener.Close() // Ensure the listener is closed when main function exits. // // Log the actual server listening address (e.g., ":7860" or "[::]:7860") // // and the publicly accessible documentation URL, which uses the PublicURL from config. // log.Printf("INFO: Server listening on %s (Access docs at %s/docs)\n", listener.Addr().String(), cfg.PublicURL) // Dynamically Get the Actual Host and Port (for logging) listener, err := net.Listen("tcp", listenAddr) if err != nil { log.Fatalf("Server failed to listen on %s: %v", listenAddr, err) } defer listener.Close() // Extract just the port number from the listener's address _, portStr, err := net.SplitHostPort(listener.Addr().String()) if err != nil { log.Fatalf("Failed to parse listener address: %v", err) } publicBaseURL := fmt.Sprintf("http://localhost:%s", portStr) appEnv := os.Getenv("APP_ENV") if publicURL := os.Getenv("API_PUBLIC_URL"); publicURL != "" && appEnv != "development" { publicBaseURL = publicURL } // Use the constructed publicBaseURL in the log message log.Printf("Server listening on %s (Access docs at %s/docs)\n", listener.Addr().String(), publicBaseURL) // Configure the HTTP server with explicit timeouts for read, write, and idle operations. // This helps prevent resource exhaustion and improves server robustness. srv := &http.Server{ Addr: listener.Addr().String(), // Server address determined by listener Handler: router, // The Chi router handles all incoming requests ReadTimeout: 5 * time.Second, // Maximum duration for reading the entire request WriteTimeout: 10 * time.Second, // Maximum duration before timing out writes of the response IdleTimeout: 120 * time.Second, // Maximum amount of time to wait for the next request when keep-alives are enabled } // Start the HTTP server in a separate goroutine. // This allows the main goroutine to proceed to set up graceful shutdown. go func() { log.Printf("INFO: Starting HTTP server on %s...", srv.Addr) // srv.Serve() blocks until the server closes or an error occurs. if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { // Log a fatal error if the server fails to start or serve requests unexpectedly, // ensuring the application exits. log.Fatalf("FATAL: HTTP server failed: %v", err) } log.Println("INFO: HTTP server stopped.") }() // 9. Graceful Shutdown // Set up a channel to receive OS signals. This allows the application to respond // to termination requests (e.g., Ctrl+C, `docker stop`). quit := make(chan os.Signal, 1) // Notify the 'quit' channel upon receiving SIGINT (Ctrl+C) or SIGTERM (termination request). signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // Block the main goroutine until an OS signal is received on the 'quit' channel. sig := <-quit log.Printf("INFO: Received signal '%s'. Shutting down server gracefully...", sig) // Create a context with a timeout for the graceful shutdown process. // If the server doesn't shut down within this duration, it will be forcefully closed. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Ensure the context's cancel function is called to release resources. // Attempt to gracefully shut down the HTTP server. // Existing connections are given time to complete before the server is stopped. if err := srv.Shutdown(ctx); err != nil { log.Fatalf("FATAL: Server shutdown failed: %v", err) } log.Println("INFO: Server gracefully shut down.") }